/* FUNCTIONS FOR PRESENTING GOOGLE MAPS ON THE CCC WEB SITE (http://camdencyclists.org.uk/) 

	Copyright (c) George Coulouris, http://www.coulouris.net/  2007-9
	
	I'm happy for you to use the code for non-commercial purposes. Contact me for commercial use.
*/

// default initial map setup for Camden.
// these can be overridden by setting query parameters in the URL when opening a map
// E.g. ?centre=51.7,-0.12&zoom=13&maptype=G_HYBRID_MAP

initial_zoom_level = 14; // higher number means closer view; at 600px wide, may be overridden in CCCInitialMapState
			// 7 = width of New Mexico, 12 = San Francisco
initial_center = new GLatLng(51.531355,-0.138915); //  may be overridden in MenuDef
initial_map_type = G_NORMAL_MAP; //  may be overridden in MenuDef

var CCCTrackList = new Array(); // subscripted by text_id
var CCCRouteSets = new Array();	// subscripted by Category or Category_SubCat
var CCCInfoMarkerSets = new Array(); // suscripted by info marker set names
var CCCInitialInfo = null; // an optionally instantiated object to open an info marker
var CCCInitialFocusRoute = null; // text_id of a route that will be initially highlighted
var CCCCurrentHiRoute = null; // object reference for the currently highlighted route
var CCCCurrentFocusRoute = null; // object reference for the currently focussed route
var CCCCurrentMarker = null; // object reference for the currently open infomarker (if any)
var CCCInitiallyOnRoutes = null; // for args.onRoutes
var CCCInitiallyOffRoutes = null; // for args.offRoutes
var CCCInitiallyOnMarkers = null; // for args.onMarkers
var CCCInitiallyOffMarkers = null; // for args.offMarkers
var CCCStreetViewState = null; // for args.streetView
var CCCCurrentMapName;			// set when map is loaded or changed
var CCCCurrentMapType;			// set when map is loaded or changed
var CCCMenuId = null;		// set when menu is created
var CCCMenuDef = null;	// Will be read, either from the HTML page header or from database or from the URL arg.menu
var CCCMenuHtmlElement;	// will be instantiated as the menu HTMLElement
var CCCPendingMarkerShow;	// instantiated in args.showmarker
var CCCRoutePlanner = null;
var CCCStreetView = null;
var minStandZoom = 15; // stands won't be shown below this level
// var CCCGeoCoder;
var BrowserSupportsPolyHide = null; // set (once) and tested in function polyHide
var gmap;		// the Google Map reference returned by a GMap() call

var CCCMarkerEditing;	// will be instantiated with an instance of CCCSetUpInfoMarkerEditing() 
						// during the onload process (CCCfunctions file) when editing is permitted

// a unique id for each record added to the DB
// London Potters Bar 51.56839N, South Croydon 51.33211N, Hillingdon -0.5356693E, Dagenham 0.227365E
//     based on lat and lng - 4 digits each: when we used 10 digits, the value changed on way to DB

function polyShow (aPolyline) {
	if(BrowserSupportsPolyHide === null) 
		BrowserSupportsPolyHide = aPolyline.supportsHide();
	if(BrowserSupportsPolyHide && aPolyline.isHidden()) aPolyline.show();
	else gmap.addOverlay(aPolyline);
}

function polyHide (aPolyline) {
	if(BrowserSupportsPolyHide === null) 
		BrowserSupportsPolyHide = aPolyline.supportsHide();
	if(BrowserSupportsPolyHide) aPolyline.hide();
	else gmap.removeOverlay(aPolyline);
}

//when there is trouble with AJAX ...
function ajaxDiagnostics(request){
	var xmlDoc = request.responseXML;
	var response = request.responseText;
	GLog.write("AJAX says: readyState,status=" + request.readyState +',' + request.status + " response=" + response);	
}

function syncDBQuery(tableName, recordName, queryString, format) {
	if(!format) format = 'XMLJSON';
	var query = '?x=QUERY&dbCommand=' + queryString + '&tableName=' + tableName + 
		'&recordName=' + recordName + '&format=' + format;
	var result = null;
	var request = GXmlHttp.create();
// GLog.write(query);

	request.open('GET', db_url_string + query, false); // last param = false for sync
	request.send(null);	
	// for synchronous case only
	if(request.status == 200) {
// GLog.write (request.responseText);
	return request.responseXML ? request.responseXML : GXml.parse(request.responseText);
	} else {
		ajaxDiagnostics(request);
		return false;
	}
}

function aSyncDBQuery(tableName, recordName, queryString, format, callback) {
	if(!format) format = 'XMLJSON';
	var query = '?x=QUERY&dbCommand=' + queryString + '&tableName=' + tableName + 
		'&recordName=' + recordName + '&format=' + format;
	var result = null;
	var request = GXmlHttp.create();
	// for asynchronous case only
	request.onreadystatechange = function () {
		if (request.readyState == 4) {
			if(request.status != 200) {
				var response = request.responseText;
					GLog.write("AJAX says: " + response);						
			} else {
				var xml = request.responseXML ? request.responseXML : GXml.parse(request.responseText);
				
				callback(xml, request.responseText);
			}
		} // end if (request.readyState == 4) 
	}; // end handler function;
	request.open('GET', db_url_string + query, true); // last param = true for async
	request.send(null);	
}

function syncGetFile(url) { // like GDownloadUrl() but synchronous
	var request = GXmlHttp.create();
	request.open('GET', url, false); // last param = false for sync
	request.send(null);	
	// for synchronous case only
	if(request.status == 200) {
			return request.responseText;
	} else {
		return false;
	}
}

// See DB table for MenuDef format (JSON)

function CCCMenu () {
// Constructor for the HTML menu

	this.menuHTML = null;	// initialized at the end of this constructor
	this.menuDefObj;		// instantiated in loadMenu and used in buildMenu
	this.expectedDBResults = [];
	this.receivedDBResults = [];
	
	this.loadMenu = function (menuDefObj) { 
		this.menuDefObj = menuDefObj;
		// This is pass 1 to start the process of loading all the routes table entries need into CCCTrackSets, etc.
		// This is done asynchronously for each folder in the menu. 
		// Pass 2 (this.buildMenu) is invoked from this.checkLoadedAndBuildMenu
		// when all the asynchronous lookups of route data have completed
		
		// Pass 0, just to set up this.expectedDBResults
		for(var aCategory in menuDefObj) {
			if(aCategory == 'MarkerSets') {
			// just fills up the this.expectedDBResults array
			} else {
				for(var aSubCat in menuDefObj[aCategory]) {
					if(aSubCat != 'colour' &&
						aSubCat != 'closed' && 
						aSubCat != 'menuString') { // skip these elements of the menudef
					if(aSubCat == 'routes') { // it's an explicit route text_ids
							var listId = aCategory + '_routes';
							this.expectedDBResults.push(listId);
						} // if(aSubCat == 'routes') 
						else { // it's a real subcategory
							var listId = aCategory + '_' + aSubCat;
							this.expectedDBResults.push(listId);
						} // real subcategory
					}
				} // for aSubCat
			}
		} // for var aCategory
		// Pass 1 starts the async loading of routes
		for(var aCategory in menuDefObj) {
			if(aCategory == 'MarkerSets') {
			// leave the processing and retrieval of markers to the second pass
			} else {
				var colourName = menuDefObj[aCategory].colour; 
				colourName = (colourName == null) ? 'orange' : colourName;
				if(!CCCRouteSets[aCategory])
					CCCRouteSets[aCategory] = new CCCRouteSet(aCategory, INITIALLYOFF, colourName); // create a set of routes for each category
				for(var aSubCat in menuDefObj[aCategory]) {
					if(aSubCat != 'colour' &&
						aSubCat != 'closed' && 
						aSubCat != 'menuString') { // skip these elements of the menudef
						if(aSubCat == 'routes') { // it's an explicit route text_ids
							var routeSet = menuDefObj[aCategory]['routes'];
							var listId = aCategory + '_routes';
							var searchExpression = 'category="' + aCategory + '" AND ' ;
							var n = 0;
							var initialRouteStates = [];
							for (var aRouteId in routeSet) {
								if(n++ > 0) searchExpression += ' OR ';
								searchExpression += 'text_id="' + aRouteId + '"';
							}
							searchExpression += ' ORDER BY "id"';
							if(!CCCRouteSets[listId]) 
								CCCRouteSets[listId] = new CCCRouteSet(listId, INITIALLYON, colourName); 
							var routeList = this.loadAsyncRouteSpecs(searchExpression, aCategory, listId, routeSet);
						} // if(aSubCat == 'routes') 
						else { // it's a real subcategory
							var listId = aCategory + '_' + aSubCat;
							var initialRouteState = 
								menuDefObj[aCategory][aSubCat][1] == 'on' ? INITIALLYON : INITIALLYOFF;
							var searchExpression = 
								'category="' + aCategory + '" AND ' + 'subcat="' + aSubCat + '"' +
								' ORDER BY "id"';
							// create a set of routes for each category_subcategory
							CCCRouteSets[listId] = new CCCRouteSet(listId, initialRouteState, colourName); 
							CCCRouteSets[aCategory].subCategories.push(aSubCat);
							// start the loading of the route details
							var routeList = this.loadAsyncRouteSpecs(searchExpression, aCategory, listId);
						} // real subcategory
					}
				} // for aSubCat
			}
		} // for var aCategory
	} // this.loadMenu

	this.buildMenu = function () { 
	// Pass 2, after all route details are loaded
	// NB this is called asynchronously after the menu def has loaded
	// menuDefObj is a JSON representation of the hierarchic menu
		
		var menuDefObj = this.menuDefObj;
		var theMenuHTML = "<div id='CCCmenu' class='treeview'>"; //starting from scratch
		
		for(var aCategory in menuDefObj) {
			if(aCategory == 'MarkerSets') {
				var markerSets = menuDefObj[aCategory];
				for (var aMarkerSet in markerSets) {
					var colourName = (markerSets[aMarkerSet].colour == null) ? 'orange' : markerSets[aMarkerSet].colour;
					var menuString = markerSets[aMarkerSet].menuString;
					if (!menuString) menuString = aMarkerSet;
					if(!CCCInfoMarkerSets[aMarkerSet]) {
						CCCInfoMarkerSets[aMarkerSet] = new CCCInfoMarkerSet(aMarkerSet, colourName, 
							markerSets[aMarkerSet].initially, markerSets[aMarkerSet].labels);
					}
					var menuIcon = CCCIconSets.markerIconSets[aMarkerSet]._menuURL;
					var colour = markerSets[aMarkerSet].colour;
					if(colour == null) colour = 'orange';
				
					theMenuHTML += '\n<li>' + this.makeMenuItemHTML(MARKERSET, aMarkerSet, 
						'<img src=' + menuIcon + '> ' + menuString, colourTable[colour].code);
				}
			} else {
				var colourName = menuDefObj[aCategory].colour; 
				colourName = (colourName == null) ? 'orange' : colourName;
				var colour = colourTable[colourName].code; 
				var menuColourKey = '<image src="' + MarkerIcons_path + 'key-' + colourName + '.png"> ';
				var menuString = (menuDefObj[aCategory].menuString) ? 
						menuDefObj[aCategory].menuString : aCategory + ' routes' ;
				var initiallyOpen = !(menuDefObj[aCategory].closed);
				var hiddenMenu = (menuString == 'hidden');

				if (!hiddenMenu) {
					theMenuHTML += '\n<li>' + this.makeMenuItemHTML(LISTHEAD, aCategory, 
						menuColourKey + menuString, colour) +'\n';
					theMenuHTML += (initiallyOpen) ? '<ul rel="open">\n' : '<ul rel="closed">\n' ;
					theMenuHTML += this.makeMenuItemHTML(SHOW, aCategory, '', colour); 
				}

				for(var aSubCat in menuDefObj[aCategory]) {
					if(aSubCat != 'colour' &&
						aSubCat != 'closed' && 
						aSubCat != 'menuString') { // skip these elements of the menudef
						if(aSubCat == 'routes') { // it's an explicit route text_ids
							var routeSet = menuDefObj[aCategory]['routes'];
							var listId = aCategory + '_routes';
							var searchExpression = 'category="' + aCategory + '" AND ' ;
							var n = 0;
							var routeList = CCCRouteSets[listId].members;
							for (var RouteId in routeSet) {
								var routeDbRec = CCCTrackList[RouteId].routeDbRec;
								var menuString = routeDbRec.menuString;
								var initialRouteState = (routeSet[RouteId] == 'on') ? INITIALLYON : INITIALLYOFF;
								menuString = menuString.length > 0 ? menuString : RouteId;
								if (!hiddenMenu) {
									var aMenuItem = 
										this.makeMenuItemHTML(initialRouteState, RouteId, menuString, colour);
									theMenuHTML += aMenuItem;
								}
							} // for i
						} // if(aSubCat == 'routes') 
						else { // it's a real subcategory
							var initialMenuState = menuDefObj[aCategory][aSubCat][0];
							var initialRouteState = menuDefObj[aCategory][aSubCat][1] == 'on' ? INITIALLYON : INITIALLYOFF;
							var folderString = menuDefObj[aCategory][aSubCat][2];
							if(folderString == null) folderString = aSubCat;
							var listId = aCategory + '_' + aSubCat;
							// create a set of routes for each category_subcategory		
							var routeList = CCCRouteSets[listId].members;
							if(routeList.length > 0 && !hiddenMenu) {
								routeList.sort(function(a, b) {
								// the elements of a routeList are route textid's
									var aMenuString = CCCTrackList[a].routeDbRec.menuString;
									var bMenuString = CCCTrackList[b].routeDbRec.menuString;
									return aMenuString.localeCompare(bMenuString);
									});
								theMenuHTML += '\n<li>' + 
									this.makeMenuItemHTML(LISTHEAD, listId, folderString, colour) +
									'\n<ul rel="' + initialMenuState + '">\n';									theMenuHTML += this.makeMenuItemHTML(SHOW, listId, '', colour); 
							}
							for (var i = 0; i < routeList.length; i++) {
								var RouteId = routeList[i];
								var routeDbRec = CCCTrackList[RouteId].routeDbRec;
								var menuString = routeDbRec.menuString;
								menuString = menuString.length > 0 ? menuString : RouteId;
								if (!hiddenMenu) {
									var aMenuItem = 
										this.makeMenuItemHTML(initialRouteState, RouteId, menuString, colour);
									theMenuHTML += aMenuItem;
								}
							} // for i
							if (routeList.length > 0 && !hiddenMenu) theMenuHTML += '</ul ' + aSubCat + '>\n';
						} // real subcategory
					}
				} // for aSubCat
			}
			if (!hiddenMenu) theMenuHTML += '</ul ' + aCategory + '>\n';
		} // for var aCategory
		theMenuHTML += "</div>";
//  GLog.write(theMenuHTML);

		gmap.routeMenuControl.load(theMenuHTML);
		CCCMenuHtmlElement = document.getElementById('CCCmenu');
		ddtreemenu.createTree("CCCmenu", false);	// call to simpletreemenu.js 
													// second param controls 'persist' of menu state
		gmap.routeMenuControl.changed();			// trigger a check on the menu length
		
		// we do this here to give time for GMenuMapTypeControl code to load
		CCC_Map.setupTypeControl();
	}
	
	this.makeMenuItemHTML = function (type, id, menuString, colour) { 
		// InitiallyOff is used only for the SHOW items
	// each item is denoted by a <span>
		var theItemHTML;

		switch (type) {
			case MARKERSET:				
				theItemHTML = '<span id=\"' + id +'\" ' +
					'style=\"color:' + colour + '; cursor:pointer\" ' +  				
					'onclick=\"CCCInfoMarkerSets[\'' + id + '\'].show()\" ' + 				
					'onmouseover=\"this.style.textDecoration=\'underline\';' +
					'CCCInfoMarkerSets[\'' + id + '\'].showLabels()\" ' +
					'onmouseout=\" this.style.textDecoration=\'none\';' +
					'CCCInfoMarkerSets[\'' + id + '\'].hideLabels()\"> ' +
					menuString + '</span>' + 
					'<span style=\"cursor:pointer\" >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>' + 
					'<span id =\"toggle' + id +'\" style=\"color:black; cursor:pointer\"' +
					'onmouseover=\"this.style.textDecoration=\'underline\'\" ' +
					'onmouseout=\"this.style.textDecoration=\'none\'\" ' +
					'onclick=\"CCCInfoMarkerSets[\'' + id + '\'].toggle()\" ' + 				
					'><i>Show</i>' + '</span>\n' + 
					'<br>\n';
				break;
			case LISTHEAD:
				theItemHTML = '<span id=\"' + id +'\" ' + 'style=\"color:' + colour + '\" ' +
				'title=\"Reveal/hide list below\" ' + 
				'onmouseover=\"this.style.textDecoration=\'underline\';' +
				'CCCRouteSets[\'' + id + '\'].showMarkerLabels()\" ' +
				'onmouseout=\"this.style.textDecoration=\'none\';' +
				'CCCRouteSets[\'' + id + '\'].hideMarkerLabels()\"> ' +
				menuString + '</span>\n'; 
				break;
			case SHOW:
				var showColour = CCCDimmedColour;
				var hideColour = CCCDimmedColour;
				theItemHTML = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' +
				'<span id=\"' + 'show_' + id +'\" ' + 'style=\"color:' + showColour + '\" ' +
				'title = "Show all in this folder"' +
				'onclick=\"CCCRouteSets[\'' + id + '\'].showAll()\" ' +
				'onmouseover=\"this.style.textDecoration=\'underline\'\" ' +
				'onmouseout=\"this.style.textDecoration=\'none\'\"> <i>(show all)</i></span>\n' + 
				'&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' +
				'<span id=\"' + 'hide_' + id +'\" ' + 'style=\"color:' + hideColour + '\" ' +
				'title = "Hide all in this folder"' +
				'onclick=\"CCCRouteSets[\'' + id + '\'].hideAll()\" ' +
				'onmouseover=\"this.style.textDecoration=\'underline\'\" ' +
				'onmouseout=\"this.style.textDecoration=\'none\'\"> <i>(hide all)</i></span>\n' + 
				'<br>';
				break;
			case INITIALLYOFF:
				colour = CCCDimmedColour;
			case INITIALLYON:
				theItemHTML = '<span id=\"' + id +'\" ' +
				'title=\"Click to focus on this route\"' + 
				'style=\"color:' + colour + '; cursor:pointer;\" ' +  
				'onclick=\"CCCTrackList[\'' + id + '\'].menuClick();\" ' + 
				'onmouseover=\"this.style.textDecoration=\'underline\'; ' +
				'CCCTrackList[\'' + id + '\'].menuMouseOver();\" ' + 
				'onmouseout=\"this.style.textDecoration=\'none\'; ' +
				'CCCTrackList[\'' + id + '\'].menuMouseOut()\"> ' + 
				menuString + '</span>' +
				'<br>\n'; 
				break;
		}
		return theItemHTML;
	}

	this.loadAsyncRouteSpecs = function (aSearchExpression, aCategory, listId, aRouteSet /* optional */) { 
	// returns a routeList built from items retrieved from the SQL database server 
		var recordName = 'routespec';
		var queryString = 'SELECT * FROM ' + DBTables.routes + ' WHERE ' + aSearchExpression;
		var routeList = [];
		var me = this;
		
//		this.expectedDBResults.push(listId);
		aSyncDBQuery(DBTables.routes, recordName, queryString, 'XMLJSON', function(XMLResult, textResult) {
			var routeSpecs = XMLResult.documentElement.getElementsByTagName(recordName);
			var initialRouteState = CCCRouteSets[listId].initialState;
			var colourName = CCCRouteSets[listId].colour;
			if (routeSpecs.length){
				for (var i = 0; i < routeSpecs.length; i++) {
					var routeJSON = routeSpecs[i].getAttribute("JSON");
					var aRouteSpec = eval('(' + routeJSON + ')');
					for( var anAttribute in aRouteSpec) 
						aRouteSpec[anAttribute] = decodeURIComponent(aRouteSpec[anAttribute]);
					routeList.push(aRouteSpec);
				} //for
			} //if			
			for (var i = 0; i < routeList.length; i++) {
				var routeDbRec = routeList[i];
				var RouteId = routeDbRec.text_id;
				// override the default colour if there is an explicit setting:
				if(aRouteSet) initialRouteState = (aRouteSet[RouteId] == 'on') ? INITIALLYON : INITIALLYOFF; 			
				var aTrack = new CCCTrack(routeDbRec, initialRouteState, colourName, gmap);
				CCCTrackList[RouteId] = aTrack;
				CCCRouteSets[aCategory].addRoute(RouteId);
				CCCRouteSets[listId].addRoute(RouteId);
			} // for i
			me.checkLoadedAndBuildMenu(listId);
		} );

	}
	
	this.checkLoadedAndBuildMenu = function(aRouteSet) {
		this.receivedDBResults.push(aRouteSet);
		if(this.receivedDBResults.length == this.expectedDBResults.length) {
			this.buildMenu();
			
			// for routes in args.inRoutes
			if(CCCInitiallyOnRoutes) 
				for (var i = 0; i < CCCInitiallyOnRoutes.length; i++) {
					CCCTrackList[CCCInitiallyOnRoutes[i]].loadOrShow();
				}
			// for the route selected in ?hilight=EWRoute type args:
/* 			if(CCCInitialFocusRoute) { */
/* 				var theRoute = CCCTrackList[CCCInitialFocusRoute]; */
/* 				if(theRoute) { */
/* 					if(!theRoute.loaded)  */
/* 						theRoute.loadTrack(); // asynch call */
/* 				} else */
/* 					alert(CCCInitialFocusRoute + ': route not found.'); */
/* 			} // CCCInitialFocusRoute */
		}
	}

} // end function CCCMenu

function CCCLoadMenuDef (aMenuName) { 
	// returns a menu definition retrieved from the SQL database server 
	var recordName = 'menudef';
	var queryString = 'SELECT * FROM ' + DBTables.menudefs + ' WHERE name=\"' + aMenuName +'\"';
	var results = [];

	var xmlResults = syncDBQuery(DBTables.menudefs, recordName, queryString, 'XML');
	var menuDefRec = 
		xmlResults.documentElement.getElementsByTagName(recordName)[0];
	if(menuDefRec) {
		results['menuDef'] = eval('(' + menuDefRec.getAttribute('definition') + ')');
		var initialMapState = menuDefRec.getAttribute('initialMapState');
		if(initialMapState) results['initialMapState'] = eval('(' + initialMapState + ')');
		
		return results;
	} else {
		alert(aMenuName + ': menu not defined');
	}
}

// sets of routes whose labels can be toggled
function CCCRouteSet(aMenuItemId, initialState, colourName) {
	this.id = aMenuItemId;
	this.members = new Array();
	this.subCategories = new Array();
	this.hidden = (initialState == INITIALLYOFF);
	this.initialState = initialState; 
	this.colour = colourName;
	
	this.addRoute = function (aRouteId) {
		this.members.push(aRouteId);
		if(!this.colour) this.colour = CCCTrackList[aRouteId].colour;
	}
	
	 this.showMarkerLabels = function () {
		for(aMember in this.members) {
			if(!CCCTrackList[this.members[aMember]].hidden) CCCTrackList[this.members[aMember]].marker.showLabel();
		}
	}

	this.hideMarkerLabels = function () {
		for(aMember in this.members) 
			if(!CCCTrackList[this.members[aMember]].hidden) CCCTrackList[this.members[aMember]].marker.hideLabel();
	}
	
	this.showAll = function () {
		for (aMember in this.members) {
			var aRoute = CCCTrackList[this.members[aMember]];
			if(aRoute.hidden) aRoute.loadOrShow();
		}
		this.hidden = false;
	}

	this.hideAll = function () {
		for (aMember in this.members) {
			var aRoute = CCCTrackList[this.members[aMember]];
			if(!aRoute.hidden) aRoute.hide();
		}
		this.hidden = true;
	}
}

// Constructor for our track objects
function CCCTrack(routeDbRec, initialState, colourName, map) { 

	this.routeDbRec = routeDbRec;
	this.initialState =initialState;		// currently one of (INITIALLYON, INITIALLYOFF)
	this.colourName = colourName;
	this.colour = colourTable[colourName];
	this.map = map;							// a Google Map, as returned by the GMap() function
	this.width = CCCTrackWidth;							// in pixels
	this.marker = new CCCMarker(this);
	
	this.polylines = new Array(); 			// an array of polylines (i.e. an array of vectors of GLatLng's)
	this.polyLengths = new Array(); 
	this.segmentNames = new Array();
	this.arrows = new Array();
	this.bounds = null;  					// will be instantiated this.getBounds() as a GLatLngBounds;
	this.hidden = true;
	this.hilighted = false;
	this.focussed = false;
	this.OsmMode = false;
	this.polysVisible = false;
	this.menuItem = null;					// used in mouseClick();
	this.loaded = false;
	this.distance = 0;	// will hold computed distance along the track, in metres
	this.midPoint = null;
	this.tempHidden = false;
	var me = this;
	
	// class methods
	CCCTrack.deactivateAll = function() {
		for (aTrack in CCCTrackList) 
			if(!CCCTrackList[aTrack].hidden) CCCTrackList[aTrack].deactivate();
	}

	CCCTrack.activateAll = function() {
		for (aTrack in CCCTrackList) 
			if(!CCCTrackList[aTrack].hidden) CCCTrackList[aTrack].activate();
	}
	
	CCCTrack.tempHideAll = function() {
		for (aTrack in CCCTrackList) {
			if(!CCCTrackList[aTrack].hidden)
				for (j = 0; j < CCCTrackList[aTrack].polylines.length; j++) polyHide(CCCTrackList[aTrack].polylines[j]);
			CCCTrackList[aTrack].tempHidden = true;
		}
	}
	
	CCCTrack.unTempShowAll = function() {
		for (aTrack in CCCTrackList) {
			if(CCCTrackList[aTrack].tempHidden)
				for (j = 0; j < CCCTrackList[aTrack].polylines.length; j++) polyShow(CCCTrackList[aTrack].polylines[j]);
			CCCTrackList[aTrack].tempHidden = false;
		}
	}
	
	CCCTrack.redrawVisibleTracks = function() {
		for (aTrack in CCCTrackList) {
			if(!CCCTrackList[aTrack].hidden && CCCTrackList[aTrack] != CCCCurrentHiRoute) {
				if((CCCCurrentMapType == OSM_CYCLE_MAP) && CCCTrackList[aTrack].OsmMode) {
					if(CCCTrackList[aTrack].polysVisible) CCCTrackList[aTrack].hidePolys();
				} else {
					if(!CCCTrackList[aTrack].polysVisible) CCCTrackList[aTrack].showPolys();
				}
			}
		}
	}
	
	CCCTrack.getChangedOn = function () {
		var changedOn = "";
		for (aRoute in CCCTrackList) {
			if(CCCTrackList[aRoute].initialState == INITIALLYOFF && !CCCTrackList[aRoute].hidden) 
				changedOn += "," + aRoute;
		}
		return changedOn.substring(1,changedOn.length);
	}
	
	CCCTrack.getChangedOff = function () {
		var changedOff = "";
		for (aRoute in CCCTrackList) {
			if(CCCTrackList[aRoute].initialState == INITIALLYON && CCCTrackList[aRoute].hidden) 
				changedOff += "," + aRoute;
		}
		return changedOff.substring(1,changedOff.length);
	}

	CCCTrack.synthesiseTrack = function (xmlRouteDoc) {
	// create a route with a 'fake' routeDbRec and add it to the CCCTrackList: 
	// used for routes made by the CycleStreets Route planner
		var routeDbRec = new Object();
		var theSegments = [];
		var theElevations = '';
		var theMarkers = xmlRouteDoc.getElementsByTagName("marker");
		
		if(theMarkers == null || theMarkers.length < 2) {
			alert('An error occurred.');
			return false;
		}
		 
		var plan = theMarkers[0].getAttribute("plan") == "manchest" ? "balanced" : theMarkers[0].getAttribute("plan");
		var finishLoc = new GLatLng(theMarkers[0].getAttribute("finish_latitude"), 
									theMarkers[0].getAttribute("finish_longitude"));
	  	routeDbRec.text_id = 'generatedroute#' + theMarkers[0].getAttribute("id");
	  	routeDbRec.menuString = theMarkers[0].getAttribute("name");
	  	routeDbRec.camRouteNumber = theMarkers[0].getAttribute("itinerary");
		camRouteLink = "http://camden.cyclestreets.net/journey/" + routeDbRec.camRouteNumber + "/";
	  	routeDbRec.distance = 0;	// will be the accumulated distance in metres
	  	routeDbRec.category = "Generated";
	  	routeDbRec.title = routeDbRec.category + ': ' + routeDbRec.menuString;
	  	routeDbRec.description = 
	  		"<p><i><a href=\"" + camRouteLink +"\" target = \"_blank\">Route No. " + 
	  		routeDbRec.camRouteNumber + "</a>" +
	  		" generated by <a href=\"http://www.cyclestreets.net/\"> " +
	  		"CycleStreets.net's Journey Planner</a>, " +
	  		"using the <a href=\"http://www.openstreetmap.org/\"> OpenStreetMap</a>" + 
	  		" community mapping database.<br>" + 
	  		"NB: The CycleStreets system is in Beta. " +
	  		"Route quality cannot be guaranteed.</i>" ;
	  	
		var segNameList = '', 
			previousSegDescription;
/*			totalHeight = 0,*/
/*			totalDistance = 0,*/
/*			theInclines = [];*/
			
			for (var i=1; i<theMarkers.length; i++) {
	  		// check for error report
	  		var markerType = theMarkers[i].getAttribute("type");
	  		if(markerType && markerType == "error") return false;
	  	
	  		// get the road names
	  		var segName = theMarkers[i].getAttribute("name"), 
	  			segWalk= theMarkers[i].getAttribute("walk"),
	  			segDistance = theMarkers[i].getAttribute("distance"),
	  			shortSegDescription, 
	  			segDescription = (segName.substr(0,3) == "Way") ? segName + '&nbsp;&nbsp;' : segName,
	  			segElevations = theMarkers[i].getAttribute("elevations"), 
/*	  			segDistances = theMarkers[i].getAttribute("distances"), */
	  			segHeight;
	  			
	  		theElevations += ',' + segElevations;
/*	  		if(segElevations && segDistances) {*/
/*	  			segElevations = segElevations.split(',');*/
/*	  			segDistances = segDistances.split(',');*/
/*	  			segHeight = segElevations[segElevations.length-1] - segElevations[0];*/
/*	  			totalHeight += segHeight;*/
/*	  			totalDistance += (segDistance - 0);*/
/*	  			for(var j = 1; j < segElevations.length; j++) {*/
/*	  				var h = segElevations[j] - segElevations[j-1], d = segDistances[j];*/
/*	  				if(h > 6)	// too unreliable if less than this height */
/*	  					theInclines.push((h/d)*100);*/
/*	  			}*/
/*	  		}*/
	  		
	  		routeDbRec.distance += segDistance - 0;
	  		// get the route segments
	  		var pointString = theMarkers[i].getAttribute("points");
	  		var segPoints = [];
	  		if(pointString) {
	  			var points = pointString.split(" ");
	  			for (var j=0; j< points.length; j++) {
	  				var aPoint = points[j].split(",");
	  				
	  				segPoints.push(new GLatLng(aPoint[1], aPoint[0]));
	  			}
	  		}
	  		theSegments.push({'points':segPoints, 'name':segName, 'walk': (segWalk==1)});
	// Need to record the walk distance and incline for each segment and then describe them
	  		if(segDescription != previousSegDescription) {
	  			var walkDescription = '';
	  			if(segWalk==1) {
	  				walkDescription += '<i style = "color:red"> dismount, ' + segDistance + ' metres</i>';
	  			}
/*	  			maxIncline = 0;*/
/*	  			for(var j = 0; j < theInclines.length; j++) {*/
/*	  				var incline = theInclines[j];*/
/*	  				maxIncline = maxIncline > incline ? maxIncline : incline;*/
/*	  			}*/
/*	  			if(maxIncline > 3) {*/
/*	  				walkDescription += '<i style = "color:red"> uphill, avg: ' + */
/*	  					parseInt((totalHeight/totalDistance)*100) + '% for ' + totalDistance + */
/*	  					' metres, steepest: ' + parseInt(maxIncline) + '%</i>';*/
/*	  			}*/
/*	  			theInclines = [];*/
/*	  			totalHeight = 0;*/
/*	  			totalDistance = 0;*/
	  			segNameList += segDescription + walkDescription + '<br>';
	  			shortSegDescription = segDescription.substring(0, segDescription.indexOf(', ['));
	  		} 
	  		previousSegDescription = segDescription;
	  	}
	  	
// GLog.write(theElevations);
		var elevations = theElevations.split(',');
	  	var climbing = descending = 0;
	  	var diff;
	  	for(var i = 2; i < elevations.length; i++) {
	  		diff = elevations[i] - elevations[i-1];
// GLog.write(diff);
	  		if(diff > 0) climbing += diff;
	  		else descending += diff;
	  	}

	  	routeDbRec.description += '<p>Route type requested: <i>' + plan + 
								';&nbsp;&nbsp;quietness: ' + theMarkers[0].getAttribute("quietness")  + '%</i><br/>';
		var miles = (routeDbRec.distance * 0.00062137).toFixed(1);
		routeDbRec.description += 'Distance: <i>' + miles  + ' miles</i>; ';
		routeDbRec.description += 'Climbing: <i>' + climbing  + ' metres</i>; ';
		routeDbRec.description += 'Descending: <i>' + (0-descending)  + ' metres</i></p>';

	  	routeDbRec.description += segNameList;
	  		  	
	  	var theTrack = new CCCTrack(routeDbRec, INITIALLYOFF, 'orange', gmap);
	  	CCCTrackList[routeDbRec.text_id] = theTrack;
	  	theTrack.addTrackElements(theSegments);
	  	theTrack.loaded = true;
	  	theTrack.marker.instantiateMarker();
	  	theTrack.show();
	  	theTrack.focus();
	  	// show stands near the finishing point of the route:
	  	CCCInfoMarkerSets["Stands"].showMarkersAround(finishLoc);
	  	return true;
	}

	this.addTrackElements = function (trackElements) {
		// adds an array of segments to the track
		var polyline, i, j;
		for (i = 0; i < trackElements.length; i++) {
			if(trackElements[i].walk)
				polyline = new GPolyline(trackElements[i].points, this.colour.code, this.width-3, this.colour.opacity);
			else
				polyline = new GPolyline(trackElements[i].points, this.colour.code, this.width, this.colour.opacity);
			this.polylines.push(polyline);
			var segLen = polyline.Distance();	// .Distance is a method of epoly.js (see libraries.js)
			this.polyLengths[i] = segLen;
			this.segmentNames[i] = trackElements[i].name;
			
			// compute distance excluding segments with 'xx' in their names
			if(!this.segmentNames[i] || !NoDistanceRegExp.test(this.segmentNames[i])) 
				this.distance += segLen;
			// prepare to place arrows on segments with '->' or '<-' in their names
			if(this.segmentNames[i]) {
				if(ForwardArrowRegExp.test(this.segmentNames[i])) {
					var p1Index = parseInt(this.polylines[i].getVertexCount()/2) + 1;
					var bearing = this.polylines[i].Bearing(p1Index-1, p1Index);
					var tooltip = this.segmentNames[i].slice(0, this.segmentNames[i].indexOf('|'));
					this.arrows.push({'position': this.polylines[i].getVertex(p1Index), 
						'bearing': bearing, 'tooltip': tooltip});
				} else if(BackwardArrowRegExp.test(this.segmentNames[i])) {
					var p1Index = parseInt(this.polylines[i].getVertexCount()/2);
					var bearing = this.polylines[i].Bearing(p1Index+1, p1Index);
					var tooltip = this.segmentNames[i].slice(0, this.segmentNames[i].indexOf('|'));
					this.arrows.push({'position': this.polylines[i].getVertex(p1Index), 
						'bearing': bearing, 'tooltip': tooltip});
				}
			}
			
		}
		var i, midLen = this.distance/2, l = 0, d;
		for (i = 0; i < this.polyLengths.length; i++) {
			if(l + this.polyLengths[i] > midLen) break;
			l += this.polyLengths[i];
		}
		if(this.routeDbRec.category == 'Generated') this.midPoint = this.polylines[i].getVertex(0);
		else this.midPoint = this.polylines[i].GetPointAtDistance(midLen-l); 
					// .GetPointAtDistance is a method of epoly.js (see libraries.js)
	}
	
	this.loadOrShow = function () {
		if (this.loaded) {
			this.show(); 
			if(CCCInitialFocusRoute == this.routeDbRec.text_id) this.focus();
		} else this.loadTrack();		// NB this is asynchronous
		this.updateMenu();
	}

	this.menuClick = function () {
		if(this.hidden) {
			CCCInitialFocusRoute = this.routeDbRec.text_id;
			this.loadOrShow();
		} else {
			if(this.focussed) {
				this.defocus();
			} else {
				this.focus(); 
			}
		} 
	}

	this.menuMouseOver = function () {
	// Hilight the track
		if (!this.hidden) {
			this.hilight();
		}
	}
	
	this.menuMouseOut = function () {
	// lowlight the track	
		if (!this.hidden) {
			this.lowlight();
		}
	}
	
	this.showPolys = function () {
		for (j = 0; j < this.polylines.length; j++) {
			polyShow(this.polylines[j]);
		}
		this.polysVisible = true;
	}

	this.hidePolys = function () {
		for (j = 0; j < this.polylines.length; j++) {
			polyHide(this.polylines[j]);
		}
		this.polysVisible = false;
	}
	
	this.redrawPolys = function () {
		if(!this.polysVisible) this.showPolys();
		for (j = 0; j < this.polylines.length; j++) {
			this.polylines[j].redraw(true);
		}
	}
	
	this.focus = function (point) { // the point parameter is null unless we were called from a Polyline click event
		if(!this.menuItem) 
			this.menuItem = document.getElementById(this.routeDbRec.text_id);
		var infoWin = gmap.getInfoWindow();
		if(this.hidden) return false;
		if(CCCCurrentFocusRoute) CCCCurrentFocusRoute.defocus(); // only one focussed route is possible
		this.marker.hideLabel(); // in case it was showing
		
		if(this.menuItem) this.menuItem.style.color = CCCFocusColour.code;
		this.changeAppearance(CCCFocusColour);
		this.redrawPolys();
		this.showRouteMarkers();
		if(point) {
			gmap.openInfoWindowTabsHtml(point, this.marker.getInfoTabsHTML(), {maxWidth: 300});
		} else { // we were called by a click from the menu, so take steps to make the marker and infowin visible
			var mapBounds = gmap.getBounds();
			if(!mapBounds.contains(me.marker.position)) {
				var xOffset1 = this.marker.position.lng() - mapBounds.getNorthEast().lng(),
					yOffset1 = this.marker.position.lat() - mapBounds.getNorthEast().lat(),
					xOffset2 = this.marker.position.lng() - mapBounds.getSouthWest().lng(),
					yOffset2 = this.marker.position.lat() - mapBounds.getSouthWest().lat(),
					deltaX = 0, deltaY = 0;
				if(xOffset1 > 0) deltaX = -1; else if(xOffset2 < 0) deltaX = 1;
				if(yOffset1 > 0) deltaY = 1; else if(yOffset2 < 0) deltaY = -1;
				var newCenter = gmap.getCenter();
				if(deltaX != 0) newCenter = new GLatLng(newCenter.lat(), me.marker.position.lng());
				if(deltaY != 0) newCenter = new GLatLng(me.marker.position.lat(),newCenter.lng());
				gmap.setCenter(newCenter);
			} 
			this.marker.GMarker.openInfoWindowTabsHtml(this.marker.getInfoTabsHTML(), {maxWidth: 300});
		}
		infoWin.name = me.marker.name;
		CCCCurrentFocusRoute = this;
		this.focussed = true;
		return true;
	}
	
	this.defocus = function () { 
		if(!this.menuItem) 
			this.menuItem = document.getElementById(this.routeDbRec.text_id);
		var infoWin = gmap.getInfoWindow();
		if(this.menuItem) this.menuItem.style.color = this.colour.code;
		this.changeAppearance(this.colour);
		if(CCCCurrentMapType == OSM_CYCLE_MAP && this.OsmMode) this.hidePolys();
		else this.redrawPolys();
		this.hideRouteMarkers();
		if(infoWin) if(!infoWin.isHidden())
				if(infoWin.name == this.marker.name) infoWin.hide();
		if(this.hilighted) this.hilight();
		CCCCurrentFocusRoute = null;
		this.focussed = false;
	}
	
	this.zoomAndCenter = function() {
		var bounds = this.getBounds();
		gmap.setCenter(bounds.getCenter(), gmap.getBoundsZoomLevel(bounds));
		// redisplay the infowindow to ensure it is in-screen:
		this.marker.GMarker.openInfoWindowTabsHtml(this.marker.getInfoTabsHTML(), {maxWidth: 300});
}
	
	this.changeAppearance = function(theColour) {
		for (j = 0; j < this.polylines.length; j++) {
			if(this.polylines[j].color != theColour.code || this.polylines[j].opacity != theColour.opacity) {
				this.polylines[j].color = theColour.code;
				this.polylines[j].opacity = theColour.opacity;
			}
		}
		this.marker.GMarker.setImage(CCCIconSets.routeIcons[theColour.name].image);	
	}

	this.hilight = function (aColour) {
		var theColour = aColour ? aColour : CCCHilightColour;
		if(CCCCurrentHiRoute) CCCCurrentHiRoute.lowlight(); // only one route can be hilighted
		this.changeAppearance(theColour);
		this.redrawPolys();
		this.marker.showLabel();
		CCCCurrentHiRoute = this;
		this.hilighted = true;
	}
	
	this.lowlight = function () {
		if (this.focussed) {
			this.changeAppearance(CCCFocusColour);
			this.redrawPolys();
		} else {
			this.changeAppearance(this.colour);
			if(CCCCurrentMapType == OSM_CYCLE_MAP && this.OsmMode) this.hidePolys();
			else this.redrawPolys();
			if(this.marker.iconImage) this.marker.GMarker.setImage(this.marker.iconImage);
		}
		this.marker.hideLabel();
		CCCCurrentHiRoute = null;
		this.hilighted = false;
	}
	
	this.showRouteMarkers = function() {
		if(CCCInfoMarkerSets[this.routeDbRec.text_id]) CCCInfoMarkerSets[this.routeDbRec.text_id].show();
	}
		
	this.hideRouteMarkers = function() {
		if(CCCInfoMarkerSets[this.routeDbRec.text_id]) CCCInfoMarkerSets[this.routeDbRec.text_id].hideAll();
	}
		
	this.updateMenu = function () {
			var menuItemDiv = document.getElementById(this.routeDbRec.text_id);
			if(menuItemDiv) {
				if(this.hidden) {
					menuItemDiv.style.color = CCCDimmedColour;
				}
				else menuItemDiv.style.color = this.colour.code;
			}
	}
	
	this.activate = function () {
		for (j = 0; j < this.polylines.length; j++) {
			GEvent.addListener(this.polylines[j], 'click', function(point) {
					if(me == CCCCurrentFocusRoute) {
						me.defocus();
					} else {
						me.focus(point);
					}
				});
		}
	}
			
	this.deactivate = function () {
		for (j = 0; j < this.polylines.length; j++) {
			GEvent.clearInstanceListeners(this.polylines[j]);
		}
	}

	this.show = function () {
		if(!(CCCCurrentMapType == OSM_CYCLE_MAP && this.OsmMode)) {
			this.showPolys();
			for (i = 0; i < this.arrows.length; i++) {
				if(!this.arrows[i].overlay) {
					this.arrows[i].overlay = 
					// Uses Bill Chadwick's DivOverlays code (see libraries.js)
						new ArrowMarker(this.arrows[i].position, this.arrows[i].bearing,
							colourTable['black'].code, CCCArrowWidth, colourTable['black'].opacity, 
							this.arrows[i].tooltip);
				}
				gmap.addOverlay(this.arrows[i].overlay);
			}
		}
		this.hidden = false;
		this.marker.show();
		this.activate();
		this.updateMenu();
	}

	this.hide = function () {
		if(!(CCCCurrentMapType == OSM_CYCLE_MAP && this.OsmMode)) {
			this.hidePolys();
		}
		for (i = 0; i < this.arrows.length; i++) {
			if(this.arrows[i].overlay) 
			gmap.removeOverlay(this.arrows[i].overlay);
		}
		if (this.hilighted) {
			this.lowlight();
			this.hideRouteMarkers();
		}
		if(this.focussed) {
			this.hideRouteMarkers();
			this.focussed = false;
			CCCCurrentFocusRoute = null;
		}
		this.marker.remove();
		this.deactivate();
		this.hidden = true;
		this.updateMenu();
	}
	
	this.loadTrack = function () {
	// if the format is GPX or KML, then call loadXMLTrack()
	// if JSON then just transfer the point vectors from the JSON
		var theFormat = this.routeDbRec.format.toUpperCase();
		
		if (theFormat == 'JSON') {
		 	// the text_id refers to a pre-loaded array of GLatLng() elements
			var trackSegs = eval(this.routeDbRec.text_id);
			if (trackSegs == null) {
				alert(this.routeDbRec.text_id + ':no track data');
				return false;
			} else {
				this.addSegments(trackSegs);
				this.trackLoaded();
				return true;
			}
		} else if (theFormat == 'GPX' || theFormat == 'KML') {
			this.loadXMLTrack(theFormat); // NB call to an asynchronous procedure!
		} else {
			alert(this.routeDbRec.fileURL + ': format = ' + this.routeDbRec.format + ' not correctly specified');
			return false;
		}
	}

	this.trackLoaded = function() {
		this.loaded = true;
	  	if(this.routeDbRec.onOSM == 1) this.OsmMode = true;
		this.marker.instantiateMarker();
		this.show();
		this.updateMenu();
		if (this.routeDbRec.text_id == CCCInitialFocusRoute) {
			this.focus(); 
			CCCInitialFocusRoute = null; // We only do it once.
		} 
	}
	
	this.getBounds = function () {
	// returns a rectangle that is the least bound of the polylines in a track
		if(!this.bounds) {
			var startLatLng = this.polylines[0].getVertex(0);
			
			this.bounds = new GLatLngBounds(startLatLng, startLatLng);
			for (j=0; j < this.polylines.length; j++) {
			// could do the following with a single call to polyline.getBounds() - since GMaps v2.85
				var nPoints = this.polylines[j].getVertexCount();
				for (i=0; i < nPoints; i++) {
					this.bounds.extend(this.polylines[j].getVertex(i));
				}
			}
		}
		return this.bounds;
	}
	
	this.loadXMLTrack = function (expectedFormat) {
		var fileURL = ((this.routeDbRec.format == 'GPX') ? GPXroutes_path :
				KMLroutes_path) + this.routeDbRec.fileURL;	

		GDownloadUrl(fileURL, function(fileText, status) {	//callback function
			if(!fileText) {
				alert(me.routeDbRec.text_id + ', ' + me.routeDbRec.fileURL + " couldn't read file.");
				return;
			}
			
			var xmlDoc = GXml.parse(fileText);
			
			var xmlDocFormat = xmlDoc.childNodes[0].nodeName.toUpperCase(); // FF returns 'GPX' but IE returns 'XML'!
	
			if(xmlDocFormat == 'XML' || xmlDocFormat == '#TEXT') // we must be in IE or Opera,
																 // so get the node type from the xmldoc
				if(xmlDoc.childNodes[1])
					xmlDocFormat = xmlDoc.childNodes[1].nodeName.toUpperCase();
			
			if(xmlDocFormat != expectedFormat) {
				alert(me.routeDbRec.fileURL +': wrong format. Expected ' + me.routeDbRec.format +
					', not ' + xmlDocFormat);
				return;
			}
			var theTrackElements;
			switch (xmlDocFormat) { 
			case 'GPX':
				theTrackElements = GetGPXTrackElements(xmlDoc); 
				break;
			case 'KML':
				theTrackElements = GetKMLTrackElements(xmlDoc);
				break;
			}
			
			if(theTrackElements) {
				me.addTrackElements(theTrackElements);
				me.loaded = true;
				me.trackLoaded();
			} else {
				alert(me.routeDbRec.fileURL +': no track segments found in file');
			}
		} ); // end of callback function
		
		function GetKMLTrackElements(KMLDoc) { // returns the parsed track as an array of of TrackElement objects
		// originally based on MWilliams EGeoXML code:
			var trackElements = [];
			var placemarks = KMLDoc.documentElement.getElementsByTagName("Placemark");
		
			// check each LineString to see whether it is a polyline
			for (var j = 0; j < placemarks.length; j++) {
				var LineStrings = placemarks[j].getElementsByTagName("LineString");
				var name = GXml.value(placemarks[j].getElementsByTagName("name")[0]);
		
				for (var i = 0; i < LineStrings.length; i++) {
					var coords = GXml.value(LineStrings[i].getElementsByTagName("coordinates")[0]);
					var path = coords.split(/\s+/);
			
					if (path.length > 1) { 	  	// Is this a polyline?
						var points = []; 
						for (var p=0; p<path.length; p++) {
							if(path[p].length > 1) { // is this point non-blank?
								var bits = path[p].split(",");
								var point = new GLatLng(parseFloat(bits[1]),parseFloat(bits[0]));
								points.push(point);
							}
						} // for p
						trackElements.push({'name': name, 'points': points});
					} // if path.length
				} // for i 
			} // for j
			return trackElements;
		}
		
		function GetGPXTrackElements(GPXTrack) { // returns the parsed track as an array of of TrackElement objects
			var routeData = GPXTrack.getElementsByTagName("rte");
			var trackData = GPXTrack.getElementsByTagName("trkseg");
			var trackElements = new Array(); 
		
			if (routeData.length > 0) {
				GPXtype = ROUTE;
				segments = routeData;
			} else {
				GPXtype = TRACK;
				segments = trackData;
			}
			if (segments.length > 0) {
				for (var i=0; i < segments.length; i++) {
					var name = GXml.value(segments[i].getElementsByTagName('name')[0]);
					var aSegment = GetSegmentPoints(segments[i], GPXtype);
					if (aSegment) {
						trackElements.push({'name': name, 'points': aSegment});
					}
				}
			} else alert ('No valid route data found');
			return trackElements;
		}
		
		function GetSegmentPoints (trackSegment, GPXtype) {
		
			var trackpoints = (GPXtype == ROUTE)?trackSegment.getElementsByTagName("rtept"): 
												trackSegment.getElementsByTagName("trkpt");
			if (trackpoints.length == 0) {
				alert ("No points found in file:" + theTrack.label);
				return null;
			} else {
		
				var pointarray = new Array();
			
				for (var i=0; i < trackpoints.length; i++) {
					var lon = parseFloat(trackpoints[i].getAttribute("lon"));
					var lat = parseFloat(trackpoints[i].getAttribute("lat"));
			
					latlng = new GLatLng(lat,lon);
					pointarray.push(latlng);
				}	
			}
			return pointarray;
		}
	}
	// load the track  if it is supposed to be displayed initially
	if(this.initialState == INITIALLYON && !arrayContains(CCCInitiallyOffRoutes, this.routeDbRec.text_id))
			this.loadTrack();
}

function arrayContains(anArray, anElement) {
	if(!anArray) return false;
	for( var i = 0; i < anArray.length; i++) {
		if(anArray[i] == anElement) return true;
	}
	return false;
}

function CCCMarker(aTrack) {
	this.markerTitle = aTrack.routeDbRec.title;
	this.description = aTrack.routeDbRec.description;
	this.contributor = aTrack.routeDbRec.contributed_by;
	this.links = aTrack.routeDbRec.links;
	this.colourName = aTrack.colourName;
	this.position = null; // by default it goes at the mid point in the track;
	this.GMarker = null; // it will be instantiated when the associated GPolyline is created
	this.track = aTrack; // the track to which this marker is attacted.
	this.name = 'Route #' + this.track.routeDbRec.id; // needed in order to reference the infoWindow
	this.hilighted = false;
	this.iconImage = null;	// for use only in special cases where routes have distinct icons ('_iconImage' in the Routes DB)
	
	this.markerLabel = null; // will be instantiated as an ELabel when first used.
	this.infoTabsHTML = null;	// will be constructed when infowindow is first used.
	var me = this;	// so that 'me' will be bound to the closure of the listener function
	
	this.instantiateMarker = function () { 
	// called from CCCTrack.trackLoaded after the associated track points have been loaded
	
		if(this.track.routeDbRec._iconImage) 
			this.iconImage = MarkerIcons_path + this.track.routeDbRec._iconImage;	// for the special case where 
																					// we assign a position in the DB
		if(this.track.routeDbRec._LatLng) { // special case where we assign a position in the DB
			var latLngStrings = this.track.routeDbRec._LatLng.split(',');
			this.position = new GLatLng(latLngStrings[0], latLngStrings[1]);
		} else this.position = this.track.midPoint;
		
		this.GMarker = new GMarker(this.position, 
			{icon: CCCIconSets.routeIcons[this.colourName], title: this.markerTitle}); 
		GEvent.addListener(this.GMarker, "click", function() {
				if(me.track.focussed) {
					me.track.defocus();
				} else {
					me.track.focus(null);
				}
		});
/* 		GEvent.addListener(this.GMarker, "dblclick", function() { */
/* 			var bounds = me.track.getBounds(); */
/* 			gmap.setCenter(me.position, gmap.getBoundsZoomLevel(bounds)); */
/* 		}); */
	}
	
	this.getInfoTabsHTML = function () {
		if(!this.infoTabsHTML) {
			var distance = (this.track.distance*0.00062137).toFixed(1); // convert metres to miles
			var description = '<div id=\"info_window_div\"><b>' + this.markerTitle + 
				'</b>' + '<br><div class = \"routeDescription\">' + this.description + 
				'</div><div align=center> <a href=\"javascript: CCCTrackList[\'' + 
				this.track.routeDbRec.text_id + '\'].zoomAndCenter()\">'  + 
				'Zoom and centre on this route</a><br/>' + 
				'NB: cycle stands near your destination are shown on the map.<br/>';
			var camRouteLink, reviewRequest;
			
			var URL = window.location.href;
			var n = URL.indexOf('?');
			var nameList = '', segmentNames = this.track.segmentNames, segmentLengths = this.track.polyLengths;

			if(me.track.routeDbRec.camRouteNumber) {
				camRouteLink = "http://camden.cyclestreets.net/journey/" + this.track.routeDbRec.camRouteNumber + "/";
				description += "View this route with printable details and give feedback<br/><a href=\"" + camRouteLink + 
				'\" target=\"_blank">on the CycleStreets web site</a>';
			}
			description += '</div></div>';
			
			for (var i = 0; i < segmentNames.length; i++)
				if(segmentNames[i]) nameList += segmentNames[i] +' ' + 
					(segmentLengths[i]*0.000621371192).toFixed(1) + 'mi, ';
			
			URL = (n > 0) ? URL.slice(0,n) : URL;
						
			var details = '<div id=\"info_window_div\"><b>' + this.markerTitle + '</b>' + 
				'<br>Distance = ' + distance + ' miles <br>';
			if(camRouteLink) {
				details += '<a href=\"mailto:?subject=' + encodeURIComponent('CycleStreets Route ' + 
					this.markerTitle) + '&body=' + camRouteLink + '\">Email a link to this route</a>' +
				 	'<br>routeid = ' + this.track.routeDbRec.text_id; 
			} else {
				details += 
				'<a href=\"mailto:?subject=' + encodeURIComponent('CCC Route: ' + this.markerTitle) + '&body=' +
				URL + encodeURIComponent('?focus=' + this.track.routeDbRec.text_id) + 
				'\">Email a link to this route</a>' +
				((this.links == '') ? '' : '<br><a href=\"' + this.links + 
				'\" target=\"_blank">Web page with further details</a>') +
				((this.contributor == '') ? '' : '<br><i> Contributed by ' + this.contributor + '</i>') + 
				((editing_allowed)? '<br>Marker position: ' + this.position.lat() + ',' + this.position.lng() :'') +
				 '<br>category:subcategory = ' + this.track.routeDbRec.category + ':' + this.track.routeDbRec.subcat +
				 '<br>routeid = ' + this.track.routeDbRec.text_id + 
				 ((editing_allowed)? '<br>Links: ' + nameList : '');
			}
			details += '</div>';
			this.infoTabsHTML = [new GInfoWindowTab('Route', description), new GInfoWindowTab('Details', details)];
		}
		return this.infoTabsHTML;
	}

	this.showLabel = function () {
		if(!this.markerLabel) {
			this.markerLabel = 
				new ELabel(this.position, this.markerTitle, "marker_label_div", null /* offset */, 80 /* opacity */);
			gmap.addOverlay(this.markerLabel);	
		}
		else this.markerLabel.show();
	}
	
	this.hideLabel = function () {
		if (this.markerLabel) this.markerLabel.hide();
	}
	
/* 	this.openInfoWin = function() { */
/* 		this.GMarker.openInfoWindowTabsHtml(this.getInfoTabsHTML(), {maxWidth: 300});  */
/* 		gmap.getInfoWindow().name = this.name; */
/* 	} */
/* 	 */
	this.show = function() {
		gmap.addOverlay(this.GMarker);
		if(this.iconImage) {	// special case
			this.GMarker.setImage(this.iconImage);
		}
		if(CCCMarkerEditing && CCCMarkerEditing.table_being_edited() == 'Routes' && CCCInfoMarkerSets['Routes']) {
			CCCInfoMarkerSets['Routes'].members[this.track.routeDbRec.id].show();
		}
	}
	
	this.remove = function() {
		var infoWin = gmap.getInfoWindow();
		if(infoWin) if(!infoWin.isHidden()) {
			if(infoWin.name == this.name) infoWin.hide();
		}
		if(CCCMarkerEditing && CCCMarkerEditing.table_being_edited() == 'Routes' && CCCInfoMarkerSets['Routes']) {
			CCCInfoMarkerSets['Routes'].members[this.track.routeDbRec.id].hide();
		}
		gmap.removeOverlay(this.GMarker);
	}
}

/*
 * borrowed from Flanagan, 'Javascript' Edn 5
 	http://www.oreilly.com/catalog/jscript5/
 * this is Example 14.1
 * This function parses ampersand-separated name=value argument pairs from
 * the query string of the URL. It stores the name=value pairs in 
 * properties of an object and returns that object. Use it like this:
 * 
 * var args = getArgs();  // Parse args from URL
 * var q = args.q || "";  // Use argument, if defined, or a default value
 * var n = args.n ? parseInt(args.n) : 10; 
 */
function getArgs() {
    var args = new Object();
    var query = location.search.substring(1);     // Get query string
    var pairs = query.split("&");                 // Break at ampersand
    for(var i = 0; i < pairs.length; i++) {
        var pos = pairs[i].indexOf('=');          // Look for "name=value"
        if (pos == -1) continue;                  // If not found, skip
        var argname = pairs[i].substring(0,pos);  // Extract the name
        var value = pairs[i].substring(pos+1);    // Extract the value
        value = decodeURIComponent(value);        // Decode it, if needed
        args[argname] = value;                    // Store as a property
    }
    return args;                                  // Return the object
}

/* textual zoom control */
function CCCZoomOutControl() {
	this.container;
	this.zoomListener;
	this.dragListener;
	this.hasChanged;
	var me = this;

	this.initialize = function(map) {
		this.container = document.createElement("zoomControl");

		this.container.innerHTML = "<b>Zoom Back</b>";
		this.container.style.color = '#808080'; // 'grey' doesn't seem to work in IE
		this.hasChanged = false;
		this.zoomListener = GEvent.addListener(map, 'zoomend', me.mapChanged);
		this.dragListener = GEvent.addListener(map, 'move', me.mapChanged);
		
		GEvent.addDomListener(this.container, "click", function() { 
			if(!me.hasChanged) return;
			gmap.setCenter(initial_center, initial_zoom_level);
			CCCCheckMaxZoom(); // to re-enable the Zoom In control if necessary
//			me.container.style.fontWeight = 'normal';
			me.container.style.color = '#808080'; // 'grey' doesn't seem to work in IE
			me.hasChanged = false;
			me.container.title = '';
			me.zoomListener = GEvent.addListener(map, 'zoomend', me.mapChanged);
			me.dragListener = GEvent.addListener(map, 'dragend', me.mapChanged);
		});
	
		map.getContainer().appendChild(this.container);
		return this.container;
	}
	
	this.mapChanged = function() {
		me.hasChanged = true;
//		me.container.style.fontWeight ='bold';
		me.container.style.color = '#000000'; // Black
		me.container.title = 'Return map to original size and position';
		GEvent.removeListener(me.zoomListener);
		GEvent.removeListener(me.dragListener);
	}
	
	this.getDefaultPosition = function() {
		return new GControlPosition(G_ANCHOR_TOP_RIGHT, CCCZoomOutPos);
	}
}
CCCZoomOutControl.prototype = new GControl();

function CCCRoutePlannerControl() {
	this.container = document.createElement("plannerControl");
	this.active = false;	// true when we are in the process of planning a route
	this.startMarker;
	this.endMarker;
	this.elabel;
	this.mapListener;
	this.boundaryListener;
	this.thePoints;
	this.nPoints;
	this.browserFirefox2 = 
		(navigator.userAgent.indexOf('Firefox/2') > 0) || (navigator.userAgent.indexOf('Firefox/1') > 0);
	this.boundsAdded = false;

	var me = this;
	var label = 	"<span onclick = CCCRoutePlanner.dialog()><b>&nbsp;Plan a Route...&nbsp;</b>"; // starts the dialog	
	var title =		"Beta feature to perform automatic route planning.";
	var header = 	"<b>Planning a Route:</b> ";
	var preamble = 	'<i>Courtesy of <a href=\"http://www.cyclestreets.net/\" id=\"Cyclestreets.net\" title="Cyclestreets.net" target="_blank">Cyclestreets.net</a></i><br> ';
	var firefoxCaveat = 
		"<i><b>Firefox users</b>: LCN Routes, etc. temporarily hidden during route planning to avoid a 'missed clicks' issue</i>. <br>";
	var prompt1 = 	"<br><b>Click at journey start point...</b>";
	var prompt1a = '<br>Repeat last route in  reverse or with different options:' + 
					'&nbsp;&nbsp;<input type="button" name="Repeat" value="Repeat Last"' +
					'onclick = CCCRoutePlanner.repeatLast()><br>';
	var prompt1done = "<br>Start point &#8730;";
	var prompt2 = 	"<br><b>Click at journey finish point...</b>";
	var prompt2done = "<br>Finish point &#8730;";
	var action = 	'&nbsp;&nbsp;<input type="button" name="PlanRoute" value="Plan Route"' +  
					'onclick = CCCRoutePlanner.planRoute()>';
	var cancelButton =	/* '<br>&nbsp;&nbsp;<b>or</b>&nbsp;&nbsp;' + */
						'<input type="button" name="Cancel" value="Cancel" onclick = CCCRoutePlanner.cancel()>';
	var reverseButton =	'<br>Return route? <input type="checkbox" id="reverseButton"' + 
						'onclick = CCCRoutePlanner.reversePlan()>';
	var options = 	'<br><input type="radio" name="routeTypeButton" value="balanced" ' + 
						'checked="true">Balanced&nbsp;&nbsp;' + 
				'<input type="radio" name="routeTypeButton" value="fastest" >Fastest&nbsp;&nbsp;' + 
				'<input type="radio" name="routeTypeButton" value="quietest" >Quietest&nbsp;&nbsp;';
	var prompt3 = 	"<br><b>The CycleStreets Journey Planner is preparing a route ...</b>" + 
					"&nbsp;&nbsp&nbsp;&nbsp" + CCCProgressImage ;
	
	this.initialize = function(map) {
		this.container.title = title;
		this.container.innerHTML = label;
		this.container.style.width = "auto";
		map.getContainer().appendChild(this.container);
		return this.container;
	}
	
	this.getDefaultPosition = function() {
		return new GControlPosition(G_ANCHOR_TOP_RIGHT, CCCRoutePlannerPos);
	}
		
	this.dialog = function () {
		me.nPoints = 0;
		me.active = true;
		me.container.title = '';
		me.container.style.width = CCCRoutePlannerMaxWidth + "px";
		me.container.innerHTML = header + preamble + (me.browserFirefox2?firefoxCaveat:'') + 
					  cancelButton + (me.thePoints ? prompt1a + 'Or plan a new route:' : '') + prompt1;
		CCCTrack.deactivateAll();
		if(me.browserFirefox2) CCCTrack.tempHideAll();

		me.mapListener = GEvent.addListener(gmap, "click", me.clickListener);
	}
	
	this.clickListener = function (overlay, point, overlayPoint) { 
		if(me.eLabel) {
			gmap.removeOverlay(me.eLabel);
			me.eLabel = null;
		}
		if(overlay) {
			GEvent.trigger(gmap, 'click', null, overlayPoint);
				return;
		}
		me.nPoints += 1;
		switch(me.nPoints) {
		case 1:
			me.setMarker('start', point);
			me.container.innerHTML = header + preamble + (me.browserFirefox2?firefoxCaveat:'') +
								cancelButton + prompt1done + prompt2;
			break;
		case 2:
			var distance = point.distanceFrom(me.startMarker.getPoint());
			if(distance < CCCShortestRoutePlan ) {
				// show diagnostic if less than CCCShortestRoutePlan metres apart
				//       function ELabel(point, html, classname, pixelOffset, percentOpacity, overlap) 
				me.eLabel = new ELabel(point, "Points must be at least " + CCCShortestRoutePlan + " metres apart", 
												"marker_label_div", new GSize(10, -10), 80);
				gmap.addOverlay(me.eLabel);	

				me.nPoints -= 1;
			} else {
				me.setMarker('finish', point);
				me.container.innerHTML = header + preamble + (me.browserFirefox2?firefoxCaveat:'') + 
									cancelButton + prompt1done + prompt2done + options + action;
			}
			break;
		default:
		}
	}
	
	this.setMarker = function(label, point) {
		switch(label) {
		case 'start':
			this.startMarker = new GMarker(point, {icon: CCCIconSets.plannerIcons['start'], 
									title: "Route planning: proposed start point"});
			gmap.addOverlay(this.startMarker);
			break;
		case 'finish':
			this.endMarker = new GMarker(point, {icon: CCCIconSets.plannerIcons['finish'], 
									title: "Route planning: proposed finish point"});
			gmap.addOverlay(this.endMarker);
			break;
		}
	}
	
	this.cancel = function() {
		me.tidyup();
		if(me.browserFirefox2) CCCTrack.unTempHideAll();
	}
	
	this.repeatLast = function() {
		me.setMarker('start', me.thePoints[0]);
		me.setMarker('finish',  me.thePoints[1]);
		me.container.innerHTML = header + preamble  + cancelButton + '<br>' +
					"<br>Repeat last route with different options:" + reverseButton + options + action;
//		document.getElementById('reverseButton').checked = false;	//for some reason we have to force this 
	}
	
	this.reversePlan = function() {
		var startPoint = me.startMarker.getPoint(), endPoint = me.endMarker.getPoint();
		me.startMarker.setPoint(endPoint);
		me.endMarker.setPoint(startPoint);
	}
	
	this.planRoute = function () {
		var routeTypeButtons = document.getElementsByName('routeTypeButton');
		var routeType = null;
		
		me.thePoints = [me.startMarker.getPoint(), me.endMarker.getPoint()];
		for(i=0; i<routeTypeButtons.length; i++) 
			if(routeTypeButtons[i].checked) routeType = routeTypeButtons[i].value;		
		me.fetchRoute(me.thePoints, routeType); 
		me.container.innerHTML = 
				header + preamble + cancelButton + prompt1done + prompt2done + prompt3;
		if(me.browserFirefox2) CCCTrack.unTempHideAll();
	}
	
	this.fetchRoute = function(thePoints, type) {
		var params, result;
		var xmlRouteText, xmlRouteDoc;
		params = "&start_longitude=" + thePoints[0].lng() + 
				"&start_latitude="  + thePoints[0].lat() +
				"&finish_longitude=" + thePoints[1].lng() +
				"&finish_latitude="  + thePoints[1].lat() +
				"&layer=18" + "&plan=" + type/* + "&useDom=1"*/; // NB &useDom=1 fetches a GML markup document
// GLog.write(params);

		GDownloadUrl(proxyURL + "?url=" + cycleStreetsPlannerURL + encodeURIComponent(params), 
			function(xmlRouteText, responseCode) {
				if(responseCode == 200) {
					xmlRouteDoc = GXml.parse(xmlRouteText);
// GLog.write(xmlRouteText);
					if(!(result = CCCTrack.synthesiseTrack(xmlRouteDoc))) {
						GLog.write(params + ': ' + xmlRouteText + '<br/>Error reported: <br/>' + xmlRouteText );
					}
				} else switch (responseCode) {
					case 500:
						alert("Sorry, the server has insufficient resources to compute that route. You could try splitting it.");
						break;
					default:
						alert(" Error code " + responseCode + ", " + xmlRouteText);
				}
				me.tidyup();
			});

	}
		
	this.tidyup = function() {
		if(me.eLabel) {
			gmap.removeOverlay(me.eLabel);
			me.eLabel = null;
		}
		if(this.startMarker) gmap.removeOverlay(this.startMarker);
		if(this.endMarker) gmap.removeOverlay(this.endMarker);
		if(this.mapListener) GEvent.removeListener(this.mapListener);
		CCCTrack.activateAll(); 
		this.active = false;
		this.container.title = title;
		this.container.innerHTML = label;
		this.container.style.width = "auto";
	}
}
CCCRoutePlannerControl.prototype = new GControl();

// var _mSvgForced = true;	// maybe forces GMaps API to use SVG graphics in FF/Mac and Safari

// This is the 'main program' called from the HTML 'onload' action.
//
function CCC_Map(theMenuName) {
// parameter is optional. If present, it is the name of a menu in the 'MenuDefs' database table
		
	// display a splash screen if the relevant html file exists
	
	var fireFox3Mac = (navigator.userAgent.indexOf('Firefox/3')>0) && (navigator.userAgent.indexOf('Macintosh')>0);
	var splash = fireFox3Mac ? syncGetFile("http://maps.camdencyclists.org.uk/documentation/splashFF3Mac.html") :
									syncGetFile("http://maps.camdencyclists.org.uk/documentation/splash.html");
	
	if(splash) {
		var display = new CCCDisplayWindow ();	// map not yet defined, so no param
		
		display.openOnMap(CCCSplashWinPosition, CCCSplashWinWidth, splash);
 		window.setTimeout(function () {display.hide()},20000); 
	}

/*	
	CCCCookies = new Cookies();
	if(CCCCookies) {
	// this browser allows cookies
		if(CCCCookies.currentCookies && CCCCookies.currentCookies.startState) {	
												// startState is a JSON string containing
												// a loc, a zoom, a maptype and a menuSpec name
		} else { // no startState cookie
		
		}
	}
*/
		
	CCC_Map.clickLocListener = null;
	CCC_Map.ToggleClickLoc = function(URLMode) {
		var ClickLocToggleElement = document.getElementById('ClickLocToggle');
		// set a mode in which a single mouseclick returns the location and current map status for use in URLs
		if(CCC_Map.clickLocListener) {
			GEvent.removeListener(CCC_Map.clickLocListener);
			CCC_Map.clickLocListener = null;
			ClickLocToggleElement.style.background = 'white';
		} else {
			CCC_Map.clickLocListener = GEvent.addListener(gmap, "click", function (overlay, point) {
				var shortMapName = gmap.getCurrentMapType().getName(true); 
				var mapType = CCCMapTypes[shortMapName];
				var lat = point.lat().toPrecision(6), lng = point.lng().toPrecision(6);
				if(URLMode) {
					var url = String(location);
					url += (url.indexOf('?') < 0) ? '?' : '&' ;
					GLog.write(" " + url + "center=" + lat + "," + lng +
							"&zoom=" + gmap.getZoom() + "&maptype=" + mapType);
				} else 
					GLog.write(" {center:[" + lat + ',' + lng +
						"], zoom:" + gmap.getZoom() + ", maptype:" + mapType + '}');
				});
			ClickLocToggleElement.style.background = 'blue';
		}
	}

	function AddLayer (layerName) {
		// From http://www.zimmerheimer.de/mapnik/almien.html via Simon Nuttall 
	  // http://www.zimmerheimer.de/mapnik/mapnik_almien.js
		MapnikGetTileUrl = function (a,b,c) {
			var url;
			switch (layerName) {
			case "OSM": 
			case "OpenStreetMap": 
			case "OSM Mapnik": 
			case "OSM 1": 
//				url = "http://a.tile.openstreetmap.org/";	// Mapnik rendered tiles
				url = "http://tile.cloudmade.com/8bafab36916b5ce6b4395ede3cb9ddea/3/256/"; // 'Noname' rendered tiles
				break;
// this case doesn't occur because it's not in the menu				
			case "OSM Osmarend": 
			case "OSM 2": 
				 url = "http://tah.openstreetmap.org/Tiles/tile/";	// Osmarendered tiles
				break;
			case "OpenCycleMap": 
//				url = "http://www.thunderflames.org/tiles/cycle/";
// changed 25/11/09 to use same tile server as Cyclestreets. See also http://www.thbimage.com/OpenStreetMap.html
				url = "http://andy.sandbox.cloudmade.com/tiles/cycle/";
				break;
			default:
				return null;
			}
			url += b + "/" + a.x + "/" + a.y + ".png";
			return url;
		}
		
		var copycol = new GCopyrightCollection("");
		var copy = new GCopyright(1, new GLatLngBounds(new GLatLng(-90,-180),new GLatLng(90,180)), 0, 
			"<a href=\"http://www.openstreetmap.org\">OpenStreetMap:</a> licensed " +  
			"<a href=\"http://creativecommons.org/licenses/by-sa/2.0/\">CC-BY-SA</a>, API code:");
		copycol.addCopyright(copy);
		var tileMapnik = new GTileLayer(copycol,1,17);
		tileMapnik.getTileUrl = MapnikGetTileUrl;
		tileMapnik.isPng = function () { return true;}
		tileMapnik.getOpacity = function() {return 1.0;}

		var layer0 = [tileMapnik];
		// last param shd be a custom options object with maxResolution: 16 and alt: "Show OpenStreetMap-based cycle map" instead of G_SATELLITE_MAP
		var mapnikMap = new GMapType(layer0, G_SATELLITE_MAP.getProjection(), layerName, 
			{maxResolution: 17, alt: "Show OpenStreetMap " + layerName });
		return mapnikMap;
	}// end of addLayer
	
	if (!GBrowserIsCompatible()) { 
		document.getElementById('gmap_div').innerHTML = 
			'Sorry, your browser is not compatible with Google Maps.';
	} else {

		var args = getArgs();
												
		// this code lets the user set initial options by arguments in the URL string
		// Camden default is: 'centre=51.531355,-0.138915'
		// Centre: [lat, lon]

		if(args.menu) { // over-rides whatever was menu name appears in the HTML file
			if(args.menu.charAt(0) == '{')
				CCCMenuDef = eval('(' + args.menu + ')'); 
			else theMenuName = args.menu;
		} else if(args.menuName) 
			theMenuName = args.menuName;

		if(theMenuName) { 
			var menuDetails = CCCLoadMenuDef(theMenuName);
			if(menuDetails['menuDef']) CCCMenuDef = menuDetails['menuDef'];
			if(menuDetails['initialMapState'] && menuDetails['initialMapState']['center']) {
				var center = menuDetails['initialMapState']['center'];
				initial_center = new GLatLng(center[0],center[1])
			}
		}

		if(args.center || args.centre) {
			var latLngStrg =  args.center?args.center:args.centre;
			var centre = GLatLng.fromUrlValue(latLngStrg);
			initial_center = centre? centre: initial_center; // in case of a url syntax error
		}
		// these are legacy arguments:
		if (args.lat) initial_center = new GLatLng(args.lat, initial_center.lng());
		if (args.lng) initial_center = new GLatLng(initial_center.lat(), args.lng);
		if (args.lon) initial_center = new GLatLng(initial_center.lat(), args.lon);

		if(args.focus) 
			CCCInitialFocusRoute = args.focus; 
		// these are legacy arguments:
		if(args.hilight) CCCInitialFocusRoute = args.hilight;
		if(args.hilightroute) CCCInitialFocusRoute = args.hilightroute;
		
		if(args.stands)  // these two legacy features now handled by the general case below
			args.showmarkers = "Stands";
		if(args.consultation) 
			args.showmarker = "Consultations." + args.consultation;
		if(args.showmarkers) {
			if(!args.onMarkers) args.onMarkers = args.showmarkers;
			else args.onMarkers += args.showmarkers;
		}
		if(args.showmarker) { // displays a set of markers and (optionally) opens the infowin for one of them
			var parts = args.showmarker.split(".");
			var markerSet = parts[0], markerId = parts[1] ? parts[1] : null;		
			if(CCCInfoMarkerDefs[markerSet]) { 	// check that the markerSet is a valid marker type
												// (Shops, Hazards, Stands, Consultations)
				CCCPendingMarkerShow = {'setName': markerSet, 'id': markerId};
			}
		}
		
		if(args.onRoutes) 
			CCCInitiallyOnRoutes = args.onRoutes.split(",");
		if(args.offRoutes) 
			CCCInitiallyOffRoutes = args.offRoutes.split(",");
		if(args.onMarkers) 
			CCCInitiallyOnMarkers = args.onMarkers.split(",");
		if(args.offMarkers) 
			CCCInitiallyOffMarkers = args.offMarkers.split(",");
		if(args.streetView) 
			CCCStreetViewState = args.streetView;
		
		if((document.getElementById("edit_form_div") != null) && (CCCUserForumDetails != null)) { 
		// CCCUserForumDetails is: {id: $userid, name: $name, email: $email, privileges: { $privileges }}
			editing_allowed = true;
			CCCMarkerEditing = new CCCSetUpInfoMarkerEditing(CCCUserForumDetails);
		}
		
		OSM = OSM1 = OSM_1_MAP = OSM_MAP = CCC_OSM_MAP = AddLayer('OpenStreetMap'), 
		OSMCycle = OSM_CYCLE_MAP = CCC_OSM_CYCLE_MAP = AddLayer('OpenCycleMap');
//		G_DEFAULT_MAP_TYPES.push(G_PHYSICAL_MAP); 
		G_DEFAULT_MAP_TYPES.push(CCC_OSM_CYCLE_MAP);
		G_DEFAULT_MAP_TYPES.push(OSM_1_MAP);
		var mapOptions = 
			editing_allowed ? {draggableCursor:'crosshair'} :
						{draggableCursor:'pointer', 
						googleBarOptions: {
							onGenerateMarkerHtmlCallback: 
							function(theMarker, html) {
								if(CCCInfoMarkerSets["Stands"]) {
									CCCInfoMarkerSets["Stands"].showMarkersAround(theMarker.getLatLng());
									return html;
								}
							}
						}
					};

		if(menuDetails['initialMapState']) {
			if(menuDetails['initialMapState']['zoom']) 
				initial_zoom_level = menuDetails['initialMapState']['zoom'];
			if(menuDetails['initialMapState']['maptype']) {
				initial_map_type = menuDetails['initialMapState']['maptype'];
			// This is to cater for the legacy form in which maptypes are not read as strings:
				if(typeof initial_map_type == 'string') initial_map_type = eval(initial_map_type);
			}
		}
		
		initial_zoom_level = args.zoom?eval(args.zoom):initial_zoom_level;
		initial_map_type = args.maptype?eval(args.maptype):initial_map_type;

	 	gmap = new GMap2(document.getElementById("gmap_div"), mapOptions); // create map
		gmap.setCenter(initial_center, initial_zoom_level, initial_map_type);
//		gmap.enableRotation();	// this should show perspective in Satellite views
		CCCCurrentMapType = gmap.getCurrentMapType();
		CCCCurrentMapName = CCCCurrentMapType.getName();

		G_NORMAL_MAP.getName=function($short){ 
			return $short?'Map':'Google Map'; 
		}; 
		G_HYBRID_MAP.getName=function($short){ 
			return $short?'Hyb':'Satellite +'; 
		}; 
		G_PHYSICAL_MAP.getName=function($short){ 
			return $short?'Ter':'Google Terrain'; 
		}; 
		
		gmap.removeMapType(G_SATELLITE_MAP);
		gmap.removeMapType(G_HYBRID_MAP);	// This apparently illogical pair of lines 
		gmap.addMapType(G_HYBRID_MAP);		// moves the Satellite map to the end of the menu
	
		var tradZoomControl = new GLargeMapControl3D();
		gmap.addControl(tradZoomControl, new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(5,50)));
				
		var overviewMapControl = new GOverviewMapControl();
		gmap.addControl(overviewMapControl);
		
		var scaleControl = new GScaleControl(300); // the parameter sets a maximum length (in pixels for the scale
		gmap.addControl(scaleControl, new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(5,5)));
		
		var attributionBar = document.getElementById('attribution_div');
		if(attributionBar) {
			gmap.addControl(new CCCGenericControl(attributionBar, G_ANCHOR_BOTTOM_LEFT, CCCAttributionBarPos));
			attributionBar.style.display = "inline"; // it was "none" until now to prevent display in wrong place
		}

		gmap.enableGoogleBar();
		
		var CCCStreetView = new StreetViewHandler(gmap); // will right clicks to show streetview
		if(CCCStreetViewState) {	// we were called with a streetview requested
			CCCStreetView.setStateFromJSON(CCCStreetViewState);
		}
				
		GEvent.addListener(gmap, 'maptypechanged', function() { 
				CCCCurrentMapType = gmap.getCurrentMapType();
				CCCCurrentMapName = CCCCurrentMapType.getName();
				CCCTrack.redrawVisibleTracks();
			});

		 gmap.enableScrollWheelZoom(); // we detect when the mouse is over the menu now, 
										// but this is still problematic - too sensitive
											
		if(document.getElementById("login_form_div") == null) { //i.e. don't do this if we're waiting for a login
		
			var routeMenuOptions = {offset: CCCMenuPos, width: CCCMenuWidth, bottomMargin: CCCMenuBottomMargin, opacity: CCCMenuOpacity, stylesheet: 'floatBar', background: 'white', hideable: true};

			CCCMenuId = theMenuName + ' menu';
			gmap.routeMenuControl = new FloatBarControl(CCCMenuId, routeMenuOptions);
			gmap.addControl(gmap.routeMenuControl);
			gmap.routeMenuControl.load('<h4 style="color:blue">Loading <i>'  + theMenuName + '</i> menu... ' +
				CCCProgressImage + '</h4>');
					

			var zoomOutControl = new CCCZoomOutControl();
			gmap.addControl(zoomOutControl);
			
			var helpDisplay = new CCCHelpDisplay("http://maps.camdencyclists.org.uk/documentation/helpScreens/",
													G_ANCHOR_TOP_RIGHT, CCCHelpButtonPos);
			
			var mapTypeLabel = document.createElement("mapTypeLabel");
			mapTypeLabel.innerHTML = "Map Type:";
			mapTypeLabel.title = "Use the pulldown menu to change the map appearance";
			gmap.addControl(new CCCGenericControl(mapTypeLabel, G_ANCHOR_TOP_RIGHT, CCCMapTypeLabelPos));
			var mapTypeControl = new GMenuMapTypeControl();
			gmap.addControl(mapTypeControl, new GControlPosition(G_ANCHOR_TOP_RIGHT,CCCMapTypePos));

			// NB this is called after the menu has been built, to allow time for GMenuMapTypeControl code to load!
			CCC_Map.setupTypeControl = function() {
				var menumtctl = document.getElementById("menumtctl"),
					menumtctl_main = document.getElementById("menumtctl_main");
				if(menumtctl) {
					menumtctl.style.width = "8em"; 
					menumtctl_main.style.width = "8em"; 
				} else setTimeout("CCC_Map.setupTypeControl()",500);
			}
			
// Test for FF problem			
/* 	GEvent.addListener(gmap, 'click', function() { */
/*   		alert('Test for FF 2/Mac: you clicked on the map'); */
/*   	}); */
/*  */

			// defined in the gzoom.js file
			var gZoomControl = new GZoomControl(); // Defined in file gzoom.js (now included in libraries.js)
			gmap.addControl(gZoomControl, new GControlPosition(G_ANCHOR_TOP_RIGHT, CCCZoomInPos));
					
			var theMenu = new CCCMenu();	//  initialise a menu creator
			theMenu.loadMenu(CCCMenuDef);	// install the menu in the container
	
			if(args.menuHide)
				gmap.routeMenuControl.hide();
				
			if(editing_allowed) {
				CCCMarkerEditing.setListeners();	// defined in CCCediting.js
				gmap.addControl(new CCCMarkerEditing.editingModeControl());
				gmap.disableDoubleClickZoom();
/* 				if(CCCUserForumDetails) CCCGeoCoder = new CCCGeoCode(); */
			} else {
				gmap.enableDoubleClickZoom();
				// set up the RouteMarker set and subsets (one for each route that has markers
				CCCInfoMarkerSets['RouteMarkers'] = new CCCInfoMarkerSet('RouteMarkers', 'grey', 
					false, true, false, true /* i.e loadOnCreate */ ); 
/* 				CCCGeoCoder = new CCCGeoCode(); */
			}
		}
		
		if(args.info) { // for URLs containing: "?info=foo
			var infoIcon = CCCIconSets.routeIcons['red'];
			var infoMarker = new GMarker(initial_center, {icon: infoIcon});
			gmap.addOverlay(infoMarker);
			GEvent.addListener(infoMarker, 'click', function () {
				infoMarker.openInfoWindowHtml('<div id="info_window_div">' + decodeURIComponent(args.info) + '</div>');
				});
			GEvent.trigger(infoMarker, 'click');
		} // args.info
			
		if(args.clickloc)  // if url contains '?clickloc=true'
			CCC_Map.ToggleClickLoc(true);
		 else if(args.clickstate)  // if url contains '?clickstate=true'
			CCC_Map.ToggleClickLoc(false);
		CCCRoutePlanner = new CCCRoutePlannerControl();
		gmap.addControl(CCCRoutePlanner);
		
/* 		if(args.postcode) { */
/* 			CCCGeoCoder.placeMarker(args.postcode); */
/* 		} */
	} 
	
	CCC_Map.permalink = function () {
		var shortMapName = gmap.getCurrentMapType().getName(true);
		var mapType = CCCMapTypes[shortMapName];
		var mapCenter = gmap.getCenter();
		var lat = (mapCenter.lat()).toPrecision(6), lng = (mapCenter.lng()).toPrecision(6);
		var onRoutes = CCCTrack.getChangedOn();
		var offRoutes = CCCTrack.getChangedOff();
		var onMarkers = CCCInfoMarkerSet.getChangedOn();
		var offMarkers = CCCInfoMarkerSet.getChangedOff();
		var streetViewState = CCCStreetView.getJSONState();
		var url = location.href;
		var queryPos = url.indexOf('?');
		if(queryPos > 0) url = url.substring(0, queryPos); // remove the trailing args
		
		location.search = 
			"?" + (args.menu ? "menu=" + args.menu + "&" : "") + "center=" + lat + "," + lng +
			"&zoom=" + gmap.getZoom() + "&maptype=" + mapType + 
			(gmap.routeMenuControl.barVisible ? "" : "&menuHide=true") +
			(onRoutes ? "&onRoutes=" + onRoutes : "") + (offRoutes ? "&offRoutes=" + offRoutes : "") +
			(onMarkers ? "&onMarkers=" + onMarkers : "") + (offMarkers ? "&offMarkers=" + offMarkers : "") +
			(CCCCurrentMarker ? "&showmarker=" + CCCCurrentMarker.set.name + "." + CCCCurrentMarker.id : "") +
			(CCCCurrentFocusRoute ? "&focus=" + CCCCurrentFocusRoute.routeDbRec.text_id : "") +
			(streetViewState ? "&streetView=" + streetViewState : "");
	}
}

function StreetViewHandler(map) {
	var me = this;
	this.map = map;	// the google map
	this.streetView;	// the most recently created view
	this.point;		// the lat,lng initial position of the views
	this.pov;		// the point of view
	this.SVClient = new GStreetviewClient();
	this.svData;
	this.marker;
	this.indicator;
	this.infoWindow;
	this.rideDirection;
	this.rideRunning;
	
	// enable Streetview on rightClick		
	GEvent.addListener(map, 'singlerightclick', function(pixel,url,obj) { 

		if(obj) {	// the user clicked on an overlay, so pass on to the map
			GEvent.trigger(map, 'singlerightclick', pixel, url);
			return;
		}
		me.point = map.fromContainerPixelToLatLng(pixel);
		me.makeView();
	});
	
	this.makeView = function() {
		me.SVClient.getNearestPanorama(me.point, function(panoData) {
			switch (panoData.code) {
			case 500: 
				new CCCTimedELabel(me.point, 'Street View server not responding', 2);
				break;
			case 600: 
				new CCCTimedELabel(me.point, 'No Street View data here', 2);
				break;
			default:
				me.infoWindow = me.map.openInfoWindowHtml(me.point,  
					'<div id="sv_ride_controls"><table><tr>' +
					' <td><input class="sv_ride_button"  type="image" id="rideStart" name="rideStart" ' +
					' title="Start riding"' +
					'src="' + MarkerIcons_path + 'rideRight.png" alt="Ride"/>' +
					' <td><input class="sv_ride_button"  type="image" id="rideStop" name="rideStop" ' +
					' title="Stop riding"' +
					'src="' + MarkerIcons_path + 'rideStop.png" alt="Stop"/>' +
					'<td><div id = "rideRunningIndicator"> To \'ride\' along the road, point the view roughly in the direction of travel <br/> and click on the bike button. Repeat whenever the ride stops. </div>' +
					'</tr></table></div><div id="LargeStreetviewContainer">', 
					{onOpenFn: me.showStreetView});
			}
		});
	}
	
	this.showStreetView = function() {
		var theContainer = document.getElementById('LargeStreetviewContainer');
		me.indicator = document.getElementById('rideRunningIndicator');
		
		GEvent.addListener(me.map.getInfoWindow(), 'closeclick', function() {
				
			me.streetView = null;
			me.marker.remove();
			me.marker = null;
			if(me.rideRunning) me.stopRide();
		});
		
		document.getElementById('rideStart').onclick = me.startRide;
		document.getElementById('rideStop').onclick = me.stopRide;
		
		me.streetView = new GStreetviewPanorama(theContainer, {latlng: me.point, pov: me.pov});
		GEvent.addListener(me.streetView, 'error', function(errorCode) {
			var reportStrings = {
					600: 	'Streetview not available at this position',
					603:	"Flash doesn't appear to be supported by your browser"
				}, 
				report = reportStrings[errorCode];
				
			theContainer.innerHTML = 
				'<i>' + (report?report:"Streetview error number: " + errorCode) + '</i>';
			});

		GEvent.addListener(me.streetView, 'initialized', function(svLoc) {
			me.point = svLoc.latlng;

			me.SVClient.getPanoramaById(svLoc.panoId, function(svData){
				if(svData.code == 200) {
					me.svData = svData;
				} else {
					me.svData = null;
						return;
				}
				if(!me.marker) {
					me.marker = new GMarker(me.point, {draggable: false});
					me.map.addOverlay(me.marker);
				} else {
					me.marker.setPoint(me.point);
				}
			});
		});
	}
	
	this.getJSONState = function() {	// returns a string that can be used to save the SV state in a 'permalink' 
		if(me.streetView) {
			var stateJSON = JSON.stringify({position: {x: me.point.x.toPrecision(8), y: me.point.y.toPrecision(8)}, 
												pov: me.streetView.getPOV()});
			return stateJSON;
		} else {
			return null;
		}
	}

	this.setStateFromJSON =function(stateJSON) {	// recreate the SV state from a 'permalink' SV string
//		var state = eval('(' + stateJSON + ')');
		var state = JSON.parse(stateJSON);
		
		if(!state) {
			GLog.write('Invalid streetView state: ' + stateJSON);
			return;
		}
		this.point = new GLatLng(state.position.y, state.position.x);
		this.pov = state.pov;
		
		this.makeView();
	}
	
	this.startRide = function() {
		me.rideDirection = me.streetView.getPOV().yaw;
		me.indicator.innerHTML = '<img src ="' + MarkerIcons_path + 'fiets118.gif" height="20px" />';
		me.streetView.followLink(me.rideDirection);
		me.rideRunning = window.setInterval(me.ride, 2000);
		
	}
	
	this.stopRide = function() {
		if(me.rideRunning) {
			window.clearInterval(me.rideRunning);
			me.rideRunning = null;
			me.indicator.innerHTML = '';
		}
	}

	this.ride = function() {
		if(me.svData) {
  			if(me.svData.links.length != 2) {
  				me.stopRide();
  				return;
  			}
		}
		if(me.rideDirection !== null) me.streetView.followLink(me.rideDirection);
	}
}

/* Journey planner control */

var CCCHelpWindow = null;
// Originally from http://www.htmlcodetutorial.com/linking/linking_famsupp_72.html
// but much-improved!
// Used to pop up the help window
function popup(mylink, windowname, w, h) {

	if(CCCHelpWindow) if(!CCCHelpWindow.closed) {
		CCCHelpWindow.focus();
		return false;
	}
	CCCHelpWindow = window.open(mylink, windowname, 'width=' + w + ',height=' + h + 
			',scrollbars=yes,dependent=yes,resizable=yes');
	return false;
}

function DumpObject(anObject) {
	var obj = '';
	if(anObject['name']) obj += anObject['name'] + ':';
	else if(anObject['id']) obj += anObject['id'] + ':';
	obj += '{';
	
	for(anElementName in anObject) {
		var anElement = anObject[anElementName];
		if((typeof anElement) == 'object') obj += DumpObject(anElement);
		else obj += anElementName + ': ' + anElement + ', ';
	}
	obj += '}';
	return (obj);
}

function CCCDisplayWindow () {
	this.visible = false;
	
	var me = this;

	this.container = document.createElement("div");
	this.closeBox = document.createElement("closeBox");

	this.container.style.position = "absolute";
//	map.getPane(G_MAP_FLOAT_SHADOW_PANE).appendChild(this.container); // for a window that floats with the map.
//	if(map) map.getContainer().appendChild(this.container);
//	else 
	document.body.appendChild(this.container);

	this.openOnMap = function (offset, maxWidth, html, nonClickable)	{
		this.container.innerHTML = '<div id="displayWindow">' + html + '</div>';
		this.container.style.left = offset.x + 'px';
		this.container.style.top = offset.y + 'px';
		this.container.style.zIndex = 99;
		if(maxWidth > 0) this.container.style.width = maxWidth +'px';
		if(!nonClickable) this.container.style.cursor = 'pointer';
		this.visible = true;
		this.show();
		if(!nonClickable) {
			this.container.appendChild(this.closeBox);
			this.closeBox.innerHTML = "<b>X</b>"; 
			GEvent.addDomListener(this.container, 'click', function() { me.hide(); });
		}
    }
    
      this.remove = function() {
        this.container.parentNode.removeChild(this.container);
        this.visible = false;
      }

      this.show = function() {
        this.container.style.display="";
        this.visible = true;
      }
      
      this.hide = function() {
        this.container.style.display="none";
        this.visible = false;
      }
      
      this.isHidden = function() {
        return !this.visible;
      }
}

/* CCCHelpDisplay is a function to add an overlaid help feature to a Google map mashup. 

	It adds a '?' button to the map. When the button is selected it overlays four transparent PNG images containing help information.
	The four PNG files are in a directory referenced by the parameter helpImageDirectoryUrl with file names:
		topLeft.png, topRight.png, bottomLeft.png, bottomRight.png
	
*/

function CCCHelpDisplay (helpImageDirectoryUrl, controlAnchor, controlOffset) {
	this.containers = [];
	this.positions = ["topLeft", "topRight", "bottomLeft", "bottomRight"];
	this.helpButton;
	this.visible = false;
	this.loaded = false;
	this.theControls;
	
	var me = this;
			
	this.show = function() {
		if(!this.loaded) this.load();
		for(aPosition in this.positions)
			this.containers[aPosition].style.display="";
		this.visible = true;
	}

	this.hide = function() {
		for(aPosition in this.positions)
			this.containers[aPosition].style.display="none";
		this.visible = false;
	}

	this.load = function() {
		for(aPosition in this.positions) {
			this.containers[aPosition].innerHTML =
				"<img src=\"" + helpImageDirectoryUrl + this.positions[aPosition] + ".png\">";
		}
		this.loaded = true;
	}
	
	// initialization
		this.helpButton = document.createElement("helpButton"); 
		this.helpButton.style.background = "white";
		this.helpButton.style.height = "15px";
		this.helpButton.style.width = "15px";
		this.helpButton.style.textAlign = "center";
		this.helpButton.style.fontWeight = "bold";
		this.helpButton.style.fontSize = "14px";
		this.helpButton.style.fontFamily = "Arial";
		this.helpButton.style.border =  "1px solid black";
		this.helpButton.style.cursor = "pointer";
		this.helpButton.style.zIndex = 999;	// the button must always be on top so it can be clicked
		this.helpButton.title = "Shows a description of all the controls";
		this.helpButton.innerHTML = "?";		
		gmap.addControl(new CCCGenericControl(this.helpButton, controlAnchor, controlOffset, function () {
				if(!me.visible) {
					me.show();
					me.helpButton.style.background = "black";
					me.helpButton.style.color = "white";
				} else {
					me.hide();
					me.helpButton.style.background = "white";
					me.helpButton.style.color = "black";
				}
			}));
	var overlayZIndex = this.helpButton.style.zIndex - 1;	// the button must always be on top so it can be clicked!
	for(aPosition in this.positions) {
		this.containers[aPosition] = document.createElement("div");
		this.containers[aPosition].style.position = "absolute";
		this.containers[aPosition].style.display = "none";
		this.containers[aPosition].style.zIndex = overlayZIndex;
		gmap.getContainer().appendChild(this.containers[aPosition]);
		switch (this.positions[aPosition]) {
		case "topLeft":
			this.containers[aPosition].style.top = '0px';
			this.containers[aPosition].style.left = '0px';
			break;
		case "topRight":
			this.containers[aPosition].style.top = '0px';
			this.containers[aPosition].style.right = '0px';
			break;
		case "bottomLeft":
			this.containers[aPosition].style.bottom = '0px';
			this.containers[aPosition].style.left = '0px';
			break;
		case "bottomRight":
			this.containers[aPosition].style.bottom = '0px';
			this.containers[aPosition].style.right = '0px';
			break;
		}
	}
}

function CCCGenericControl (anElement, anAnchor, aPosition, aClickHandler) {
	// convert a document element to a map control
	this.theElement = anElement;
	this.theAnchor = anAnchor;
	this.thePosition = aPosition;
	this.theListener;

	this.initialize = function(map) {
		map.getContainer().appendChild(anElement);
		return anElement;
	}
	
	this.getDefaultPosition = function() {
		return new GControlPosition(anAnchor, aPosition);
	}
	
	if(aClickHandler) this.theListener = GEvent.addDomListener(this.theElement, "click", aClickHandler);
}
CCCGenericControl.prototype = new GControl();

function CCCTimedELabel(point, contents, seconds) {
	this.eLabel = new ELabel(point, contents, 'timed_label_div');
	gmap.addOverlay(this.eLabel);
	var me = this;
		
	this.remove = function() {
		gmap.removeOverlay(me.eLabel);
	}
	window.setTimeout(this.remove, seconds * 1000);
}

// this depends on the jquery.cookies.js package at http://code.google.com/p/cookies/
function Cookies(path) {
	this.path = path ? path : "/CamdenMaps";
	jaaulde.utils.cookies.setOptions({path: this.path});

	// fail if the browser doesn't accept cookies	
	if(!jaaulde.utils.cookies.test()) return null;
	
	this.currentCookies = jaaulde.utils.cookies.get();
	
	this.set = function(name, value) {
		this.currentCookies.name = value;
		return jaaulde.utils.cookies.set(name, value);
	}

	// remove the named cookie
	this.remove = function(name) {
		this.currentCookies.name = null;
		jaaulde.utils.cookies.del(name);
	}

	this.test = function() {
		return jaaulde.utils.cookies.test();
	}
}

/*
function Prefs () {	// allows user to choose their initial map configuration
					// returns an object containing the user's preferred loc, zoom, maptype and menuSpec
// design:
//	Overlay a dialog enabling the user to choose from regions of London or make their own choice. Then click on a 'save prefs' button.
//

}
*/