/* Generic information markers for CCC GMaps
*/

var CCCProgressWin;

function CCCInfoMarkerSet (name, colour, initially, labels, subSet, loadOnCreate) {
	this.name = name;
	this.subSet = subSet;
	this.tableName = subSet ? null : 
		CCCInfoMarkerDefs[name]._DBTable ? CCCInfoMarkerDefs[name]._DBTable : name;// a database table
	this.isSpatial = subSet ? false :				//true if the database table has 'gLoc' column
		CCCInfoMarkerDefs[this.name]._isSpatial;	// true if we require the user to indicate an area for display
	this.colour = colour;
	this.labels = labels;	// when true, we display a tooltip title on each marker when mouse is over the menuItem
	this.icon = null;				// this is a default icon. May be overridden when individual object is created.
	this.initiallyOn = (initially == 'on');	
			// true if all the markers are to be displayed initially
	this.someVisible = false;
	this.someLoaded = subSet ? true : false;
	this.visibleSubType = null;			// null unless we have made all but one subtype of marker invisible
	this.selectiveDisplay = subSet ? false :
		CCCInfoMarkerDefs[this.name]._selectiveDisplay;	// false, or string: 'window' or 'selection'
	this.realTime = CCCInfoMarkerDefs[this.name]._realTime;
	this.showText = "Show";
	if(this.selectiveDisplay == 'selection') this.showText += ' ' + this.selectiveDisplay;
	this.keyHTML = CCCInfoMarkerDefs[this.name]._keyHTML;
	if(this.keyHTML) {
 		this.displayWin =  new CCCDisplayWindow ();	//  to hold the explanatory key
		this.keyText = '<span>&nbsp;&nbsp;(<span ' + 
					'onmouseover=\"this.style.textDecoration=\'underline\' \" ' +
					'onmouseout=\"this.style.textDecoration=\'none\'\" ' +
					'onclick=\"CCCInfoMarkerSets[\'' + this.name + '\'].keyDisplay()\"' +
					'><i><span id="keyToggle' + this.name+ '">key</span></i></span>)</span>';
	}
	
	this.displayBounds = null;			//  a GLatLngBounds object defining the area in 
										// which these markers are currently showing
	this.members = [];				// subscripted by id; holds just those infoMarkers that have been loaded from the DB
	this.pendingShow = false;		// true when we're waiting for a DB fetch for a selective set of markers
	var me = this;
	

/* Class variables */
	CCCInfoMarkerSet.loadsInProgress = 0;
	if(!CCCInfoMarkerSet.progressWindow) {
		CCCInfoMarkerSet.progressWindow = new CCCDisplayWindow ();
	}
	
/* Class methods */
	CCCInfoMarkerSet.aLoadInProgress = function(name) {
		CCCInfoMarkerSet.progressWindow.openOnMap(CCCProgressWinPos, 180, "Loading " + name +
			'...' + CCCProgressImage, false);
		CCCInfoMarkerSet.loadsInProgress += 1;
		CCCInfoMarkerSet.progressWindow.show();
	}

	CCCInfoMarkerSet.aLoadFinished = function() {
		if(CCCInfoMarkerSet.loadsInProgress > 0) CCCInfoMarkerSet.loadsInProgress -= 1;
		if(CCCInfoMarkerSet.loadsInProgress <= 0 && CCCInfoMarkerSet.progressWindow) {
					CCCInfoMarkerSet.progressWindow.hide();
				}
	}

	CCCInfoMarkerSet.getChangedOn = function () { // used by CCC_Map.permalink()
		var changedOn = "";
		for (aSet in CCCInfoMarkerSets) {
			if(!CCCInfoMarkerSets[aSet].subSet && !CCCInfoMarkerSets[aSet].initiallyOn && 
				CCCInfoMarkerSets[aSet].someVisible) 
				changedOn += "," + aSet;
		}
		return changedOn.substring(1,changedOn.length);
	}
	
	CCCInfoMarkerSet.getChangedOff = function () { // used by CCC_Map.permalink()
		var changedOff = "";
		for (aSet in CCCInfoMarkerSets) {
			if(!CCCInfoMarkerSets[aSet].subSet && CCCInfoMarkerSets[aSet].initiallyOn && 
				!CCCInfoMarkerSets[aSet].someVisible) 
				changedOff += "," + aRoute;
		}
		return changedOff.substring(1,changedOff.length);
	}
	
	CCCInfoMarkerSet.mapViewChanged = function () { // invoked by callback(s) that detect any changes in the map view
		for (aSet in CCCInfoMarkerSets) {
			if(CCCInfoMarkerSets[aSet].someVisible && CCCInfoMarkerSets[aSet].selectiveDisplay == "window") {
				CCCInfoMarkerSets[aSet].showAllInWindow();
			}
		}
	}

/* Instance methods */
	
	this.loadFromDB = function (boundString) { // if boundString isn't present, loads all in DB
		// load from DB, create InfoMarker objects and add them to members
		// NB, it is possible to load the same DB entries more than once, so should check this.members before 
		// creating each new marker.

//	GLog.write('loadFromDB ' + me.name);
			
		var whereClause = boundString ? "MBRContains(GeomFromText('POLYGON("+boundString+ ")'),gLoc)":"1";
		var recordName = this.name + '_record';
		var queryString = "?x=QUERY&dbCommand=SELECT " + this.getFieldNames() + 
				" FROM " + this.tableName + ' WHERE ' + whereClause;
		if(this.name != 'RouteMarkers') {
			this.pendingShow = true;
			CCCInfoMarkerSet.aLoadInProgress(this.name);
		}
		aSyncDBQuery(this.tableName, recordName, queryString, 'XMLJSON', function (XMLResult, textResult) {
			var records = XMLResult.documentElement.getElementsByTagName(me.name + '_record');
			for( var i = 0; i < records.length; i++) {
				var JSON = records[i].getAttribute("JSON");
				var markerData = eval('(' + JSON + ')');
				// apply decodeURIComponent to each element of markerData
				for( var anAttribute in markerData) {
					markerData[anAttribute] = decodeURIComponent(markerData[anAttribute]);
				}
				var anInfoMarker = new CCCInfoMarker(markerData.id, me, markerData);
				
				if(!me.members[markerData.id] ) me.members[markerData.id] = anInfoMarker;
	
				if(this.name == 'RouteMarkers') { // special case to create subset of info markers for each route
					var aTextId = markerData.text_id;
					if(!CCCInfoMarkerSets[aTextId]) {
						CCCInfoMarkerSets[aTextId] = 
							new CCCInfoMarkerSet (aTextId, colour, false, false, labels);
					}
					CCCInfoMarkerSets[aTextId].members[markerData.id] = anInfoMarker;
				}
			}
			me.someLoaded = true;
				
//			if (me.pendingShow) me.showAll();
			if(CCCPendingMarkerShow && CCCPendingMarkerShow.setName == me.name) {
				if(!me.initiallyOn) me.showAll();
				var theMarker = me.members[CCCPendingMarkerShow.id];
				if(!theMarker.gMarker) theMarker.createGMarker();
				
				if(theMarker && theMarker.gMarker) {
					theMarker.showInfoWindow();
				}
				CCCPendingMarkerShow = null;
			}
			me.pendingShow = false;
			CCCInfoMarkerSet.aLoadFinished();
			if(me.name != 'RouteMarkers') {
					me.showAll();
			}
		});
	}

	// special case for loading stands from Camden's GIS server
	// note that 0.5 km is the maximum effective value for distance - higher values will be ignored by the server
	this.loadFromCamden = function(midPoint, distance, maxNo) {
		var params = "?type=Bicycle+stands&lat=" + midPoint.lat().toPrecision(5) + 
								"&lng=" + midPoint.lng().toPrecision(5);
		var nLoaded = 0;
		CCCInfoMarkerSet.aLoadInProgress(this.name);
		GDownloadUrl(proxyURL + "?url=" + CamdenGISURL + encodeURIComponent(params), 
			function(xmlText, responseCode) {
				if(responseCode == 200) {
					xmlDoc = GXml.parse(xmlText);
					var markers = xmlDoc.getElementsByTagName("ParkingBay"); 
					var nShown = 0;
					for(var i = 0; i<markers.length; i++) {
						var aMarker = markers[i];
						if(distance && (Number(aMarker.getAttribute("Distance")) > distance  || nShown > maxNo)) 
							break;
						if(aMarker.getAttribute("Type") == "Bicycle stand") {
							var id = "lbc" + aMarker.getAttribute("LocationID");
							if(!me.members[id]) { // if we haven't already loaded this stand
								var dbData = 
									{subType:"Street", 
									number: 1,
									title: "<img src=\"" + MarkerIcons_path + 
													"camdenlogo-20.png\"/> Camden street stand",
									id: id, 
									_lat: aMarker.getAttribute("Lat"), 
									_lng: aMarker.getAttribute("Lng"), 
									_iconLabel: "Camden",
									SVState: null, 
									_contributed_by: "LBC", _contributor_id: null};
								dbData.comment = "Based on live data from the London Borough of Camden.";
								var anInfoMarker = new CCCInfoMarker(id, me, dbData);
								me.members[id] = anInfoMarker;
								nShown += 1;
								me.someLoaded = true;
								me.pendingShow = true;
								nLoaded += 1;
							}
						}
					}
					me.pendingShow = false;

				} else switch (responseCode) {
					case 500:
						alert("Problem fetching stands from Camden's server.");
						break;
					default:
						alert(" Error code " + responseCode + ", " + xmlText);
				}
				CCCInfoMarkerSet.aLoadFinished();	
				me.showAll();
			});
	}

	// Another special case for loading stands from Cloudmade's OSM DB server
	// expect JSON result
	this.loadFromCloudMade = function(midPoint, bbox, maxNo) {
// 	@"?object_type=bicycle_parking&bbox=%F,%F,%F,%F&bbox_only=true&results=%i",
//	 bounds.southwest.latitude, bounds.southwest.longitude, 
//	 bounds.northeast.latitude, bounds.northeast.longitude, (int)100];

		var params = "?object_type=bicycle_parking&bbox_only=true&bbox=" + 
						bbox.S + "," + bbox.W + "," + bbox.N + "," + bbox.E + "&results=" + maxNo;
		var nLoaded = 0;
		CCCInfoMarkerSet.aLoadInProgress(this.name);
		GDownloadUrl(proxyURL + "?url=" + CloudMadeURL + encodeURIComponent(params), 
			function(jsText, responseCode) {
				if(responseCode == 200) {
				var result = JSON.parse(jsText);
				var markers = result.features;
				if(markers) {
						var nShown = 0;
						for(var i = 0; i<markers.length; i++) {
							var aMarker = markers[i];
							
							var id = aMarker.properties.osm_id;
							if(!me.members[id]) { // if we haven't already loaded this stand
								var dbData = 
									{subType:"Street", 
									number: 1,
									title: "<img src=\"" + MarkerIcons_path + 
													"OSMlogo-20.png\"/> OSM cycle parking",
									id: "osm" + id, 
									_lat: aMarker.centroid.coordinates[0], 
									_lng: aMarker.centroid.coordinates[1], 
									_iconLabel: "OSM",
									SVState: null, 
									_contributed_by: "OpenStreetMap", _contributor_id: null};
								dbData.comment = "Based on data from openstreetmap.org";
								var anInfoMarker = new CCCInfoMarker(id, me, dbData);
								me.members[id] = anInfoMarker;
								nShown += 1;
								me.someLoaded = true;
								me.pendingShow = true;
								nLoaded += 1;
							}
						}
						me.pendingShow = false;
					}
				} else switch (responseCode) {
					case 500:
						alert("Problem fetching stands from CloudMade's server.");
						break;
					default:
						alert(" Error code " + responseCode + ", " + xmlText);
				}
				CCCInfoMarkerSet.aLoadFinished();
				me.showAll();
			});
	}
	
	// Yet another special case for loading stands from CycleStreets server
	// expect JSON result
	this.loadFromCycleStreets = function(midPoint, bbox, maxNo) {
		// Unfortunately CycleStreets wants geo-coordinate bounding box, which is tricky to calculate, 
		// so we'll do a very crude approximation based on southern England latitudes.
		var centerLat = midPoint.lat().toPrecision(6);
		var centerLng = midPoint.lng().toPrecision(6);
		
		var params = "&useDom=0&feature=cycleparking&meta=good&zoom=12&latitude=" + midPoint.lat().toPrecision(6) + 
					"&longitude=" + midPoint.lng().toPrecision(6) + 
					"&n=" + bbox.N + "&s=" + bbox.S + "&e=" + bbox.E +  "&w=" + bbox.W;

		var nLoaded = 0;
		CCCInfoMarkerSet.aLoadInProgress(this.name);
		GDownloadUrl(proxyURL + "?url=" + cycleStreetsStandsURL + encodeURIComponent(params), 
			function(xmlText, responseCode) {
				if(responseCode == 200) {
					xmlDoc = GXml.parse(xmlText);
					var markers = xmlDoc.getElementsByTagName("marker"); 
					var nShown = 0;
					for(var i = 0; i<markers.length; i++) {
						var aMarker = markers[i];
						var id = "cs" + aMarker.getAttribute("id");
						if(!me.members[id]) { // if we haven't already loaded this stand
							var dbData = 
								{subType:"Street", 
								number: 1,
								title: "<img src=\"" + MarkerIcons_path + 
												"cyclestreetslogo-20.png\"/> CycleStreets stand",
								id: id, 
								_lat: aMarker.getAttribute("latitude"), 
								_lng: aMarker.getAttribute("longitude"), 
								_iconLabel: "CycleStreets",
								SVState: null, 
								_contributed_by: "CycleStreets", _contributor_id: null};
							dbData.comment = aMarker.getAttribute("caption") +
											"<br/>Based on live data from cyclestreets.net";
							var anInfoMarker = new CCCInfoMarker(id, me, dbData);
							me.members[id] = anInfoMarker;
							nShown += 1;
							me.someLoaded = true;
							me.pendingShow = true;
							nLoaded += 1;
						}
					}
					me.pendingShow = false;

				} else switch (responseCode) {
					case 500:
						alert("Problem fetching stands from CycleStreets server.");
						break;
					default:
						alert(" Error code " + responseCode + ", " + xmlText);
				}
				CCCInfoMarkerSet.aLoadFinished();
					me.showAll();
			});
	}
	
/* Special case code to the 'screenscraped' TfL live hire station info from cyclehireapp.com/cyclehirelive */
	this.loadFromCycleHireApp = function() { // the params are ignored at present
		CCCInfoMarkerSet.aLoadInProgress(this.name);
		GDownloadUrl(proxyURL + "?url=" + cycleHireAppScreenScrapeURL, 
			function(responseText, responseCode) {
				if(responseCode == 200) {
					var stations = responseText.split('\n');
					var stationArray = [];
					var updateTime;
//	GLog.write(stations.length + " stations loaded");
					if(stations.length < 100) alert("Problem loading Hire Stations status");
					else {
						var t = stations[0].split(' '),
							timeOffset = -parseInt((new Date()).getTimezoneOffset()/60);
							UTCHours = t[1].slice(0, t[1].indexOf(':')),
							minutes = t[1].slice(t[1].indexOf(':')+1, t[1].lastIndexOf(':'));

						updateTime = (Number(UTCHours)+timeOffset) + ':' +  minutes;
//	GLog.write(updateTime);
						for (var i = 1; i < stations.length; i++) {
//	GLog.write(stations[i]);

							var	aStation = stations[i].split(',');
							if(aStation.length < 10) continue;
							var id = aStation[0], 
								location = aStation[1] + ', ' + aStation[2], 
								nBikes = aStation[5], 
								nEmptyDocks = aStation[6],
								nB = Number(nBikes), nD = Number(nEmptyDocks),
								availability = nB/(nB + nD),
								iconType = parseInt((10 * availability) + 1);
								if(isNaN(iconType)) iconType = 1;
								else if(iconType > 10) iconType = 10;
							var marker = me.members[id];
							var dbData = {};

							if(!marker) { // no marker has been instantiated for this id
								marker = new CCCInfoMarker(id, me, dbData);
								me.members[id] = marker;
							}
							dbData = 
								{subType: "HS" + iconType, 
								id: id, 
								location: location,
								_lat: aStation[3], 
								_lng: aStation[4], 
								nBikes: nBikes,
								nEmptyDocks: nEmptyDocks,
								title: location + ' ' + 
									nBikes + ' bikes, ' + nEmptyDocks + ' spaces',
								availability: parseInt(availability*100) + '%',
								upDateTime: updateTime,
								SVState: null, 
								source: 'based on live data supplied by <a href="http://cyclehireapp.com/" target="_blank"> cyclehireapp.com </a>',
								installed: aStation[7],
								locked: aStation[8],
								temporary: aStation[9]
							};
							marker.refreshDbData(dbData);
//	GLog.write(dbData.title);
							me.someLoaded = true;
//							me.pendingShow = true;
						}
					};
					// deal with arg 'showmarker=TfLHireStations.xx'
					if(CCCPendingMarkerShow && CCCPendingMarkerShow.setName == me.name) {
						if(!me.initiallyOn) me.showAll();
						var theMarker = me.members[CCCPendingMarkerShow.id];
						if(!theMarker.gMarker) theMarker.createGMarker();
						
						if(theMarker && theMarker.gMarker) {
							theMarker.showInfoWindow();
						}
						CCCPendingMarkerShow = null;
					}

				} else switch (responseCode) {
					case 500:
						alert("Problem fetching data from CycleHireApp's server.");
						break;
					default:
						alert(" Error code " + responseCode + ", " + responseText);
				}
				CCCInfoMarkerSet.aLoadFinished();
				me.showAll();
			});
	}
	
	this.getFieldNames = function() {
		var nameList = 'id', 
			fieldDefs = CCCInfoMarkerFormDefs[this.name].fields;
		
		if(this.name != "Routes") nameList += ",_lat,_lng"; // all infoMarkers except Routes (for editing) have these fields
		
		for (var aField in fieldDefs) nameList += ',' + aField;
				
		return nameList;
	}
	
	this.setMenuState = function() {
		var toggleElement = document.getElementById('toggle' + this.name);
		if(!toggleElement) return false;
		// possible IE problem: the element sometimes doesn't exist
		if(this.someVisible) {
			toggleElement.innerHTML = '<i>Hide</i>';
			toggleElement.style.color = colourTable[this.colour].code;
		} else {
			toggleElement.innerHTML = '<i>' + this.showText + '</i>';
			toggleElement.style.color = CCCDimmedColour;
		}
	}
	
	this.toggle = function() {
		if(me.someVisible) {
			me.hideAll();
		} else {
			if(editing_allowed) me.showAll();
			else me.show();
/*
			if(me.selectiveDisplay && !editing_allowed) { // always show everything for editing
				if(me.selectiveDisplay == 'selection')
					me.showSelective();
				else
					me.showAllInWindow();
			} else {
				me.showAll();
			}
*/
		}
	}
	
	this.show = function() { // called from onclick on marker items in the menu
			if(me.selectiveDisplay) {
				if(me.selectiveDisplay == 'selection')
					me.showSelective();
				else
					me.showAllInWindow();
			} else if(me.realTime) {	// e.g. TfLHireStations
					me.upDateRealTime();
				}
			else me.showAll();
	}

	this.selectFeedbackBox = null;

	this.showSelective = function () { 
		var mouseOverListener;
		var radius = CCCStandSelectDiameter/2, side, topLeft; // side, topLeft will be computed in the mosemove handler
		var mapContainer = gmap.getContainer();
		var position = gmap.getCenter();
		var boxClickListener;
		var clickListener = GEvent.addListener(gmap, 'click', function(overlay, position, pixel) { 
				me.showMarkersAround(position, CCCStandSelectDiameter);
			// remove feedback box and turn listeners off
			GEvent.removeListener(clickListener);
			GEvent.removeListener(mouseMoveListener);
			GEvent.removeListener(boxClickListener);
			if(me.selectFeedbackBox) mapContainer.removeChild(me.selectFeedbackBox);
			me.selectFeedbackBox = null;
		});
		// we're in the 'stands position' mode, here's the feedback:
		// show a crosshair of CCCStandSelectDiameter km width
		var mouseMoveListener = GEvent.addDomListener(mapContainer, 'mousemove', function(event) {
			if(!me.selectFeedbackBox) {	
				// work out the square bounds in latLng
				var NE = position.destPoint(45, radius);
				var SW = position.destPoint(225, radius);
				// now get corners of the square in pixel coords
				var NEPixel = gmap.fromLatLngToContainerPixel(NE);
				var SWPixel = gmap.fromLatLngToContainerPixel(SW);
				side = SWPixel.y - NEPixel.y;
				halfSide = parseInt(side/2);
				
				me.selectFeedbackBox = document.createElement("selectFeedbackBox");
				me.selectFeedbackBox.style.width = side + "px";
				me.selectFeedbackBox.style.height = side + "px";
				me.selectFeedbackBox.innerHTML = "Click to show stands";
				mapContainer.appendChild(me.selectFeedbackBox);
				boxClickListener = GEvent.addDomListener(me.selectFeedbackBox, 'click', function(clickEvent) {
					GEvent.trigger(gmap, 'click', me.selectFeedbackBox, 
						gmap.fromContainerPixelToLatLng(new GPoint(clickEvent.clientX, clickEvent.clientY)), clickEvent);
					});
			}
			me.selectFeedbackBox.style.top = (event.clientY - halfSide) + "px";
			me.selectFeedbackBox.style.left = (event.clientX - halfSide) + "px";
		});
	}
	
/*
	function getBoundingPoly () {
		var displayBounds = gmap.getBounds();
		var NE = displayBounds.getNorthEast(),
			SW = displayBounds.getSouthWest(),
			N = NE.lat().toPrecision(6), S = SW.lat().toPrecision(6), 
				W = SW.lng().toPrecision(6), E = NE.lng().toPrecision(6);
				
		return '('+N+' '+W+','+N+' '+E+','+S+' '+E+','+S+' '+W+','+N+' '+W+')';
		
	}
*/

	this.showAllInWindow = function () { 
		me.showInRectangle(gmap.getBounds());
	}

	this.showInRectangle = function (bounds) {	// param is a GLatLngBounds
		// construct query string and make a DBQuery to get the markers within a rectangle
		var NE = bounds.getNorthEast(),
			SW = bounds.getSouthWest(),
			bbox = { "N": NE.lat().toPrecision(6), "S": S = SW.lat().toPrecision(6),
						"E": NE.lng().toPrecision(6), "W": SW.lng().toPrecision(6)};
		var boundString = '('+bbox.N+' '+bbox.W+','+bbox.N+' '+bbox.E+','+bbox.S+' '+
							bbox.E+','+bbox.S+' '+bbox.W+','+bbox.N+' '+bbox.W+')';

//	GLog.write('showInRectangle ' + me.name);
		// gets the markers in bbox rectangle from the DB and display them
//		if(bounds.intersects(LondonRect)) {
			me.loadFromDB(boundString);
//		}
		// special case for getting stands from 3rd party servers (e.g. Camden GIS server)
		if(me.name == "Stands") {
			if(bounds.intersects(CamdenRect)) {
				me.loadFromCamden(bounds.getCenter(), CCCStandSelectDiameter/2, 20);
			}
			me.loadFromCloudMade(bounds.getCenter(), bbox, 20);
			me.loadFromCycleStreets(bounds.getCenter(), bbox, 20);
		}
	}
	
	this.upDateRealTime = function () {
		// special case for screenscraping the TfL hire stations
		if(me.name == "TfLHireStations") {
			me.loadFromCycleHireApp();
		}

	}
	
	this.showMarkersAround = function (aPlace, size) { // size is side of square in kilometres
		// We really want a geo-coordinate bounding box, which is tricky to calculate, 
		// so we'll do a very crude approximation based on southern England latitudes.
		if(!size) size = CCCStandSelectDiameter;
		var NE = aPlace.destPoint(45, size/2),
			SW = aPlace.destPoint(225, size/2);

		this.showInRectangle(new GLatLngBounds(SW, NE));
	}
			
	this.showAll = function () { 
//	GLog.write('showAll ' + me.name);
		if(!this.someLoaded) { // load all the markers from the DB
			this.loadFromDB();
			this.pendingShow = true;
		} else {
			for (var aMarkerId in this.members) {
				this.members[aMarkerId].show();
			}
			me.someVisible = true;
			me.setMenuState();
		}
	}

	this.hideAll = function (exceptedSubType) { 
		for (var aMarkerId in this.members) {
			this.members[aMarkerId].hide();
		}
		me.someVisible = false;
		this.setMenuState();
	}
	
	this.hideExcept = function (exceptedSubType) { 
		for (var aMarkerId in this.members) {
			if(this.members[aMarkerId].subType != exceptedSubType) {
				this.members[aMarkerId].hide();
			}
		}
		this.visibleSubType = exceptedSubType;
	}
	
	this.showSubType = function(subType, checkbox) {
		if(checkbox.checked) {
			this.hideExcept(subType);
		}
		else {
			this.showAll();
			this.visibleSubType = null;
		}
	}
	
	this.showLabels = function () { 
		if (this.labels && me.someVisible) 
			for (var aMarkerId in me.members) {
				this.members[aMarkerId].showELabel();
			}
	}

	this.hideLabels = function () { 
		if (this.labels && me.someVisible) 
			for (var aMarkerId in me.members) {
				this.members[aMarkerId].hideELabel();
		}
	}
	
	this.keyDisplay = function () {
		if(me.displayWin.instantiated) {
			if(me.displayWin.visible) {
				me.displayWin.hide();
			} else {
				me.displayWin.show();
			}
		} else {
			me.displayWin.openOnMap(CCCKeyWinPosition, 0, me.keyHTML, false); // 0 width will adapt to size of content
		}
	}

	if(loadOnCreate) 
		this.loadFromDB(); // loadOnCreate is true only when explicitly specified, e.g. for RouteMarkers. 
	else 
		if((this.initiallyOn  && !arrayContains(CCCInitiallyOffMarkers, this.name)) || 
			 arrayContains(CCCInitiallyOnMarkers, this.name)) {
				if(this.selectiveDisplay) 
					this.showInRectangle(gmap.getBounds());
				else
					this.show();
		}
}	// end of CCCInfoMarkerSet

function CCCInfoMarker (id, set, dbData) { 	
// properties set from parameters
	this.id = id;
	this.set = set;			// an instance of CCCInfoMarkerSet
	this.dbData = dbData;
// required properties obtained from db
	this.subType = null;	// name for a sub-set/type should be set in handleDbData
	this._modDate = null;
	this._starts = null;
	this._expires = null;
	this._lat = null;
	this._lng = null;
// optional properties obtained from db
	this.title = null;		// optional, used in 'tooltips' use set type if absent
	this.labelText = null;	// optional, for use in eLabels
// other properties will be added according to the results retrieved.
	this.extraAttributes = [];
	
// derived properties	
	this.gLatLng = null;		// a GLatLng
	this.description = null;
	this.icon = null;		
	this.gMarker = null;		// a GMarker 
	this.infoTabs = null;
	this.eLabel = null;			// will be instantiated as an ELabel when first used.
	this.visible = false;
	this.startDate = null;
	this.expiryDate = null;
	
	var me = this;
		
	this.initialize = function () {
		// instantiate everything that doesn't require user or DB input

		this.handleDbData (this.dbData);
		if(!this.subType) this.subType = 'Standard';
		if(this.title) {
			if (!this.number && this.subType != 'Standard')
				this.title = this.set.name + ' #' + this.dbData.id + ' ' + this.subType + ': '  + this.title;
		} else { // a title is needed to display in rollover labels
			this.title = '';
			if(this._number && this._number > 0) this.title = this._number + ' '; // Special case for bike stands
			else if(this.number && this.number > 0) this.title = this.number + ' '; // Special case for hirestations
			else if(this.victim) this.title = this.victim + ', ' + this.otherVehicle + ' '; // Special case for casualties
			this.title += this.subType + ' ' + this.set.name;
		}
		
		// instantiate everthing else
		if(this.dbData._LatLng) {
			var latLng = this.dbData._LatLng.split(',');
			this._lat = latLng[0];
			this._lng = latLng[1];
		}
		if(this._lat && this._lng) 
			this.gLatLng = new GLatLng(this._lat, this._lng);
	}

	this.refreshDbData = function (dbData) {	// used for marker types that have real-time feeds
		this.updateMarker(dbData);
	}

	this.handleDbData = function (dbData) {	
		for (anAttribute in dbData) {
			if(this[anAttribute] === null) {
				this[anAttribute] = dbData[anAttribute];
			} else if(this[anAttribute] === undefined) {
				this[anAttribute] = dbData[anAttribute];
				this.extraAttributes.push(anAttribute);
			}
		}
		this._starts = (this.dbData.starts?this.dbData.starts: 
			(this.dbData._starts?this.dbData._starts:null));			
		if(this._starts) { // parse a date from the DB
			var dateParts = this.expires.split(/[- ]/);
			this.startDate = new Date(dateParts[2], dateParts[1]-1, dateParts[0]);
		}
		this._expires = (this.dbData.expires?this.dbData.expires: 
			(this.dbData._expires?this.dbData._expires:null));			
		if(this._expires) { // parse a date from the DB
			var dateParts = this.expires.split(/[- ]/);
			this.expiryDate = new Date(dateParts[2], dateParts[1]-1, dateParts[0]);
		}
	}
	
	this.updateMarker = function (newData) { // used by CCCEditing functions and this.refreshDbData only
		for (var anAttribute in newData) {
			this.dbData[anAttribute] = newData[anAttribute];
			this[anAttribute] = newData[anAttribute];
		}
		this.gLatLng = new GLatLng(this._lat, this._lng);	// in case the marker was dragged
		
		//give it a new marker in case anything has changed.
		if(this.gMarker) gmap.removeOverlay(this.gMarker);
		this.createGMarker();
		gmap.addOverlay(this.gMarker);
	}

	this.deleteMarker = function () { // used by CCCEditing functions only
		gmap.removeOverlay(this.gMarker);
		this.set.members.splice(this.id, 1); 	// expunge the element from the set
	}
	
	this.createGMarker = function () {	
		var canMove = editing_allowed ? CCCMarkerEditing.isAllowed(this.set.name, this, MOVE) : false;
		this.getIcon();
		this.gMarker = new GMarker(this.gLatLng,
				{ icon: this.icon, title: (this.title).replace(HtmlTagRegExp,''), draggable:canMove});
		if(!editing_allowed)
			GEvent.addListener(this.gMarker, "click", this.showInfoWindow);
		else {
			GEvent.addListener(this.gMarker, "click", function() {
				CCCMarkerEditing.markerClickHandler(me);
				});
			GEvent.addListener(this.gMarker, "dragend", function() { 
				CCCMarkerEditing.dragEndHandler(me);
				});
		}
	}
	
	this.getIcon = function () {
		this.icon = CCCIconSets.markerIconSets[this.set.name][this.subType].icon;
		
		if(CCCInfoMarkerDefs[this.set.name]['_labels']) this.mergeIcon();
	}

	this.mergeIcon = function () {	
		var iconLabelId = (this._iconLabel) ? this._iconLabel : 
			(this.iconLabel) ? this.iconLabel :
			(this._number) ? this._number :
				this.number;
		var theIconSubset = CCCIconSets.markerIconSets[this.set.name][this.subType];
		if(!theIconSubset.mergedIcons) theIconSubset.mergedIcons = new Array();
		
		if(theIconSubset.mergedIcons[iconLabelId]) {	// a merged icon already exists
			this.icon = theIconSubset.mergedIcons[iconLabelId];
		} else {
			var iconLabelSet = CCCInfoMarkerDefs[this.set.name]['_labels'];
			var iconLabel = iconLabelSet[iconLabelId];
			if(!iconLabel) iconLabel = iconLabelSet['other'];
			var labelSize = new GSize(iconLabel.width, iconLabelSet.height);
			var labelAnchorXY = CCCInfoMarkerDefs[this.set.name][this.subType].labelAnchorXY;
			var labelAnchor = new GPoint(labelAnchorXY.x - iconLabel.width/2, labelAnchorXY.y);
			
			this.icon = new GIcon(this.icon, null, {url: iconLabel.URL, size: labelSize, anchor: labelAnchor});
			theIconSubset.mergedIcons[iconLabelId] = this.icon;
		}
	}

	this.show = function () {
		if(this.visible) return;
		if(this.set.name == 'Routes') { // special case for route editing markers
			if(!CCCTrackList[this.dbData.text_id] || CCCTrackList[this.dbData.text_id].hidden) {
				// the marker is associated with a route that isn't visible.
				return;
			}
			if(!this.gLatLng) {
				this.gLatLng = CCCTrackList[this.dbData.text_id].marker.position;
			}
		}

		if(this.startDate) {
			var today = new Date();
			if (!editing_allowed && (this.startDate.valueOf() > today.valueOf())) return;
		}
		if(this.expiryDate) {
			var today = new Date();
			if (!editing_allowed && (this.expiryDate.valueOf() < today.valueOf())) return;
		}
		if (!this.gMarker) 
			this.createGMarker();
		gmap.addOverlay(this.gMarker);
		this.visible = true;
	}
	
	this.hide = function () {
		if(this.visible) gmap.removeOverlay(this.gMarker);
		this.visible = false;
	}
	
	this.showInfoWindow = function () { // callback function for the click event on a marker
		// if me.infoTabs is null, compose it
		// then show the info Window
		var aListener;
		if(!me.infoTabs) {
			me.infoTabs = me.buildInfoTabs();
			me.setupInfoTabs(me.infoTabs);
		}
		me.gMarker.openInfoWindowTabsHtml(me.infoTabs, {maxWidth: 350});
		CCCCurrentMarker = me;
		aListener = GEvent.addListener(me.gMarker, "infowindowclose", function () {
			CCCCurrentMarker = null; GEvent.removeListener(aListener); });
	}
	
	this.setupInfoTabs = function (theTabs) { 
		for(i = 0; i < theTabs.length; i++) {	// Street View can be in any tab
			if(theTabs[i].name == "Street View") {
				this.SVTabNo = i;
				theTabs[i].onclick = function() {
					gmap.getInfoWindow().selectTab(me.SVTabNo);
					if(!me.streetView) {
					var theContainer = document.getElementById('StreetviewContainer');
					var SVstate = me._SV_state ? eval('(' + me._SV_state + ')') : null;
					var latLng = (SVstate && SVstate.lat) ? new GLatLng(SVstate.lat, SVstate.lng) : me.gLatLng;
					me.streetView = new GStreetviewPanorama(theContainer, {latlng: latLng, pov: SVstate});
					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>';
						});
					}
				}
			}
		}
	}
	
	this.buildInfoTabs = function () { 
		var tab0HTML = '<div id="info_form_div">',
			tab1HTML = '<div id="SV_form_div"><h3>' + this.title +'</h3><div id="StreetviewContainer"/>',
			tabArray = [];
		
			tab0HTML += '<table width=100%><tr><td><h3>' + this.title +
						'</h3></td></tr></table>';
		
		if (CCCInfoMarkerDefs[this.set.name]._displayAsTable) {
			var theForm = CCCInfoMarkerFormDefs[this.set.name];
			tab0HTML += '<table>\n';
			for (var aField in theForm.fields) {
				if(this[aField]  && (aField != 'padItem') && (aField.charAt(0) != '_')) {
					var theText = this[aField];
					var theLabel = theForm.fields[aField].shortLabel ? 
						theForm.fields[aField].shortLabel : theForm.fields[aField].label;
					tab0HTML += '<tr valign=\'top\'>\n';
					switch(aField) { 
					case 'padItem':
					case 'title':	// skip because title was dealt with outside the loop
						continue;
					case 'URL':
						theText = '<a href="' + this[aField] + '" target="_blank">' + this[aField] + '</a>';
					default:
						theLabel = '<i>' + theLabel + ': </i>';
					}
					tab0HTML += '<td>' + theLabel + '</td><td>' + theText + '</td>\n';
					tab0HTML += '</tr>\n';				
				}
			}
			tab0HTML += '</table>\n';
			if(theForm.fields.subType) {	// offer the option to suppress other subTypes
				tab0HTML += '<p title = \'Conceal other types of ' + this.set.name + '\'>' + 
					'Show only \'' + this.subType + ' ' + this.set.name + '\'? <input type="checkbox" id="showOnly" onclick = "CCCInfoMarkerSets[\'' + this.set.name + 
					'\'].showSubType(\'' + this.subType + '\', this)" value="false" ></p>'; 
			}
			if(this.set.selectiveDisplay == 'selection') {	// offer the option to show all in window
				tab0HTML += '<p title = \'Show all ' + this.set.name + 
					' on the map portion visible in this window\' >' + 
					'<input type="button" id="showAllInWin" onclick= "CCCInfoMarkerSets[\'' + this.set.name + 
					'\'].showAllInWindow()" value="Show All ' + this.set.name + ' In Window" ></p>'; 
			}
			
		} else { // currently (v2.6.2) used only for RouteInfoMarkers
			for (i = 0; i < this.extraAttributes.length; i++) {
				var anAttribute = this.extraAttributes[i];
				if(anAttribute.charAt(0) != '_' && this[anAttribute]) {
					switch(anAttribute) { 
					case 'URL':
						tab0HTML += '<a href="' + this[anAttribute] + '" target="_blank">' + 
							this[anAttribute] + '</a><br>';
						break;
					case 'expires':
						tab0HTML += '<i>Expires: ' + this[anAttribute] + '</i><br>';
						break;
					case 'padItem':
						continue;
					default:
						tab0HTML += this[anAttribute] + '<br>';
					}
				}
			}
		}
		tab0HTML += '</div>';
		tabArray.push(new GInfoWindowTab("Info", tab0HTML));
		if(CCCInfoMarkerFormDefs[this.set.name].fields['_SV_state']) 
					tabArray.push(new GInfoWindowTab("Street View", tab1HTML));
		return tabArray;
	}
	
	this.showELabel = function () { 
		if (this.visible) {
		if(!this.eLabel) 
			this.eLabel = new ELabel(this.gLatLng, this.title, "marker_label_div", 0, 80);
		gmap.addOverlay(this.eLabel);	
		}
	}
	
	this.hideELabel = function () { 
		if (this.visible) gmap.removeOverlay(this.eLabel);	
	}
		
	this.initialize (); 
} // end of CCCInfoMarker

GPoint.prototype.isWithin = function (HtmlElement) {
	var top = HtmlElement.offsetTop, left = HtmlElement.offsetLeft, 
		height = HtmlElement.offsetHeight, width = HtmlElement.offsetWidth;
	return ((this.x > left && (this.x < left + width)) && (this.y > top && (this.y < top + height)));
}


