// This file Includes:
// Line 6: elabels.js Mike Williams' Extended Labels (http://www.econym.demon.co.uk/googlemaps/elabel.htm)
// Line 150: gzoom.js modified version of code from http://earthcode.com/ 27 April 2007 
// Line 600: simpletreemenu.js from http://www.dynamicdrive.com/dynamicindex1/navigate1.htm 20 march 2007

// elabel.js
// Mike Williams' Extended Labels (http://www.econym.demon.co.uk/googlemaps/elabel.htm)
// version 1.7      fix .supportsHide()

      function ELabel(point, html, classname, pixelOffset, percentOpacity, overlap) {
        // Mandatory parameters
        this.point = point;
        this.html = html;
        
        // Optional parameters
        this.classname = classname||"";
        this.pixelOffset = pixelOffset||new GSize(0,0);
        if (percentOpacity) {
          if(percentOpacity<0){percentOpacity=0;}
          if(percentOpacity>100){percentOpacity=100;}
        }        
        this.percentOpacity = percentOpacity;
        this.overlap=overlap||false;
        this.hidden = false;
      } 
      
      ELabel.prototype = new GOverlay();

      ELabel.prototype.initialize = function(map) {
        var div = document.createElement("div");
        div.style.position = "absolute";
        div.innerHTML = '<div class="' + this.classname + '">' + this.html + '</div>' ;
        map.getPane(G_MAP_FLOAT_SHADOW_PANE).appendChild(div);
        this.map_ = map;
        this.div_ = div;
        if (this.percentOpacity) {        
          if(typeof(div.style.filter)=='string'){div.style.filter='alpha(opacity:'+this.percentOpacity+')';}
          if(typeof(div.style.KHTMLOpacity)=='string'){div.style.KHTMLOpacity=this.percentOpacity/100;}
          if(typeof(div.style.MozOpacity)=='string'){div.style.MozOpacity=this.percentOpacity/100;}
          if(typeof(div.style.opacity)=='string'){div.style.opacity=this.percentOpacity/100;}
        }
        if (this.overlap) {
          var z = GOverlay.getZIndex(this.point.lat());
          this.div_.style.zIndex = z;
        }
        if (this.hidden) {
          this.hide();
        }
      }

      ELabel.prototype.remove = function() {
        this.div_.parentNode.removeChild(this.div_);
      }

      ELabel.prototype.copy = function() {
        return new ELabel(this.point, this.html, this.classname, this.pixelOffset, this.percentOpacity, this.overlap);
      }

      ELabel.prototype.redraw = function(force) {
        var p = this.map_.fromLatLngToDivPixel(this.point);
        var h = parseInt(this.div_.clientHeight);
        this.div_.style.left = (p.x + this.pixelOffset.width) + "px";
        this.div_.style.top = (p.y +this.pixelOffset.height - h) + "px";
      }

      ELabel.prototype.show = function() {
        if (this.div_) {
          this.div_.style.display="";
          this.redraw();
        }
        this.hidden = false;
      }
      
      ELabel.prototype.hide = function() {
        if (this.div_) {
          this.div_.style.display="none";
        }
        this.hidden = true;
      }
      
      ELabel.prototype.isHidden = function() {
        return this.hidden;
      }
      
      ELabel.prototype.supportsHide = function() {
        return true;
      }

      ELabel.prototype.setContents = function(html) {
        this.html = html;
        this.div_.innerHTML = '<div class="' + this.classname + '">' + this.html + '</div>' ;
        this.redraw(true);
      }
      
      ELabel.prototype.setPoint = function(point) {
        this.point = point;
        if (this.overlap) {
          var z = GOverlay.getZIndex(this.point.lat());
          this.div_.style.zIndex = z;
        }
        this.redraw(true);
      }
      
      ELabel.prototype.setOpacity = function(percentOpacity) {
        if (percentOpacity) {
          if(percentOpacity<0){percentOpacity=0;}
          if(percentOpacity>100){percentOpacity=100;}
        }        
        this.percentOpacity = percentOpacity;
        if (this.percentOpacity) {        
          if(typeof(this.div_.style.filter)=='string'){this.div_.style.filter='alpha(opacity:'+this.percentOpacity+')';}
          if(typeof(this.div_.style.KHTMLOpacity)=='string'){this.div_.style.KHTMLOpacity=this.percentOpacity/100;}
          if(typeof(this.div_.style.MozOpacity)=='string'){this.div_.style.MozOpacity=this.percentOpacity/100;}
          if(typeof(this.div_.style.opacity)=='string'){this.div_.style.opacity=this.percentOpacity/100;}
        }
      }

      ELabel.prototype.getPoint = function() {
        return this.point;
      }
      ELabel.prototype.U = function() {
        return this.point;
      }
      ELabel.prototype.V = function() {
        return this.point;
      }
      ELabel.prototype.W = function() {
        return this.point;
      }
      ELabel.prototype.X = function() {
        return this.point;
      }
      ELabel.prototype.Y = function() {
        return this.point;
      }
      ELabel.prototype.Z = function() {
        return this.point;
      }

/* ======================================================================================== */

//gzoom.js
// modified version of gzoom.js code from http://earthcode.com/ 27 April 2007 
/*
Copyright (c) 2005-2006, Andre Lewis, andre@earthcode.com
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided 
that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    * Neither the name of "Andre Lewis" nor the names of contributors to this software may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
GZoom custom map control. Version 0.2 Released 11/21/06

To use:
  oMap = new GMap2($id("large-google-map"));	
  oMap.addControl(new GMapTypeControl());

Or with options:
  oMap.addControl(new GZoomControl({sColor:'#000',nOpacity:.3,sBorder:'1px solid yellow'}), new GControlPosition(G_ANCHOR_TOP_RIGHT,new GSize(10,10)));

More info at http://earthcode.com
*/

/* modified by George Coulouris for use at maps.camdencyclists.org.uk */

// function added by George to check the upper bound of zoom range. Called from
CCCCheckMaxZoom = function() {
	// checks whether we are at the upper limit of zoom range and if so disables the relevant zoom Control
	// NB. must invoke this if map type changes
	var G=GZoomControl.G;
	var mapType = gmap.getCurrentMapType(), zoom = gmap.getZoom();

	if(zoom >= mapType.getMaximumResolution()) { 	// disable and grey out the Zoom In control 
														// and change tooltip to explain
		G.oButton.style.color = '#808080'; // 'grey' doesn't seem to work in IE
		G.oButton.title = 'At max zoom; satellite/hybrid maps have higher resolutions.'; 
		return false;
	} else { // we're not at the limit, so re-enable if needed
		G.oButton.title = 'Zoom map to a selected rectangle';
		G.oButton.style.color = 'black';
		return true;
	}
}

// base definition and inheritance
function GZoomControl(oBoxStyle,oOptions,oCallbacks) {
	//box style options
  GZoomControl.G.style = {
    nOpacity:.2,
    sColor:"#000",
    sBorder:"2px solid blue"
  };
  var style=GZoomControl.G.style;
  for (var s in oBoxStyle) {style[s]=oBoxStyle[s]};
  var aStyle=style.sBorder.split(' ');
  style.nOutlineWidth=parseInt(aStyle[0].replace(/\D/g,''));
  style.sOutlineColor=aStyle[2];
  style.sIEAlpha='alpha(opacity='+(style.nOpacity*100)+')';
	
	// Other options
	GZoomControl.G.options={
		bForceCheckResize:false,
		sButtonHTML:'<b>Zoom In</b>',
		oButtonStartingStyle:{width:'52px',border:'1px solid black',padding:'0px 5px 1px 5px'},
		oButtonStyle:{background:'#FFF'},
		sButtonZoomingHTML:'Drag a region on the map',
		oButtonZoomingStyle:{background:'#FF0'},
		nOverlayRemoveMS:CCCZoomRectDelay,
		bStickyZoom:false
	};
	
	for (var s in oOptions) {GZoomControl.G.options[s]=oOptions[s]};
	
	// callbacks: buttonClick, dragStart,dragging, dragEnd
	if (oCallbacks == null) {oCallbacks={}};
	GZoomControl.G.callbacks=oCallbacks;
}

GZoomControl.prototype = new GControl();

//class globals
GZoomControl.G={
  bDragging:false,
  mct:null,
  mcr:null,
  mcb:null,
  mcl:null,
	oMapPos:null,
	oOutline:null,
	nMapWidth:0,
	nMapHeight:0,
	nMapRatio:0,
	nStartX:0,
	nStartY:0,
	nBorderCorrect:0
};

GZoomControl.prototype.initButton_=function(oMapContainer) {
	var G=GZoomControl.G;
//	var oButton = document.createElement('div');
	var oButton = document.createElement('zoomControl'); // refers to CCCmapstyles.css
	oButton.innerHTML=G.options.sButtonHTML;
	oButton.id='gzoom-control';
/* 	acl.style([oButton],{cursor:'pointer',zIndex:200}); */
/* 	acl.style([oButton],G.options.oButtonStartingStyle); */
/* 	acl.style([oButton],G.options.oButtonStyle); */
	oMapContainer.appendChild(oButton);
	return oButton;
};

GZoomControl.prototype.setButtonMode_=function(sMode){
	var G=GZoomControl.G;
	if (sMode=='zooming') {
		G.oButton.innerHTML=G.options.sButtonZoomingHTML;
		acl.style([G.oButton],G.options.oButtonZoomingStyle);
	} else {
		G.oButton.innerHTML=G.options.sButtonHTML;
		acl.style([G.oButton],G.options.oButtonStyle);
	}
};

// ******************************************************************************************
// Methods required by Google maps -- initialize and getDefaultPosition
// ******************************************************************************************
GZoomControl.prototype.initialize = function(oMap) {

  	var G=GZoomControl.G;
	var oMC=oMap.getContainer();
  //DOM:button
  var oButton=this.initButton_(oMC);

	//DOM:map covers
	var o = document.createElement("div");
  o.id='gzoom-map-cover';
	o.innerHTML='<div id="gzoom-outline" style="position:absolute;display:none;"></div><div id="gzoom-mct" style="position:absolute;display:none;"></div><div id="gzoom-mcl" style="position:absolute;display:none;"></div><div id="gzoom-mcr" style="position:absolute;display:none;"></div><div id="gzoom-mcb" style="position:absolute;display:none;"></div>';
	acl.style([o],{position:'absolute',display:'none',overflow:'hidden',cursor:'crosshair',zIndex:101});
	oMC.appendChild(o);

  // add event listeners
	GEvent.addDomListener(oButton, 'click', GZoomControl.prototype.startZoom_);
	GEvent.addDomListener(o, 'mousedown', GZoomControl.prototype.coverMousedown_);
	GEvent.addDomListener(document, 'mousemove', GZoomControl.prototype.drag_);
	GEvent.addDomListener(document, 'mouseup', GZoomControl.prototype.mouseup_);

//	Added by George:
	GEvent.addListener(oMap, 'maptypechanged', CCCCheckMaxZoom); // to enable/disable the zoom button if needed

  // get globals
	G.oMapPos=acl.getElementPosition(oMap.getContainer());
	G.oOutline=$id("gzoom-outline");	
	G.oButton=$id("gzoom-control");
	G.mc=$id("gzoom-map-cover");
	G.mct=$id("gzoom-mct");
	G.mcr=$id("gzoom-mcr");
	G.mcb=$id("gzoom-mcb");
	G.mcl=$id("gzoom-mcl");
	G.oMap = oMap;

	G.nBorderCorrect = G.style.nOutlineWidth*2;	
  this.setDimensions_();

  //styles
  this.initStyles_();
  	oButton.title = 'Zoom map to a selected rectangle'; 


  debug("Finished Initializing gzoom control");  
  return oButton;
};

// Default location for the control
GZoomControl.prototype.getDefaultPosition = function() {
  return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(3, 120));
};

// ******************************************************************************************
// Private methods
// ******************************************************************************************
GZoomControl.prototype.coverMousedown_ = function(e){
  var G=GZoomControl.G;
  var oPos = GZoomControl.prototype.getRelPos_(e);
  debug("Mouse down at "+oPos.left+", "+oPos.top);
  G.nStartX=oPos.left;
  G.nStartY=oPos.top;
  
	acl.style([G.mc],{background:'transparent',opacity:1,filter:'alpha(opacity=100)'});
  acl.style([G.oOutline],{left:G.nStartX+'px',top:G.nStartY+'px',display:'block',width:'1px',height:'1px'});
  G.bDragging=true;

  G.mct.style.top=(G.nStartY-G.nMapHeight)+'px';
  G.mct.style.display='block';
  G.mcl.style.left=(G.nStartX-G.nMapWidth)+'px';
  G.mcl.style.top=(G.nStartY)+'px';
  G.mcl.style.display='block';

  G.mcr.style.left=(G.nStartX)+'px';
  G.mcr.style.top=(G.nStartY)+'px';
  G.mcr.style.display='block';
  G.mcb.style.left=(G.nStartX)+'px';
  G.mcb.style.top=(G.nStartY)+'px';
  G.mcb.style.width='0px';
  G.mcb.style.display='block';

	// invoke the callback if provided
	if (G.callbacks.dragStart !=null){G.callbacks.dragStart(G.nStartX,G.nStartY)};

  debug("mouse down done");
  return false;
};

GZoomControl.prototype.drag_=function(e){
  var G=GZoomControl.G;
  if(G.bDragging) {
    var oPos=GZoomControl.prototype.getRelPos_(e);
    oRec = GZoomControl.prototype.getRectangle_(G.nStartX,G.nStartY,oPos,G.nMapRatio);
    G.oOutline.style.width=oRec.nWidth+"px";
    G.oOutline.style.height=oRec.nHeight+"px";
    
    G.mcr.style.left=(oRec.nEndX+G.nBorderCorrect)+'px';
    G.mcb.style.top=(oRec.nEndY+G.nBorderCorrect)+'px';
    G.mcb.style.width=(oRec.nWidth+G.nBorderCorrect)+'px';
		
		// invoke callback if provided
		if (G.callbacks.dragging !=null){G.callbacks.dragging(G.nStartX,G.nStartY,oRec.nEndX,oRec.nEndY)};
		
    return false;
  }  
};
GZoomControl.prototype.mouseup_=function(e){
  var G=GZoomControl.G;
  if (G.bDragging) {
    var oPos = GZoomControl.prototype.getRelPos_(e);
    G.bDragging=false;
    
    var oRec = GZoomControl.prototype.getRectangle_(G.nStartX,G.nStartY,oPos,G.nMapRatio);
    debug("mouse up at "+oRec.nEndX+", "+oRec.nEndY+". Height/width="+oRec.nWidth+","+oRec.nHeight); 

    GZoomControl.prototype.resetDragZoom_();
// Added by George 
	if(oRec.nWidth < 3 && oRec.nHeight < 3) return; //  mouse not dragged -> cancel
//	if(GZoomControl.prototype.isWithin_(G.oButton, G.nStartX, G.nStartY)) return; // mouse clicked in zoom button -> cancel
// end of addition

		var nwpx=new GPoint(oRec.nStartX,oRec.nStartY);
		var nepx=new GPoint(oRec.nEndX,oRec.nStartY);
		var sepx=new GPoint(oRec.nEndX,oRec.nEndY);
		var swpx=new GPoint(oRec.nStartX,oRec.nEndY);
		var nw = G.oMap.fromContainerPixelToLatLng(nwpx); 
    var ne = G.oMap.fromContainerPixelToLatLng(nepx); 
    var se = G.oMap.fromContainerPixelToLatLng(sepx); 
    var sw = G.oMap.fromContainerPixelToLatLng(swpx); 

    var oZoomArea = new GPolyline([nw,ne,se,sw,nw],G.style.sOutlineColor,G.style.nOutlineWidth+1,.4);

    try{
      G.oMap.addOverlay(oZoomArea);
      setTimeout (function(){G.oMap.removeOverlay(oZoomArea)},G.options.nOverlayRemoveMS);  
    }catch(e){
      jslog.error("error adding zoomarea overlay:"+e.message);
    }

    oBounds=new GLatLngBounds(sw,ne);
    nZoom=G.oMap.getBoundsZoomLevel(oBounds);
    // Added by George (to simulate the effect of a zoom even when there isn't one, so that 
    // CCCShowStands always gets its callback
    if(nZoom == G.oMap.getZoom())
    	GEvent.trigger(G.oMap, 'zoomend');
	// end of addition
    oCenter=oBounds.getCenter();
    G.oMap.setCenter(oCenter, nZoom);

		// invoke callback if provided
		if (G.callbacks.dragEnd !=null){G.callbacks.dragEnd(nw,ne,se,sw,nwpx,nepx,sepx,swpx)};
		
		//re-init if sticky
		if (G.options.bStickyZoom) {GZoomControl.prototype.initCover_()};		
  }
};

// set the cover sizes according to the size of the map
GZoomControl.prototype.setDimensions_=function() {
  var G=GZoomControl.G;
	if (G.options.bForceCheckResize){G.oMap.checkResize()};
  var oSize = G.oMap.getSize();
  G.nMapWidth  = oSize.width;
  G.nMapHeight = oSize.height;
  G.nMapRatio  = G.nMapHeight/G.nMapWidth;
	acl.style([G.mc,G.mct,G.mcr,G.mcb,G.mcl],{width:G.nMapWidth+'px', height:G.nMapHeight+'px'});
  // Added by George to fix bug where mask is too small when map size is increased
  // GEvent.addListener(G.oMap, 'resize', this.setDimensions_);
};

GZoomControl.prototype.initStyles_=function(){
  var G=GZoomControl.G;
	acl.style([G.mc,G.mct,G.mcr,G.mcb,G.mcl],{filter:G.style.sIEAlpha,opacity:G.style.nOpacity,background:G.style.sColor});
  G.oOutline.style.border=G.style.sBorder;  
  debug("done initStyles_");	
};

// Added by George:
GZoomControl.prototype.startZoom_=function(){ // control comes here from a click on the zoom In
	  if(CCCCheckMaxZoom()) GZoomControl.prototype.initCover_();
}

// The zoom button's click handler.
GZoomControl.prototype.buttonClick_=function(){
  if (GZoomControl.G.mc.style.display=='block'){ // reset if clicked before dragging
    GZoomControl.prototype.resetDragZoom_();
  } else {
		GZoomControl.prototype.initCover_();
	}
};

// George: NB: control also comes here from the CCCShowStands function
// Shows the cover over the map  
GZoomControl.prototype.initCover_=function(customLabel){
  var G=GZoomControl.G;
	var standardLabel;
	if(customLabel) {
	 	standardLabel = GZoomControl.G.options.sButtonZoomingHTML;
		GZoomControl.G.options.sButtonZoomingHTML = customLabel;
	}
	G.oMapPos=acl.getElementPosition(G.oMap.getContainer());
	GZoomControl.prototype.setDimensions_();
	GZoomControl.prototype.setButtonMode_('zooming');
	if(customLabel) GZoomControl.G.options.sButtonZoomingHTML = standardLabel;
	acl.style([G.mc],{display:'block',background:G.style.sColor});
	acl.style([G.oOutline],{width:'0px',height:'0px'});
	//invoke callback if provided
	if(GZoomControl.G.callbacks['buttonClick'] !=null){GZoomControl.G.callbacks.buttonClick()};
	debug("done initCover_");
};

GZoomControl.prototype.getRelPos_=function(e) {
  var oPos=acl.getMousePosition (e);
  var G=GZoomControl.G;
  return {top:(oPos.top-G.oMapPos.top),left:(oPos.left-G.oMapPos.left)};
};

GZoomControl.prototype.getRectangle_=function(nStartX,nStartY,oPos,nRatio){
	var dX=oPos.left-nStartX;
	var dY=oPos.top-nStartY;
	if (dX <0) dX =dX*-1;
	if (dY <0) dY =dY*-1;
	delta = dX > dY ? dX : dY;

  return {
    nStartX:nStartX,
    nStartY:nStartY,
    nEndX:nStartX+delta,
    nEndY:nStartY+parseInt(delta*nRatio),
    nWidth:delta,
    nHeight:parseInt(delta*nRatio)
  }
};

GZoomControl.prototype.resetDragZoom_=function() {
	var G=GZoomControl.G;
	acl.style([G.mc,G.mct,G.mcr,G.mcb,G.mcl],{display:'none',opacity:G.style.nOpacity,filter:G.style.sIEAlpha});
	G.oOutline.style.display='none';	
	GZoomControl.prototype.setButtonMode_('normal');
  debug("done with reset drag zoom");
};

/* alias get element by id */
function $id(sId) { return document.getElementById(sId); }
/* utility functions in acl namespace */
if (!window['acldefined']) {var acl={};window['acldefined']=true;}//only set the acl namespace once, then set a flag

/* A general-purpose function to get the absolute position of
the mouse */
acl.getMousePosition=function(e) {
	var posx = 0;
	var posy = 0;
	if (!e) var e = window.event;
	if (e.pageX || e.pageY) {
		posx = e.pageX;
		posy = e.pageY;
	} else if (e.clientX || e.clientY){
		posx = e.clientX + (document.documentElement.scrollLeft?document.documentElement.scrollLeft:document.body.scrollLeft);
		posy = e.clientY + (document.documentElement.scrollTop?document.documentElement.scrollTop:document.body.scrollTop);
	}	
	return {left:posx, top:posy};  
};

/*
To Use: 
	var pos = acl.getElementPosition(element);
	var left = pos.left;
	var top = pos.top;
*/
acl.getElementPosition=function(eElement) {
  var nLeftPos = eElement.offsetLeft;          // initialize var to store calculations
	var nTopPos = eElement.offsetTop;            // initialize var to store calculations
	var eParElement = eElement.offsetParent;     // identify first offset parent element  
	while (eParElement != null ) {                // move up through element hierarchy
		nLeftPos += eParElement.offsetLeft;      // appending left offset of each parent
		nTopPos += eParElement.offsetTop;  
		eParElement = eParElement.offsetParent;  // until no more offset parents exist
	}
	return {left:nLeftPos, top:nTopPos};
};
//elements is either a coma-delimited list of ids or an array of DOM objects. o is a hash of styles to be applied
//example: style('d1,d2',{color:'yellow'});  
acl.style=function(a,o){
	if (typeof(a)=='string') {a=acl.getManyElements(a);}
	for (var i=0;i<a.length;i++){
		for (var s in o) { a[i].style[s]=o[s];}
	}
};
acl.getManyElements=function(s){		
	t=s.split(',');
	a=[];
	for (var i=0;i<t.length;i++){a[a.length]=$id(t[i])};
	return a;
};
	
// added by George:
GZoomControl.prototype.isWithin_ = function(anElement, X, Y) {
	var G=GZoomControl.G;
	
	var left = G.oButton.offsetLeft, top = G.oButton.offsetTop;
	var right = left + G.oButton.scrollWidth, bottom = top + G.oButton.scrollHeight + 25;
	
	if(X > left && X < right && Y > top && Y < bottom) return true;
	return false;
}

var jslog = {debug:function(){},info:function(){}, 
	warning:function(){}, error:function(){},
	text:function(){}}; var debug=function(){};
if (location.href.match(/enablejslog/)){
		document.write('<script type="text/javascript" src="http://earthcode.com/includes/scripts/jslog.js"></script>');};	
		

/* ======================================================================================== */

// simpletreemenu.js
/***********************************************
* Dynamic Countdown script- © Dynamic Drive (http://www.dynamicdrive.com)
* This notice MUST stay intact for legal use
* Visit http://www.dynamicdrive.com/ for this script and 100s more.
***********************************************/
/* 
	Code from www.dynamicdrive.com
	http://www.dynamicdrive.com/dynamicindex1/navigate1.htm 
	20 march 2007
*/

MarkerIcons_path = "http://maps.camdencyclists.org.uk/markericons/";

var persisteduls=new Object()
var ddtreemenu=new Object()

ddtreemenu.closefolder= MarkerIcons_path + "closed.gif" //set image path to "closed" folder image
ddtreemenu.openfolder= MarkerIcons_path + "open.gif" //set image path to "open" folder image

//////////No need to edit beyond here///////////////////////////

ddtreemenu.createTree=function(treeid, enablepersist, persistdays){
var ultags=document.getElementById(treeid).getElementsByTagName("ul")
if (typeof persisteduls[treeid]=="undefined")
persisteduls[treeid]=(enablepersist==true && ddtreemenu.getCookie(treeid)!="")? ddtreemenu.getCookie(treeid).split(",") : ""
for (var i=0; i<ultags.length; i++)
ddtreemenu.buildSubTree(treeid, ultags[i], i)
if (enablepersist==true){ //if enable persist feature
var durationdays=(typeof persistdays=="undefined")? 1 : parseInt(persistdays)
ddtreemenu.dotask(window, function(){ddtreemenu.rememberstate(treeid, durationdays)}, "unload") //save opened UL indexes on body unload
}
}

ddtreemenu.buildSubTree=function(treeid, ulelement, index){
ulelement.parentNode.className="submenu"
if (typeof persisteduls[treeid]=="object"){ //if cookie exists (persisteduls[treeid] is an array versus "" string)
if (ddtreemenu.searcharray(persisteduls[treeid], index)){
ulelement.setAttribute("rel", "open")
ulelement.style.display="block"
ulelement.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"
}
else
ulelement.setAttribute("rel", "closed")
} //end cookie persist code
else if (ulelement.getAttribute("rel")==null || ulelement.getAttribute("rel")==false) //if no cookie and UL has NO rel attribute explicted added by user
ulelement.setAttribute("rel", "closed")
else if (ulelement.getAttribute("rel")=="open") //else if no cookie and this UL has an explicit rel value of "open"
ddtreemenu.expandSubTree(treeid, ulelement) //expand this UL plus all parent ULs (so the most inner UL is revealed!)

ulelement.parentNode.onclick=function(e){
var submenu=this.getElementsByTagName("ul")[0]
if (submenu.getAttribute("rel")=="closed"){
submenu.style.display="block"
submenu.setAttribute("rel", "open")
ulelement.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"
}
else if (submenu.getAttribute("rel")=="open"){
submenu.style.display="none"
submenu.setAttribute("rel", "closed")
ulelement.parentNode.style.backgroundImage="url("+ddtreemenu.closefolder+")"
}
ddtreemenu.preventpropagate(e)
// next statement added by George 1/7/07:
// something changed, so trigger a check on the menu length
// it would probably be better to trigger an event.
gmap.routeMenuControl.changed();
}
ulelement.onclick=function(e){
ddtreemenu.preventpropagate(e)
}
}

ddtreemenu.expandSubTree=function(treeid, ulelement){ //expand a UL element and any of its parent ULs
var rootnode=document.getElementById(treeid)
var currentnode=ulelement
currentnode.style.display="block"
currentnode.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"
while (currentnode!=rootnode){
if (currentnode.tagName=="UL"){ //if parent node is a UL, expand it too
currentnode.style.display="block"
currentnode.setAttribute("rel", "open") //indicate it's open
currentnode.parentNode.style.backgroundImage="url("+ddtreemenu.openfolder+")"
}
currentnode=currentnode.parentNode
}
}

ddtreemenu.flatten=function(treeid, action){ //expand or contract all UL elements
var ultags=document.getElementById(treeid).getElementsByTagName("ul")
for (var i=0; i<ultags.length; i++){
ultags[i].style.display=(action=="expand")? "block" : "none"
var relvalue=(action=="expand")? "open" : "closed"
ultags[i].setAttribute("rel", relvalue)
ultags[i].parentNode.style.backgroundImage=(action=="expand")? "url("+ddtreemenu.openfolder+")" : "url("+ddtreemenu.closefolder+")"
}
}

ddtreemenu.rememberstate=function(treeid, durationdays){ //store index of opened ULs relative to other ULs in Tree into cookie
var ultags=document.getElementById(treeid).getElementsByTagName("ul")
var openuls=new Array()
for (var i=0; i<ultags.length; i++){
if (ultags[i].getAttribute("rel")=="open")
openuls[openuls.length]=i //save the index of the opened UL (relative to the entire list of ULs) as an array element
}
if (openuls.length==0) //if there are no opened ULs to save/persist
openuls[0]="none open" //set array value to string to simply indicate all ULs should persist with state being closed
ddtreemenu.setCookie(treeid, openuls.join(","), durationdays) //populate cookie with value treeid=1,2,3 etc (where 1,2... are the indexes of the opened ULs)
}

////A few utility functions below//////////////////////

ddtreemenu.getCookie=function(Name){ //get cookie value
var re=new RegExp(Name+"=[^;]+", "i"); //construct RE to search for target name/value pair
if (document.cookie.match(re)) //if cookie found
return document.cookie.match(re)[0].split("=")[1] //return its value
return ""
}

ddtreemenu.setCookie=function(name, value, days){ //set cookei value
var expireDate = new Date()
//set "expstring" to either future or past date, to set or delete cookie, respectively
var expstring=expireDate.setDate(expireDate.getDate()+parseInt(days))
document.cookie = name+"="+value+"; expires="+expireDate.toGMTString()+"; path=/";
}

ddtreemenu.searcharray=function(thearray, value){ //searches an array for the entered value. If found, delete value from array
var isfound=false
for (var i=0; i<thearray.length; i++){
if (thearray[i]==value){
isfound=true
thearray.shift() //delete this element from array for efficiency sake
break
}
}
return isfound
}

ddtreemenu.preventpropagate=function(e){ //prevent action from bubbling upwards
if (typeof e!="undefined")
e.stopPropagation()
else
event.cancelBubble=true
}

ddtreemenu.dotask=function(target, functionref, tasktype){ //assign a function to execute to an event handler (ie: onunload)
var tasktype=(window.addEventListener)? tasktype : "on"+tasktype
if (target.addEventListener)
target.addEventListener(tasktype, functionref, false)
else if (target.attachEvent)
target.attachEvent(tasktype, functionref)
}

/* ======================================================================================== */

/*********************************************************************\
*                                                                     *
* epolys.js                                          by Mike Williams *
*                                                                     *
* A Google Maps API Extension                                         *
*                                                                     *
* Adds various Methods to GPolygon and GPolyline                      *
*                                                                     *
* .Contains(latlng) returns true is the poly contains the specified   *
*                   GLatLng                                           *
*                                                                     *
* .Area()           returns the approximate area of a poly that is    *
*                   not self-intersecting                             *
*                                                                     *
* .Distance()       returns the length of the poly path               *
*                                                                     *
* .Bounds()         returns a GLatLngBounds that bounds the poly      *
*                                                                     *
* .GetPointAtDistance() returns a GLatLng at the specified distance   *
*                   along the path.                                   *
*                   The distance is specified in metres               *
*                   Reurns null if the path is shorter than that      *
*                                                                     *
* .GetIndexAtDistance() returns the vertex number at the specified    *
*                   distance along the path.                          *
*                   The distance is specified in metres               *
*                   Reurns null if the path is shorter than that      *
*                                                                     *
* .Bearing(v1?,v2?) returns the bearing between two vertices          *
*                   if v1 is null, returns bearing from first to last *
*                   if v2 is null, returns bearing from v1 to next    *
*                                                                     *
***********************************************************************
*                                                                     *
* Version 1.1       6-Jun-2007                                        *
* Version 1.2       1-Jul-2007 - fix: Bounds was omitting vertex zero *
*                                add: Bearing                         *
*                                                                     *
\*********************************************************************/


// === A method for testing if a point is inside a polygon
// === Returns true if poly contains point
// === Algorithm shamelessly stolen from http://alienryderflex.com/polygon/ 
GPolygon.prototype.Contains = function(point) {
  var j=0;
  var oddNodes = false;
  var x = point.lng();
  var y = point.lat();
  for (var i=0; i < this.getVertexCount(); i++) {
    j++;
    if (j == this.getVertexCount()) {j = 0;}
    if (((this.getVertex(i).lat() < y) && (this.getVertex(j).lat() >= y))
    || ((this.getVertex(j).lat() < y) && (this.getVertex(i).lat() >= y))) {
      if ( this.getVertex(i).lng() + (y - this.getVertex(i).lat())
      /  (this.getVertex(j).lat()-this.getVertex(i).lat())
      *  (this.getVertex(j).lng() - this.getVertex(i).lng())<x ) {
        oddNodes = !oddNodes
      }
    }
  }
  return oddNodes;
}

// === A method which returns the approximate area of a non-intersecting polygon in square metres ===
// === It doesn't fully account for spechical geometry, so will be inaccurate for large polygons ===
// === The polygon must not intersect itself ===
GPolygon.prototype.Area = function() {
  var a = 0;
  var j = 0;
  var b = this.Bounds();
  var x0 = b.getSouthWest().lng();
  var y0 = b.getSouthWest().lat();
  for (var i=0; i < this.getVertexCount(); i++) {
    j++;
    if (j == this.getVertexCount()) {j = 0;}
    var x1 = this.getVertex(i).distanceFrom(new GLatLng(this.getVertex(i).lat(),x0));
    var x2 = this.getVertex(j).distanceFrom(new GLatLng(this.getVertex(j).lat(),x0));
    var y1 = this.getVertex(i).distanceFrom(new GLatLng(y0,this.getVertex(i).lng()));
    var y2 = this.getVertex(j).distanceFrom(new GLatLng(y0,this.getVertex(j).lng()));
    a += x1*y2 - x2*y1;
  }
  return Math.abs(a * 0.5);
}

// === A method which returns the length of a path in metres ===
GPolygon.prototype.Distance = function() {
  var dist = 0;
  for (var i=1; i < this.getVertexCount(); i++) {
    dist += this.getVertex(i).distanceFrom(this.getVertex(i-1));
  }
  return dist;
}

// === A method which returns the bounds as a GLatLngBounds ===
GPolygon.prototype.Bounds = function() {
  var bounds = new GLatLngBounds();
  for (var i=0; i < this.getVertexCount(); i++) {
    bounds.extend(this.getVertex(i));
  }
  return bounds;
}

// === A method which returns a GLatLng of a point a given distance along the path ===
// === Returns null if the path is shorter than the specified distance ===
GPolygon.prototype.GetPointAtDistance = function(metres) {
  // some awkward special cases
  if (metres == 0) return this.getVertex(0);
  if (metres < 0) return null;
  var dist=0;
  var olddist=0;
  for (var i=1; (i < this.getVertexCount() && dist < metres); i++) {
    olddist = dist;
    dist += this.getVertex(i).distanceFrom(this.getVertex(i-1));
  }
  if (dist < metres) {return null;}
  var p1= this.getVertex(i-2);
  var p2= this.getVertex(i-1);
  var m = (metres-olddist)/(dist-olddist);
  return new GLatLng( p1.lat() + (p2.lat()-p1.lat())*m, p1.lng() + (p2.lng()-p1.lng())*m);
}

// === A method which returns the Vertex number at a given distance along the path ===
// === Returns null if the path is shorter than the specified distance ===
GPolygon.prototype.GetIndexAtDistance = function(metres) {
  // some awkward special cases
  if (metres == 0) return this.getVertex(0);
  if (metres < 0) return null;
  var dist=0;
  var olddist=0;
  for (var i=1; (i < this.getVertexCount() && dist < metres); i++) {
    olddist = dist;
    dist += this.getVertex(i).distanceFrom(this.getVertex(i-1));
  }
  if (dist < metres) {return null;}
  return i;
}

// === A function which returns the bearing between two vertices in decgrees from 0 to 360===
// === If v1 is null, it returns the bearing between the first and last vertex ===
// === If v1 is present but v2 is null, returns the bearing from v1 to the next vertex ===
// === If either vertex is out of range, returns void ===
GPolygon.prototype.Bearing = function(v1,v2) {
  if (v1 == null) {
    v1 = 0;
    v2 = this.getVertexCount()-1;
  } else if (v2 ==  null) {
    v2 = v1+1;
  }
  if ((v1 < 0) || (v1 >= this.getVertexCount()) || (v2 < 0) || (v2 >= this.getVertexCount())) {
    return;
  }
  var from = this.getVertex(v1);
  var to = this.getVertex(v2);
  if (from.equals(to)) {
    return 0;
  }
  var lat1 = from.latRadians();
  var lon1 = from.lngRadians();
  var lat2 = to.latRadians();
  var lon2 = to.lngRadians();
  var angle = - Math.atan2( Math.sin( lon1 - lon2 ) * Math.cos( lat2 ), Math.cos( lat1 ) * Math.sin( lat2 ) - Math.sin( lat1 ) * Math.cos( lat2 ) * Math.cos( lon1 - lon2 ) );
  if ( angle < 0.0 ) angle  += Math.PI * 2.0;
  angle = angle * 180.0 / Math.PI;
  return parseFloat(angle.toFixed(1));
}

// ==== Added by George 20-7-07
// ==== Returns the bearing of GLatLng p2 relative to p1
function EBearing(p1,p2) {

  var lat1 = p1.latRadians();
  var lon1 = p1.lngRadians();
  var lat2 = p2.latRadians();
  var lon2 = p2.lngRadians();
  var angle = - Math.atan2( Math.sin( lon1 - lon2 ) * Math.cos( lat2 ), Math.cos( lat1 ) * Math.sin( lat2 ) - Math.sin( lat1 ) * Math.cos( lat2 ) * Math.cos( lon1 - lon2 ) );
  if ( angle < 0.0 ) angle  += Math.PI * 2.0;
  angle = angle * 180.0 / Math.PI;
  return parseFloat(angle.toFixed(1));
}

// === Copy all the above functions to GPolyline ===
GPolyline.prototype.Contains             = GPolygon.prototype.Contains;
GPolyline.prototype.Area                 = GPolygon.prototype.Area;
GPolyline.prototype.Distance             = GPolygon.prototype.Distance;
GPolyline.prototype.Bounds               = GPolygon.prototype.Bounds;
GPolyline.prototype.GetPointAtDistance   = GPolygon.prototype.GetPointAtDistance;
GPolyline.prototype.GetIndexAtDistance   = GPolygon.prototype.GetIndexAtDistance;
GPolyline.prototype.Bearing              = GPolygon.prototype.Bearing;

/* ======================================================================================== */

 // Bill Chadwick's DivOverlays.js
// A line Overlay done without SVG or VML 

//colour as string e.g. "#00FF00",
//width in pixels
//opacity as 0.0 to 1.0
 
function LineOverlay(p1,p2,colour,width,opacity,title) {

	this.p1_ = p1;
	this.p2_ = p2;
	this.colour_ = colour || "";
	this.width_ = width || 1;
	this.opacity_ = opacity || 1.0;
	this.cntnr_ = null;
	this.ieOpacity_ = Math.round(opacity * 100);
    this.w2_ = Math.round(width / 2);
    this.title_ = title || "";

}
LineOverlay.prototype = new GOverlay();

LineOverlay.prototype.initialize = function(map) {
  this.map_ = map;
}

LineOverlay.prototype.remove = function() {

  if(this.cntnr_ != null){
	  var div = this.map_.getPane(G_MAP_MARKER_SHADOW_PANE);
	  div.removeChild(this.cntnr_);
	  this.cntnr_ = null;
  }

}

LineOverlay.prototype.copy = function() {
  return new LineOverlay(this.p1_,this.p2_,this.colour_,this.width_,this.opacity_);
}

// Redraw the line based on the current projection and zoom level
LineOverlay.prototype.redraw = function(force) {

  //clear old
  this.remove();

  this.cntnr_ = document.createElement("DIV");   
  if(this.title_.length > 0){
    this.cntnr_.title = this.title_;
    this.cntnr_.style.cursor = "help";
  }
    
  
  var p1 = this.map_.fromLatLngToDivPixel(this.p1_);
  var p2 = this.map_.fromLatLngToDivPixel(this.p2_);

  this.buildLine(p1,p2,this.cntnr_);
  
  //pane/layer to write on
  var mapDiv = this.map_.getPane(G_MAP_MARKER_SHADOW_PANE);
  mapDiv.insertBefore(this.cntnr_,null);
  
}

//Create a div holding a set of horz and vert line segments making up the line between p1 and p2
//Uses this.sx,sy,ex,ey,steep

LineOverlay.prototype.buildLine = function(p1,p2,cntnr) {

  var x0 = p1.x;
  var x1 = p2.x;
  var y0 = p1.y;
  var y1 = p2.y;
  
  //TODO nothing if line does not intersect view

  //Bresenham algorithm for line drawing see - wikipedia

  this.steep = false;
  if(Math.abs(y1 - y0) > Math.abs(x1 - x0))
	this.steep = true;
  
  this.uColour_ = this.colour_
  if(this.uColour_.length < 1)
      this.uColour_ = this.map_.getCurrentMapType().getTextColor();
  
  if (this.steep){
		 var t = x0;
		 x0 = y0;
		 y0 = t;
		 
		 t = x1;
		 x1 = y1;
		 y1 = t;
  }
  if (x0 > x1){
		 var t = x0;
		 x0 = x1;
		 x1 = t;

		 t = y0;
		 y0 = y1;
		 y1 = t;
  }

  var deltax = x1 - x0;
  var deltay = Math.abs(y1 - y0);
  var error = 0;
  var ystep;
  var y = y0;
  
  if (y0 < y1)
	ystep = 1; 
  else 
	ystep = -1;

  //start and end coords of each horz/vertical line segment
  //We want to use the min of divs
  this.sx;
  this.ex;
  this.sy;
  this.ey;

  //initial coord of first line segment  
  if (this.steep){ 
     this.sx = y0;
     this.sy = x0;
  } 
  else{ 
     this.sy = y0;
     this.sx = x0;
  }

  var div = null;//the div whose background is a horz or vert line segment
  var last = false;//force last plot 
      
  //main Bresenham loops
  if (this.steep){ 
      for( var x = x0; x <= x1; x++){
    		 
	     if (x == x1)
		    last = true;
			
         div = this.plotLinePoint(y,x,last); 

	     if(div != null)
		    cntnr.appendChild(div);
         //else point being held over until horz/vert segment ends 
    
         error = error + deltay;
         if ((2*error) >= deltax){
             y = y + ystep;
             error = error - deltax;
         }
      }  
  }
  else{
      for( var x = x0; x <= x1; x++){
    		 
	     if (x == x1)
		    last = true;
			
         div = this.plotLinePoint(x,y,last);

	     if(div != null)
		    cntnr.appendChild(div);
         //else point being held over until horz/vert segment ends 
    
         error = error + deltay;
         if ((2*error) >= deltax){
             y = y + ystep;
             error = error - deltax;
         }        
      }    
   }
}


//Plot a point that is part of a horizontal or vertical line
//only emit a div if a horz or vert segment has been completed.
//Uses this.sx,sy,ex,ey,steep

LineOverlay.prototype.plotLinePoint = function(x,y,last) {
	     
		 this.ey = y;//end coords of line segment
		 this.ex = x;

		 if((x == this.sx)&&(!last))
			return null;//same x coord as last point
		 if((y == this.sy)&&(!last))
			return null;//same y coord as last point

		 		 
		 //swap start and end to get +ve width and height
		 var ox = this.ex;
		 if(this.ex < this.sx){
			var t = this.ex;
			this.ex = this.sx;
			this.sx = t;
		 } 		 

		 var oy = this.ey;
		 if(this.ey < this.sy){
			var t = this.ey;
			this.ey = this.sy;
			this.sy = t;
		 } 	
		 
		 //establish width/height of div
		 var w = this.ex - this.sx;
		 var h = this.ey - this.sy;
		 
		 var x = this.sx;
		 var y = this.sy;
         if(this.steep){
            w = this.width_;
            x -= this.w2_;//centre the line segment according to the line width
            }
         else{
            h = this.width_;
            y -= this.w2_;//centre the line segment according to the line width
            }

	     this.sx = ox;//restore original bresenham coords of segment start
	     this.sy = oy;
		 
         //actually make the div		 		 
		 var div = document.createElement("DIV");
	     var ds = div.style;
	     
	     ds.position = "absolute";
	     ds.overflow = "hidden";
	     ds.backgroundColor = this.uColour_;
	     ds.left = x + "px";
	     ds.top = y + "px";
	     ds.width = w + "px";
	     ds.height = h + "px";
	     	     
	     if(this.opacity_ != 1.0){
	        ds.filter = "progid:DXImageTransform.Microsoft.Alpha(opacity=" + this.ieOpacity_ +")";
	        ds.opacity = this.opacity_;
	     }
	     	     
	     return div;
}

function ArrowMarker(point, rotation, colour, width, opacity, title) {

  this.point_ = point;
  this.rotation_ = rotation;
  var r = rotation + 90;//compass to math
  this.dx = Math.round(CCCArrowLength*Math.cos(r*Math.PI/180));//length is 10 pixels, could be another param
  this.dy = Math.round(CCCArrowLength*Math.sin(r*Math.PI/180));
  this.hdx1 = Math.round(CCCArrowHead*Math.cos((r+45)*Math.PI/180));//length of head is 5 pixels, could be another param
  this.hdy1 = Math.round(CCCArrowHead*Math.sin((r+45)*Math.PI/180));
  this.hdx2 = Math.round(CCCArrowHead*Math.cos((r-45)*Math.PI/180));
  this.hdy2 = Math.round(CCCArrowHead*Math.sin((r-45)*Math.PI/180));


  //fields common with LineOverlay
  
  this.colour_ = colour || "";
  this.width_ = width || 1;
  this.opacity_ = opacity || 1.0;
  this.title_ = title || "";
  this.ieOpacity_ = Math.round(opacity * 100);
  this.w2_ = Math.round(width / 2);
  
  this.cntnr_ = null;
  this.map_ = null;

  
}
ArrowMarker.prototype = new GOverlay();

ArrowMarker.prototype.initialize = function(map) {
  this.map_ = map;
}

// Remove the main DIV from the map pane
ArrowMarker.prototype.remove = function() {
  if(this.cntnr_ != null){
	  var div = this.map_.getPane(G_MAP_MARKER_SHADOW_PANE);
	  div.removeChild(this.cntnr_);
	  this.cntnr_ = null;
  }
}

// Copy our data to a new ArrowMarker
ArrowMarker.prototype.copy = function() {
  return new ArrowMarker(this.point_, this.rotation_, this.color_, this.title_);
}


// Redraw the arrow based on the current projection and zoom level
ArrowMarker.prototype.redraw = function(force) {


  this.remove();

  var p1 = this.map_.fromLatLngToDivPixel(this.point_);
  var p2 = new GPoint(p1.x + this.dx, p1.y + this.dy);
  
  this.cntnr_ = document.createElement("DIV");   
  if(this.title_.length > 0){
    this.cntnr_.title = this.title_;
    this.cntnr_.style.cursor = "help";
  }

  this.buildLine(p1,p2,this.cntnr_);
  
  if(this.rotation_ > 0)
  {

    p2 = new GPoint(p1.x + this.hdx1, p1.y + this.hdy1);
    this.buildLine(p1,p2,this.cntnr_);

    p2 = new GPoint(p1.x + this.hdx2, p1.y + this.hdy2);
    this.buildLine(p1,p2,this.cntnr_);
  
  }
  
  //pane/layer to write on
  var mapDiv = this.map_.getPane(G_MAP_MARKER_SHADOW_PANE);
  mapDiv.insertBefore(this.cntnr_,null);

}

ArrowMarker.prototype.buildLine = function(p1,p2,cntnr) {
    LineOverlay.prototype.buildLine.call(this,p1,p2,cntnr); 
}

ArrowMarker.prototype.plotLinePoint = function(x,y,last) {
    return LineOverlay.prototype.plotLinePoint.call(this,x,y,last);
}

/* ======================================================================================== */

/*
    http://www.JSON.org/json2.js
    2009-06-18

    See http://www.JSON.org/js.html

    This file creates a global JSON object containing two methods: stringify
    and parse.

        JSON.stringify(value, replacer, space)
            value       any JavaScript value, usually an object or array.

            replacer    an optional parameter that determines how object
                        values are stringified for objects. It can be a
                        function or an array of strings.

            space       an optional parameter that specifies the indentation
                        of nested structures. If it is omitted, the text will
                        be packed without extra whitespace. If it is a number,
                        it will specify the number of spaces to indent at each
                        level. If it is a string (such as '\t' or '&nbsp;'),
                        it contains the characters used to indent at each level.
        JSON.parse(text, reviver)
            This method parses a JSON text to produce an object or array.
            It can throw a SyntaxError exception.

            The optional reviver parameter is a function that can filter and
            transform the results. It receives each of the keys and values,
            and its return value is used instead of the original value.
            If it returns what it received, then the structure is not modified.
            If it returns undefined then the member is deleted.
*/

var JSON=JSON||{};(function(){function f(n){return n<10?'0'+n:n;}
if(typeof Date.prototype.toJSON!=='function'){Date.prototype.toJSON=function(key){return this.valueOf()?this.getUTCFullYear()+'-'+
f(this.getUTCMonth()+1)+'-'+
f(this.getUTCDate())+'T'+
f(this.getUTCHours())+':'+
f(this.getUTCMinutes())+':'+
f(this.getUTCSeconds())+'Z':null;};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf();};}
var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==='string'?c:'\\u'+('0000'+a.charCodeAt(0).toString(16)).slice(-4);})+'"':'"'+string+'"';}
function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==='object'&&typeof value.toJSON==='function'){value=value.toJSON(key);}
if(typeof rep==='function'){value=rep.call(holder,key,value);}
switch(typeof value){case'string':return quote(value);case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null';}
gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==='[object Array]'){length=value.length;for(i=0;i<length;i+=1){partial[i]=str(i,value)||'null';}
v=partial.length===0?'[]':gap?'[\n'+gap+
partial.join(',\n'+gap)+'\n'+
mind+']':'['+partial.join(',')+']';gap=mind;return v;}
if(rep&&typeof rep==='object'){length=rep.length;for(i=0;i<length;i+=1){k=rep[i];if(typeof k==='string'){v=str(k,value);if(v){partial.push(quote(k)+(gap?': ':':')+v);}}}}else{for(k in value){if(Object.hasOwnProperty.call(value,k)){v=str(k,value);if(v){partial.push(quote(k)+(gap?': ':':')+v);}}}}
v=partial.length===0?'{}':gap?'{\n'+gap+partial.join(',\n'+gap)+'\n'+
mind+'}':'{'+partial.join(',')+'}';gap=mind;return v;}}
if(typeof JSON.stringify!=='function'){JSON.stringify=function(value,replacer,space){var i;gap='';indent='';if(typeof space==='number'){for(i=0;i<space;i+=1){indent+=' ';}}else if(typeof space==='string'){indent=space;}
rep=replacer;if(replacer&&typeof replacer!=='function'&&(typeof replacer!=='object'||typeof replacer.length!=='number')){throw new Error('JSON.stringify');}
return str('',{'':value});};}
if(typeof JSON.parse!=='function'){JSON.parse=function(text,reviver){var j;function walk(holder,key){var k,v,value=holder[key];if(value&&typeof value==='object'){for(k in value){if(Object.hasOwnProperty.call(value,k)){v=walk(value,k);if(v!==undefined){value[k]=v;}else{delete value[k];}}}}
return reviver.call(holder,key,value);}
cx.lastIndex=0;if(cx.test(text)){text=text.replace(cx,function(a){return'\\u'+
('0000'+a.charCodeAt(0).toString(16)).slice(-4);});}
if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,'@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']').replace(/(?:^|:|,)(?:\s*\[)+/g,''))){j=eval('('+text+')');return typeof reviver==='function'?walk({'':j},''):j;}
throw new SyntaxError('JSON.parse');};}}());
/* ======================================================================================== */

/*
 * calculate destination point given start point, initial bearing (deg) and distance (km)
 *   see http://williams.best.vwh.net/avform.htm#LL
 * modifed to extend Google Maps GLatLng class by George Coulouris
 */
GLatLng.prototype.destPoint = function(brng, d) {
  var R = 6371; // earth's mean radius in km
  var lat1 = this.lat().toRad(), lon1 = this.lng().toRad();
  brng = brng.toRad();

  var lat2 = Math.asin( Math.sin(lat1)*Math.cos(d/R) + 
                        Math.cos(lat1)*Math.sin(d/R)*Math.cos(brng) );
  var lon2 = lon1 + Math.atan2(Math.sin(brng)*Math.sin(d/R)*Math.cos(lat1), 
                               Math.cos(d/R)-Math.sin(lat1)*Math.sin(lat2));
  lon2 = (lon2+Math.PI)%(2*Math.PI) - Math.PI;  // normalise to -180...+180

  if (isNaN(lat2) || isNaN(lon2)) return null;
  return new GLatLng(lat2.toDeg(), lon2.toDeg());
}

Number.prototype.toRad = function() {  // convert degrees to radians
  return this * Math.PI / 180;
}

Number.prototype.toDeg = function() {  // convert radians to degrees (signed)
  return this * 180 / Math.PI;
}

Number.prototype.toBrng = function() {  // convert radians to degrees (as bearing: 0...360)
  return (this.toDeg()+360) % 360;
}
