// ----------------------------------------------------------------------------
// Copyright - J. C. Parker 2011-2012
// ----------------------------------------------------------------------------

"use strict";

if (typeof google.maps === undefined) alert('The Google Maps API is off-line. Please try again later.');

var map = null;

var layerCtrlNSW  = null;
var layerCtrlSA   = null;
var layerCtrlVic  = null;
var layerCtrlMisc = null;

var NEARMAP = 'nearmap';
var OSMBIKE = 'osmbike';
var OSM     = 'osm';

var baseLayersInfo = [
   [google.maps.MapTypeId.ROADMAP,   'm' ],
   [google.maps.MapTypeId.SATELLITE, 'k' ],
   [google.maps.MapTypeId.HYBRID,    'h' ],
   [google.maps.MapTypeId.TERRAIN,   'p' ],
   [                      NEARMAP,   'nr'],
   [                      OSMBIKE,   'cy'],
   [                      OSM,       'os']
];

// Australia wide overlays
var pLga                 = null;
var pOsmOnRoadBikeRoute  = null;
var pOsmOffRoadBikeRoute = null;

// Sydney specific ovelays
var pSydneyCampaigns     = null;
var pSydneyCounters      = null;
var pSydneyRtaCctv       = null;

// Melbourne specific ovelays
var pMelbourneNavigation = null;
var pMelbourneBikeShare  = null;
var pMelbourneCampaigns  = null;
var pMelbourneMagpies    = null;

// Adelaide specific ovelays
var pAdelaideCampaigns   = null;
var pAdelaideBakeries    = null;

// Miscellaneous ovelays
var pLayerWeatherRadar   = null;
var pTimeByBikeMarker    = null;

// Miscellaneous fusion tables
var layerFt0Id = -1;
var layerFt1Id = -1;

// MELBOURNE
// Melbourne's GPO is located at Australian Map Grid (AMG) 320 725 E, 5812 900 N, Zone 55H
var MELB_CENTER_LAT = -37.8135784679527;
var MELB_CENTER_LNG = 144.963341474327;

// holds the last state that the map was centered on
var lastState = 'Unknown';

var cookieName    = 'Bike Trails';
var cookieVersion = 1;

// the default permalink variables
// set the default to Melbourne's GPO
var mapCenterLat = MELB_CENTER_LAT;
var mapCenterLng = MELB_CENTER_LNG;

var mapZoom      = 8;  // higher is more closer in
var mapTypeText  = "nr";

// time by bike parms
var bikeTimeLat = mapCenterLat;
var bikeTimeLng = mapCenterLng;
var speed       = 34.0;
var speeds      = [10,15,18,20,22,24,26,28,30,32,34];
var speedIdx    = 2;     // 18 kph
var ringsLocked = false;

var showAboutMsg = true;
var showLayerSw  = true;

var timeByBikeMarker = null;
var infoWindow       = null;
var accessPanesDummy = null;

var me = null;

// routing
var markerStartPoint  = null;
var markerFinishPoint = null;
var polylineRoute     = null;

// ----------------------------------------------------------------------------

var places = [

'U',                                      // -- Select a Place --
'-34.927013,138.599520,12',               // Adelaide - GPO at 2 Franklin St
'-36.091311,146.906473,12',               // Albury-Wodonga - bridge over Murray River
'-37.561562,143.858216,12',               // Ballarat
'-36.758123,144.281561,12',               // Bendigo - old GPO at Sturt & Lydiard St
'-27.468174,153.028173,12',               // Brisbane - GPO between Queen St & Elizabeth St
'-16.922104,145.775784,12',               // Cairns Mall
'-35.278104,149.128058,12',               // Canberrra - GPO 53-73 Alinga Street
'-12.460969,130.841802,12',               // Darwin - GPO 48 Cavenagh St
'-38.147655,144.362116,12',               // Geelong
'-28.001970,153.428626,12',               // Gold Coast - the old pub
'-33.429724,151.341425,12',               // Gosford - old GPO at 27 Mann St
'-42.882594,147.330243,12',               // Hobart - GPO at 9 Elizabeth St
'-41.435534,147.137683,12',               // Launceston - GPO at Cameron St & St John St
'-37.8135784679527,144.963341474327,12',  // Melbourne - old GPO at Elzabeth St & Bourke St
'-32.927469,151.783447,12',               // Newcastle - old GPO at Hunter St & Bolton St
'-31.952265,115.859198,12',               // Perth - old GPO at St George's Terrace & Barrack St
'-26.599309,153.051897,12',               // Sunshine Coast - some arbitary spot
'-33.867716,151.207699,12',               // Sydney - GPO at No. 1 Martin Place
'-27.561586,151.956181,12',               // Toowoomba - old GPO at 136 Margaret St
'-19.258434,146.818357,12',               // Townsville - old GPO at 252-270 Flinders St
'-34.424621,150.900976,12'                // Wollongong - old GPO at 11 Market St
];

// ----------------------------------------------------------------------------

var trailDataNT = [
'U,-,-,-- Select a trail in the NT --',
'-12.463710,130.838112,17,Esplanade Trail',
'-12.415030,130.830759,15,East Point Trail'
];


var trailDataACT = [
'U,-,-,-- Select a trail in the ACT --',
'-35.206449,149.089139,15,Ginninderra Creek Trail',
'-35.197661,149.149060,15,Gungaderra Creek Trail',
'-35.199072,149.131376,14,Gungahlin Trail',
'-35.293430,149.120923,14,Lake Burley Griffin Trail',
'-35.266675,149.124197,14,Sullivans Creek Trail'
];

var trailDataNSW = [
'U,-,-,-- Select a trail in NSW --',
'-33.927143,151.169436,15,Alexandra Canal Trail',
'-33.902152,151.224039,15,Anzac Pde Trail',
'-33.865903,151.146889,16,Bay Run Trail',
'-33.988802,151.147909,13,Botany Bay Trail',
'-33.761871,151.105207,17,Browns Waterhole Trail',
'-33.790131,151.256267,15,Burnt Bridge Creek Trail',
'-33.911242,151.115541,14,Cook River Trail',
'-33.768765,151.005414,17,Darling Mills Creek Trail',
'-33.858397,151.015374,15,Duck River Trail',
'-33.850618,151.147362,17,Five Dock Bay Trail',
'-33.879424,151.175810,17,Johnstons Creek Trail',
'-33.812424,151.173125,14,M2 Trail',
'-33.811815,150.945088,13,M4 Trail',
'-33.944128,151.071136,14,M5 Trail',
'-33.838425,150.856668,12,M7 Trail',
'-33.846767,151.080175,15,Olympic Park Trails',
'-33.878016,150.931396,14,Orphans School Creek Trail',
'-33.817126,151.041565,14,Paramatta River Trail',
'-33.832397,150.945853,14,Prospect Canal Trail',
'-33.836996,151.138352,17,Tarban Creek Trail',
'-33.877472,151.168580,17,Whites Creek Trail',
'-33.687172,150.919151,12,Windsor Road Trail',
'U,-,-,-------------------',
'-32.987987,151.708334,14,Fernleigh Trail'
];

var trailDataQld = [
'U,-,-,-- Select a trail in Queensland --',
'-27.476282,153.002348,15,Bicentennial Trail',
'-27.461285,152.619818,14,Brisbane Valley Rail Trail',
'-27.574000,152.944058,13,Centenary Trail',
'-27.196474,153.037072,14,Deception Bay Trail',
'-27.379016,153.037812,14,Downfall Creek Trail',
'-27.436760,152.998862,15,Enoggera Creek Trail',
'-27.369463,153.100627,14,Jim Soorley Trail',
'-27.412156,152.988945,14,Kedron Brook Trail',
'-27.492707,153.047990,15,Norman Creek Trail',
'-27.529337,153.056657,14,Pacific SE Fwy Trail',
'-27.466584,153.039826,16,River Walk Trail (closed due to flood)'
];

var trailDataSA = [
'U,-,-,-- Select a trail in SA --',
'-35.050557,138.548325,14,Adelaide Southern Veloway Trail',
'-34.966850,138.867475,16,Amy Gillett Trail',
'-34.850982,138.476786,15,Coast Park Trail',
'-34.818518,138.694303,15,Dry Creek Trail',
'-34.974697,138.675989,15,Eagle on the Hill Trail',
'-34.920719,138.583076,15,Livestrong Trail',
'-35.000763,138.602485,16,Lynton Trail',
'-34.958409,138.572292,14,Mike Turtur Trail (Tram Park)',
'-34.853942,138.677790,15,O-Bahn Trail',
'-34.900527,138.615018,13,River Torrens Trail',
'-34.656030,138.659729,13,Stuart O\'Grady Trail',
'-35.000023,138.546073,14,Sturt River Trail',
'U,-,-,-------------------',
'-34.503026,139.022460,14,Barossa Rail Trail',
'-35.185848,138.484344,12,Coast to Vines Trail',
'-34.089409,138.699089,12,Rattler Trail',
'-33.927166,138.639392,12,Riesling Trail'
];

var trailDataTas = [
'U,-,-,-- Select a trail in Tasmania --',
'-42.891683,147.310813,14,Hobart Rivulet Trail',
'-42.837735,147.289525,13,Intercity Trail'
];

var trailDataVic = [
'-,-,-,-- Select a trail in Victoria --',
'-37.832715,145.071588,13,Anniversary Trail',
'-37.970139,145.011789,13,Bay Trail (east)',
'-37.872225,144.903244,13,Bay Trail (west)',
'-37.910415,145.352471,13,Belgrave Rail Trail',
'-37.868041,145.248813,13,Blind Creek Trail',
'-37.635253,144.923308,13,Broadmeadows Valley Trail',
'-37.807538,145.131675,13,Bushy Creek Trail',
'-37.798311,144.936368,13,Capital City Trail',
'-37.956692,145.233783,13,Dandenong Creek Trail',
'-38.001919,145.200545,13,Dandenong South Trail',
'-37.710844,145.034580,13,Darebin Creek Trail - lower',
'-37.665273,145.036412,13,Darebin Creek Trail - upper',
'-37.793211,144.807904,13,Derrimut Trail',
'-37.679732,145.150621,13,Diamond Creek Trail',
'-37.981885,145.194236,12,East Link Trail',
'-37.675207,145.007629,13,Edgars Creek Trail',
'-37.835729,144.788326,13,Federation Trail',
'-37.856216,145.073365,13,Ferndale Park Trail',
'-37.895651,145.269548,13,Ferny Creek Trail',
'-37.584220,144.940991,13,Galada Tamboore Trail (Craigieburn Bypass)',
'-37.849402,145.049064,13,Gardiners Creek Trail',
'-37.808670,145.101521,13,Gawler Chain Trail',
'-37.754993,145.163655,13,Greengully Trail',
'-37.694840,145.093110,13,Greensborough Bypass Trail',
'-38.025707,145.318984,13,Hallam Bypass Trail',
'-38.052175,145.319813,13,Hallam Main Drain Trail',
'-37.794177,145.060970,15,Hays Paddock Trail',
'-37.878443,144.675915,14,Heathdale Glen Orden Trail',
'-37.641263,145.069181,13,Hendersons Rd Drain Trail',
'-37.885911,145.107715,15,Holmesglen Trail',
'-37.796974,145.141340,13,Koonung Creek Trail',
'-37.856274,144.837549,13,Kororoit Creek Trail - lower',
'-37.789345,144.826536,13,Kororoit Creek Trail - upper',
'-37.862868,144.781970,13,Laverton Creek Trail',
'-37.891138,144.620410,13,Lollypop Creek Trail',
'-37.774906,144.860942,13,Maribyrnong River Trail',
'-37.871151,145.085321,13,Markham Reserve Trail',
'-37.692872,145.168295,14,Maroondah Aqueduct Trail',
'-37.712621,144.978831,13,Merri Creek Trail',
'-37.717442,144.905936,13,Moonee Ponds Creek Trail',
'-37.796610,145.389509,14,Mt Evelyn Aqueduct Trail',
'-37.774055,145.189256,13,Mullum Mullum Creek Trail - lower',
'-37.785538,145.260890,13,Mullum Mullum Creek Trail - upper',
'-37.775515,145.359296,14,Olinda Creek trail',
'-37.716199,145.113325,13,Plenty River Trail',
'-37.738102,145.080214,13,River Gum Walk Trail',
'-37.762532,145.127176,13,Ruffey Creek Trail',
'-37.833171,144.943112,13,Sandridge Trail',
'-37.890826,145.103716,13,Scotchmans Creek Trail',
'-37.874348,144.744424,13,Skeleton Creek Trail',
'-37.737210,145.000238,13,St Georges Rd Trail',
'-37.926571,145.122745,13,Station Trail',
'-37.743348,144.882481,13,Steele Creek Trail',
'-37.830595,145.019781,15,Survey Paddock Trail',
'-37.861987,145.154742,13,Syndal Heatherdale Pipe Reserve Trail',
'-37.806323,145.279868,13,Tarralla Creek Trail',
'-37.704083,144.795016,13,Taylors Creek Trail',
'-37.675834,144.593079,13,Toolern Creek Trail',
'-37.752132,144.962217,13,Upfield Rail Trail',
'-37.875500,145.122736,13,Waverley Rail Trail',
'-37.767754,144.744595,13,Wellness Trail',
'-37.910426,144.652706,13,Werribee River Trail',
'-37.965537,145.135870,13,Westall Rd Trail',
'-37.700772,144.734695,13,Western Plains Heritage Trail',
'-38.316037,145.186908,12,Western Port Bay Trail',
'-37.693029,144.941914,12,Western Ring Rd Trail',
'-37.843564,145.155239,13,Wurundjeri Walk Trail',
'-37.768150,145.071370,13,Yarra River Trail',
'U,-,-,-------------------',
'-38.164267,144.350092,14,Barwon River Trail',
'-38.143312,144.361901,14,Corio Bay Trail',
'-38.094393,144.344904,14,Cowies Creek Trail',
'-38.060372,144.412514,14,Hovell Creek Trail',
'-38.089037,144.327281,13,Ted Wilson Trail',
'-38.124340,144.330753,14,Tom McKean Trail',
'-38.197840,144.324484,14,Waurn Ponds Trail',
'U,-,-,-------------------',
'-37.678118,143.653741,11,Ballarat-Skipton Rail Trail',
'-38.528231,145.449109,12,Bass Coast Rail Trail',
'-38.191007,144.578136,11,Bellarine Peninsula Rail Trail',
'-38.409781,142.979888,11,Coast to Crater Rail Trail',
'-37.708968,147.835187,11,East Gippsland Rail Trail',
'-37.021314,145.980898,12,Goulburn River High Country Rail Trail',
'-38.600535,146.047489,11,Great Southern Rail Trail',
'-36.173218,147.035217,11,High Country Rail Trail',
'-36.123864,146.875489,15,House Creek Trail',
'-37.798178,147.925789,11,Mississippi Creek Trail',
'-36.647017,146.866421,11,Murray to the Mountains Rail Trail',
'-36.380750,146.681744,11,Murray to the Mountains - Beechworth spur',
'-36.781526,144.389668,12,O\'Keefe Rail Trail',
'-38.491296,143.568465,11,Old Beechy Rail Trail',
'-38.298669,142.327504,12,Port Fairy to Warrnambool Rail Trail',
'-37.769556,145.632565,11,Warburton Rail Trail',
'-38.308626,145.196751,11,Western Port Bay Trail'
];

var trailDataWA = [
'U,-,-,-- Select a trail in WA --',
'-31.919724,115.804795,15,Herdsman Lake Trail',
'-31.965259,115.800472,18,Karrakatta Trail',
'-31.890233,115.755192,12,Sunset Coast Trail',
'-31.999179,115.817552,13,Three Bridges River Trail',
'U,-,-,-------------------',
'-17.954353,122.229090,15,Cable Beach Trail',
'-32.293601,115.835047,11,Kwinana Trail'
];

// ----------------------------------------------------------------------------

var trailData = trailDataVic;

// ----------------------------------------------------------------------------

// a default radar located in the "Great Australian Bight"
var radar = {'lat':-38.0,'lng':129.0,'state':'xyz','name':'Default radar','ranges':{'range512':['urlB'],'range256':['urlB'],'range128':['urlC'],'range64':['urlD']}};

// default to 512 km composites, which are available for all sites
var radarRange     = 'range512';
var lastRadarRange = '';

var gettingRadarInfo = false;

// ----------------------------------------------------------------------------
// Display an object's properties and functions for debugging purposes
// Note in IE you may get this error: "Object doesn't support this action"
// It appears some objs such as httpRequest.responseXML can't be enumerated
// by a "for in loop". Google propertyIsEnumerable() for more details.
// ----------------------------------------------------------------------------

function examineObject(theObject)
{
   if (theObject === undefined)
   {
      alert("The variable is 'undefined' as opposed to 'null'");
      return;
   }

   if (theObject === null)
      alert("The object is 'null' as opposed to 'undefined'. 'null' is a valid empty object");

   if (theObject instanceof Event)
   {
      alert("The variable is an 'Event' and can't be enumerated");
      return;
   }

   // loop through the object and show all the properties
   var tmp = [];
   var propertyName;
   for (propertyName in theObject)
   {
      if (!(theObject[propertyName] instanceof Function))
         tmp.push(propertyName+' = '+theObject[propertyName]);
   }

   alert('Properties:\n\n' + tmp.join('\n'));

   // loop through the object and show all the functions
   tmp = [];
   var functionName;
   for (functionName in theObject)
   {
      if (theObject[functionName] instanceof Function)
         tmp.push(functionName+'()');
   }

   alert('Functions:\n\n' + tmp.join('\n'));
}

// ----------------------------------------------------------------------------
// Display 500 page error in a new window
// http://www.onlamp.com/pub/a/onlamp/2005/05/19/xmlhttprequest.html
// ----------------------------------------------------------------------------

function handleErrFullPage(strIn)
{
   var errorWin;

   // create new window and display error
   try
   {
      errorWin = window.open('', 'errorWin');
      errorWin.document.body.innerHTML = strIn;
   }
   // if pop-up gets blocked, inform user
   catch(e)
   {
      alert('An error occurred, but the error message cannot be' +
            ' displayed because of your browser\'s pop-up blocker.\n' +
            'Please allow pop-ups from this Web site.');
   }
}

// ----------------------------------------------------------------------------
// Handle the response
// Custom messages can be returned - just make sure they contain 'Debug:' or 'Error:'
// http://www.onlamp.com/pub/a/onlamp/2005/05/19/xmlhttprequest.html
//
// In IE7 responseXML is only available if the server sends back an XML document
// with MIME type text/xml, and not if it sends back, for instance, a KML document.
// ----------------------------------------------------------------------------

function handleServerReponse(httpRequest, url, listener)
{
   switch (httpRequest.readyState)
   {
   case 2:
   case 3:
      // display a progress indicator of some kind
      break;
   case 4:
      var textFromServer = httpRequest.responseText;
      switch (httpRequest.status)
      {
         case 0: // Possible cross domain problem
            //alert('AJAX error 0: Maybe cross domain url or HTTP/S conflict:\n' + url);
            break;
         case 200: // the request has succeeded
            // got a custom error msg?
            if (textFromServer.indexOf('Error:') > -1 || textFromServer.indexOf('Debug:') > -1)
               alert(textFromServer);
            else // return the raw XML text and the XML DOM object result
            {
               if (window.DOMParser)
               {
                  // not an IE browser
                  listener(textFromServer, httpRequest.responseXML);
               }
               else  // IE wants a mime indicating XML - a KML mime is not sufficient
               {
                  // manually parse the text into xml
                  xmlDoc = new ActiveXObject('Microsoft.XMLDOM');
                  xmlDoc.async = false;
                  xmlDoc.loadXML(textFromServer);

                  listener(textFromServer, xmlDoc);
               }
            }
            break;
         case 404: // page not found error
            alert('AJAX error 404: Page not Found. The requested URL ' + url + ' could not be found.');
            break;
         case 500: // display results in a full window for server-side errors
            handleErrFullPage(textFromServer);
            break;
         default:
            alert('AJAX error: Status is ' + httpRequest.status);
         break;
      }
      break;
   default:
      break;
   }
}

// ----------------------------------------------------------------------------
// Get a http request object
// https://developer.mozilla.org/en/AJAX/Getting_Started
// ----------------------------------------------------------------------------

function getXMLObject(handler, url, listener)
{
   // keep it all local so we can make multiple
   // requests without confusion between them
   var httpRequest = false;

   if (window.XMLHttpRequest)
   {  // IE7, Mozilla, Safari, ...
      httpRequest = new XMLHttpRequest();
   }
   else if (window.ActiveXObject)
   {  // IE
      try { httpRequest = new ActiveXObject("Msxml2.XMLHTTP"); }
      catch (e1)
      {
         try { httpRequest = new ActiveXObject("Microsoft.XMLHTTP"); }
         catch (e2)
         {}
      }
   }

   if (!httpRequest)
   {
      alert('Cannot create XMLHTTP instance');
      return false;
   }

   // the server's response will be screened by the handler and then passed on to the result function
   httpRequest.onreadystatechange = function() { handler(httpRequest, url, listener); };

   return httpRequest;
}

// ----------------------------------------------------------------------------
// do an asynchronous or synchronous GET or POST
// PLEASE encodeURI parameters before passing in the URL if needed
// ----------------------------------------------------------------------------

function ajaxRequest(method, url, async, listener)
{
   // get a reference to the function that will initially screen the server's response
   var handler = handleServerReponse;

   // get the url and the parameters
   var urlParms = url.split("?");

   // set up the object to suit the browser being used
   // once the server's response has been checked, it's passed on to the listener
   var xmlhttp = new getXMLObject(handler, urlParms[0], listener);

   if (xmlhttp) // then execute method
   {
      // confuse cache
      var date    = new Date();
      var theTime = date.getTime();

      if (method === 'POST')
      {
         // get the parms ready to post
         var postThis = '';
         if (urlParms.length > 1) postThis = urlParms[1];

         // defeat caching
         url = urlParms[0] + "?" + theTime;

         xmlhttp.open('POST',url,async);
         xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
         xmlhttp.send(postThis);
      }
      else if (method === 'GET')
      {
         // defeat caching
         url += (url.match(/\?/) === null ? "?" : "&") + theTime;

         xmlhttp.open('GET',url,async);
         xmlhttp.send(null);
      }
   }

   return xmlhttp;
}

// ----------------------------------------------------------------------------
// JSON parser based on code from here:
// https://github.com/douglascrockford/JSON-js/blob/master/json2.js
// ----------------------------------------------------------------------------

function parseJson(jsonStr)
{
   //alert(jsonStr);
   var jsonObject = null;

   // got a native parser? If so use it
   if (window.JSON && (typeof JSON.parse === "function"))
   {
      try
      {
         //alert('Using native parser');
         jsonObject = JSON.parse(jsonStr);
      }
      catch(e1)
      {
         jsonObject = null;
         //alert('Error name = '+e1.name+', error message = '+e1.message+', jsonStr = '+jsonStr);
      }
   }
   else try // and do it the hard way
   {
      // run the text against a regular expression which looks for non-JSON
      // characters. We are especially concerned with '()' and 'new' because
      // they can cause invocation, and '=' because it can cause mutation.
      // But just to be safe, we will reject all unexpected characters.

      if (/^[\],:{}\s]*$/
         .test(jsonStr.replace(/\\["\\\/bfnrtu]/g, '@')
         .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
         .replace(/(?:^|:|,)(?:\s*\[)+/g, '')))
	   {
            jsonObject = eval('(' + jsonStr + ')');
         }
   } catch(e2)
   {
      jsonObject = null;
      //alert('Error name = '+e2.name+', error message = '+e2.message+', jsonStr = '+jsonStr);
   }

   return jsonObject;
}

// ----------------------------------------------------------------------------
// This is a dummy overlay - it is used purely to gain access to the getPanes() method in OverlayView
// Doing so then allows the panes to be manipulated. The dummy must be attached to the map and
// the corresponding dom built before the getPanes() method returns valid data.
// ----------------------------------------------------------------------------

   function dummyOverlayView(map)
   {
      this.setMap(map);
   }

   dummyOverlayView.prototype = new google.maps.OverlayView();

   // at a minimum, these methods should exist
   dummyOverlayView.prototype.onAdd    = function() {};   // called by setMap(layer)
   dummyOverlayView.prototype.draw     = function() {};   // called by initial draw, move map, zoom, etc
   dummyOverlayView.prototype.onRemove = function() {};   // called by setMap(null)

// ----------------------------------------------------------------------------
// This is layer animates an array of images at a given location
// You can set up the animation speed and also call a function to
// update the array of images also on a timed basis.
// ----------------------------------------------------------------------------

   function animatedImages(
      nameIn,
      animationIntervalIn,
      imgsUpdaterFunctionIn,
      imgsUpdaterIntervalIn)
   {
      // 'me' needs to be global, so that setInterval functionality can be accessed later
      me = this;

      // the map variable must be defined to keep our layer switcher
      // happy but we don't want it set to the map at start up
      this.setMap(null);

      this.name = nameIn;

      this.animationTimer    = null;
      this.animationInterval = animationIntervalIn * 1000.0;

      this.imgsUpdaterTimer    = null;
      this.imgsUpdaterInterval = imgsUpdaterIntervalIn * 1000.0;
      this.imgsUpdaterFunction = imgsUpdaterFunctionIn;

      // this is a dummy and sits in the Great Australian Bight
      this.bounds = new google.maps.LatLngBounds(
         new google.maps.LatLng(-40.1,130.0),
         new google.maps.LatLng(-40.0,130.1));

      this.imageUrls = [];
      this.imageUrl  = null;
      this.count     = 0;

      // We define a property to hold the image's div. We'll
      // actually create this div upon receipt of the onAdd()
      // method so we'll leave it null for now.
      this.imgDiv = null;
   }

   animatedImages.prototype = new google.maps.OverlayView();

   // this is called by 'setMap(map)'
   animatedImages.prototype.onAdd = function()
   {
      // Note: an overlay's receipt of onAdd() indicates that
      // the map's panes are now available for attaching
      // the overlay to the map via the DOM.

      // create the DIV and set some basic attributes
      var div = document.createElement('DIV');
      div.style.borderStyle = "none";
      div.style.borderWidth = "0px";
      div.style.position    = "absolute";

      // create an IMG element and attach it to the DIV
      var img = document.createElement("img");
      img.src          = this.imageUrl;
      img.style.width  = "100%";
      img.style.height = "100%";
      div.appendChild(img);

      // set the overlay's imgDiv property to this DIV
      this.imgDiv = div;

      // we add an overlay to a map via one of the map's panes
      // we'll add this overlay to the floatPane pane
      var panes = this.getPanes();
      panes.floatPane.appendChild(div);

      // setInterval can be very problematic - be careful
      this.animationTimer   = window.setInterval(function(){me.updateAnimation();},     this.animationInterval);
      this.imgsUpdaterTimer = window.setInterval(function(){me.imgsUpdaterFunction();}, this.imgsUpdaterInterval);
   };

   animatedImages.prototype.draw = function()
   {
      // Size and position the overlay. We use a southwest and northeast
      // position of the overlay to peg it to the correct position and size.
      // We need to retrieve the projection from this overlay to do this.
      var overlayProjection = this.getProjection();

      // Retrieve the southwest and northeast coordinates of this overlay
      // in latlngs and convert them to pixels coordinates.
      // We'll use these coordinates to resize the DIV.

      // this occurs at start up and I don't know why
      if ((overlayProjection === null) || (overlayProjection === undefined)) return;

      var sw = overlayProjection.fromLatLngToDivPixel(this.bounds.getSouthWest());
      var ne = overlayProjection.fromLatLngToDivPixel(this.bounds.getNorthEast());

      // resize the image's DIV to fit the indicated dimensions
      var div = this.imgDiv;
      div.style.left   =  sw.x + 'px';
      div.style.top    =  ne.y + 'px';
      div.style.width  = (ne.x - sw.x) + 'px';
      div.style.height = (sw.y - ne.y) + 'px';
   };

   // this is called by 'setMap(null)'
   animatedImages.prototype.onRemove = function()
   {
      // stop the timers - do this first to stop updating before the div is removed
      if (me.animationTimer   !== undefined) window.clearInterval(me.animationTimer);
      if (me.imgsUpdaterTimer !== undefined) window.clearInterval(me.imgsUpdaterTimer);

      this.imgDiv.parentNode.removeChild(this.imgDiv);
      this.imgDiv = null;
   };

   animatedImages.prototype.setImageUrls = function(imageUrls)
   {
      this.imageUrls = imageUrls;
      this.count     = 0;
   };

   animatedImages.prototype.setBounds = function(boundsIn)
   {
      this.bounds = boundsIn;
      this.draw();
   };

   // cycle through the image urls shoving each one in turn into the html div
   animatedImages.prototype.updateAnimation = function()
   {
      if (this.imageUrls.length <= 0) return;

      this.imgDiv.firstChild.src = this.imageUrls[this.count];

      this.count++;

      // increment through all the images
      if (this.count >= this.imageUrls.length)
         this.count = 0;
   };

// ----------------------------------------------------------------------------
//
// Open the about window - Z-index is set to 1001 in the html code
//
// ----------------------------------------------------------------------------

function aboutMsgOpenBtnClick()
{
   // make the window visible
   document.getElementById("aboutMsg").style.display = "inline";

   document.getElementById("aboutMsgBtn").value   = "Close About";
   document.getElementById("aboutMsgBtn").onclick = aboutMsgCloseBtnClick;

   showAboutMsg = true;
}

// ----------------------------------------------------------------------------
// Close the about window
// ----------------------------------------------------------------------------

function aboutMsgCloseBtnClick()
{
   // make the window invisible
   document.getElementById("aboutMsg").style.display = "none";

   document.getElementById("aboutMsgBtn").value   = "About";
   document.getElementById("aboutMsgBtn").onclick = aboutMsgOpenBtnClick;

   showAboutMsg = false;
}

// ----------------------------------------------------------------------------
// Get the map set up parameters from the cookie. If no cookie, use the defaults supplied here.
// ----------------------------------------------------------------------------

function getCookieParms()
{
   // set the defaults in case there is no cookie
   mapCenterLat = MELB_CENTER_LAT;  // set the default to Melbourne's GPO
   mapCenterLng = MELB_CENTER_LNG;  // set the default to Melbourne's GPO
   mapZoom      = 8;
   mapTypeText  = "nr";
   bikeTimeLat  = mapCenterLat;
   bikeTimeLng  = mapCenterLng;
   speedIdx     = 2;                // 18 kph
   showAboutMsg = true;
   showLayerSw  = true;
/*
   // can the map be centered by estimating the user's location from their IP address?
   if (google.loader.ClientLocation)
   {
      // set the default to client's estimated location
      mapCenterLat = google.loader.ClientLocation.latitude;
      mapCenterLng = google.loader.ClientLocation.longitude;
      bikeTimeLat  = mapCenterLat;
      bikeTimeLng  = mapCenterLng;

      mapZoom = 8;
   }
*/
   // got a cookie? - if not RETURN
   var results = document.cookie.match (cookieName + '=(.*?)(;|$)');
   if (!results) return false;

// ----------------------------------------------------------------------------
// a cookie was found - so continue
// ----------------------------------------------------------------------------

   var cookiePrms = unescape(results[1]).split(',');

   // is the cookie up to date?
   if (cookiePrms[0] != cookieVersion)
      return false;

   // a cookie was found - load the data from it
   mapCenterLat = parseFloat(cookiePrms[1]);
   mapCenterLng = parseFloat(cookiePrms[2]);

   mapZoom = parseInt(cookiePrms[3],10);

   mapTypeText = cookiePrms[4];

   bikeTimeLat = parseFloat(cookiePrms[5]);
   bikeTimeLng = parseFloat(cookiePrms[6]);
   speedIdx = cookiePrms[7];

   var ringsLockedStr = cookiePrms[8];
   var tmp = ringsLockedStr.toLowerCase().indexOf("true");

   // shouldn't need the temp var but it doesn't work without it
   ringsLocked = (tmp === 0);

   var showAboutMsgStr = cookiePrms[9];
   tmp = showAboutMsgStr.toLowerCase().indexOf("true");

   // shouldn't need the temp var but it doesn't work without it
   showAboutMsg = (tmp === 0);

   var showLayerSwStr = cookiePrms[10];
   tmp = showLayerSwStr.toLowerCase().indexOf("true");

   // shouldn't need the temp var but it doesn't work without it
   showLayerSw = (tmp === 0);

   return true;
}

// ----------------------------------------------------------------------------
// Set the map parameters in the cookie
// ----------------------------------------------------------------------------

function setCookieParameters(years)
{
   // normally this reads the status of the open layers layer switcher but we
   // don't have one here - so just leave it alone for compatability with OL
   // showLayerSw = layerCtrl.minimizeDiv.style.display !== "none";

   // the cookie holds the map layout and the home location
   var cookieValue = cookieVersion +','
                   + mapCenterLat +','+ mapCenterLng +','
                   + mapZoom +','
                   + mapTypeText +','
                   + bikeTimeLat +','+ bikeTimeLng +','
                   + speedIdx +','
                   + ringsLocked +','
                   + showAboutMsg +','
                   + showLayerSw;

   // set the expiry time
   var date = new Date();
   date.setTime(date.getTime()+(years*365*24*60*60*1000));

   // set the cookie
   document.cookie = cookieName+'='+escape(cookieValue)
                   + '; expires='+date.toGMTString()
                   + '; path=/';

   // if the year is -1 the cookie is destroyed
   return (years !== -1);
}

// ----------------------------------------------------------------------------
// Round to 6 decimal places
// ----------------------------------------------------------------------------

function roundToSixDecimalPlaces(num2Round)
{
   return Math.round(num2Round*1000000)/1000000;
}

// ----------------------------------------------------------------------------
// Generate the URL parameters for the permalink
// ----------------------------------------------------------------------------
// HACK - not implemented yet
function getPermaLinkParms()
{
   var parameters = {};

   var lat = roundToSixDecimalPlaces(mapCenterLat);
   var lng = roundToSixDecimalPlaces(mapCenterLng);

   //parms styled to match google maps
   parameters.ll = lat + ',' + lng;
   parameters.z  = mapZoom;
   parameters.t  = mapTypeText;

   return parameters;
}

// ----------------------------------------------------------------------------
// Search the base layer lookup table
// ----------------------------------------------------------------------------

function baseLayerLookup(idToParm, needle)
{
   var result = '';

   var a = 0;  // MapTypeId is the needle
   var b = 1;  // Url Parameter is the result

   if (!idToParm)
   {
      a = 1;   // Url Parameter is the needle
      b = 0;   // MapTypeId is the result
   }

   // scan through the table to find the matching value
   var layerTot = baseLayersInfo.length;
   for (var i=0; i<layerTot; i++)
   {
      if (baseLayersInfo[i][a] === needle)
      {
         result = baseLayersInfo[i][b];
         break;
      }
   }

   return result;
}

// ----------------------------------------------------------------------------
// Extract the parms from the URL
// ----------------------------------------------------------------------------

function parseParms()
{
   // get the URL and attempt to split it
   var urlParms = window.location.href.toLowerCase();
   urlParms = urlParms.split("?");

   // did the URL contain any parameters?
   if (urlParms.length > 1)
   {
      // get the parm(s) and attempt to split them
      var parms = urlParms[1].split("&");

      // for all the key value pairs
      for (var i=0; i<parms.length; i++)
      {
         // get the key value pairs and attempt to split them up
         var keyValue = parms[i].split("=");

         // the parms must come in key value pairs else the url is malformed
         if (keyValue.length != 2)
            break;

         // is this the map center lat/lng parameter?
         if (keyValue[0] == 'll')
         {
            // break up the value into lat and lng
            var latLng = keyValue[1].split(",");

            // the lat/lng must come as a pair else the url is malformed
            if (latLng.length != 2)
               break;

            mapCenterLat = parseFloat(latLng[0]);
            mapCenterLng = parseFloat(latLng[1]);
         }

         // is this the zoom parameter?
         if (keyValue[0] == 'z')
            mapZoom = parseInt(keyValue[1],10);

         // is this the map type parameter?
         if (keyValue[0] == 't')
            mapTypeText = keyValue[1];

         // is this the fusion table parameter?
         if (keyValue[0] == 'ft')
         {
            layerFt0Id = -1;
            layerFt1Id = -1;

            // got the parameter's value?
            if (keyValue[1] !== undefined)
            {
               // the key's value should consist of a comma separated string of fusion table ids
               var fusionTableList = [];
               fusionTableList = keyValue[1].split(",");

               // Save the parms if they exist and they are also integers. We
               // already have three FTs, so we can only cater for another two.
               if ((fusionTableList.length >= 1) && (!isNaN(parseInt(fusionTableList[0]))))
                  layerFt0Id = parseInt(fusionTableList[0]);

               if ((fusionTableList.length >= 2) && (!isNaN(parseInt(fusionTableList[1]))))
                  layerFt1Id = parseInt(fusionTableList[1]);
            }
         }
      }
   }
}

// ----------------------------------------------------------------------------
// Determine which state this lat/lng belongs to - this is approximate
// Vic, NSW and Canberra are pretty rough approximations
// ----------------------------------------------------------------------------

function identifyState(lat,lng)
{
   var state = 'State not found';

   if ((lat >= -39.2) && (lat <= -35.5) && (lng >= 140.9736) && (lng <= 150))
      state = 'Vic';   // do Vic before NSW and SA for best approximation
   else if ((lat >= -35.925) && (lat <= -35.125) && (lng >= 148.76) && (lng <= 149.4))
      state = 'ACT';   // do ACT before NSW for best approximation
   else if ((lat >= -37.5) && (lat <= -29) && (lng >= 140.9736) && (lng <= 154))
      state = 'NSW';
   else if ((lat >= -38.5) && (lat <= -25.996) && (lng >= 129) && (lng <= 141))
      state = 'SA';    // do SA before Qld for best approximation
   else if ((lat >= -29) && (lat <= -11) && (lng >= 138) && (lng <= 154))
      state = 'Qld';
   else if ((lat >= -36) && (lat <= -13.5) && (lng >= 112.5) && (lng <= 129))
      state = 'WA';
   else if ((lat >= -44) && (lat <= -39.2) && (lng >= 143) && (lng <= 149))
      state = 'Tas';
   else if ((lat >= -25.996) && (lat <= -11) && (lng >= 129) && (lng <= 138))
      state = 'NT';
   else if ((lat >= -48) && (lat <= -32) && (lng >= 165) && (lng <= 179))
      state = 'NZ';

   return state;
}

// ----------------------------------------------------------------------------
// Center and zoom
// ----------------------------------------------------------------------------

function centerAndZoom(lat,lng,zoom)
{
   var point = new google.maps.LatLng(lat,lng);
   map.setCenter(point);
   map.setZoom(zoom);
}

// ----------------------------------------------------------------------------
// Find the bounds of a BOM image
// image size is 512,512 pixels
// ----------------------------------------------------------------------------

function getRadarImageBounds(radar, radarRange)
{
   var kmFromRadar = 64;

   switch (radarRange)
   {
   case 'range512':
      kmFromRadar = 512;
      break;
   case 'range256':
      kmFromRadar = 256;
      break;
   case 'range128':
      kmFromRadar = 128;
      break;
   case 'range64':
      kmFromRadar = 64;
      break;
   default:
      break;
   }

   // deg2rad = 2*pi/360.0   rad2Deg = 1/deg2rad
   var deg2rad = 0.017453293;
   var rad2Deg = 57.29577951;

   var radarLat = radar.lat * deg2rad;
   var radarLng = radar.lng * deg2rad;

   // one degree at equator in km = (2*Pi*R)/(360*1000.0)
   //var oneDegInKm = 111.11;            // BOM's uses this figure
   var oneDegInKm = 111.195079734369;  // using the mean radius or 6371008.7714 = ((2*a)+b)/3
   var latDegFromRadar = (kmFromRadar/oneDegInKm)*deg2rad;

   // find the bounds
   var botLat   = radarLat - latDegFromRadar;
   var topLat   = radarLat + latDegFromRadar;
   var leftLng  = radarLng - (latDegFromRadar/Math.cos(radarLat));
   var rightLng = radarLng + (latDegFromRadar/Math.cos(radarLat));

   leftLng  = leftLng  * rad2Deg;
   botLat   = botLat   * rad2Deg;
   rightLng = rightLng * rad2Deg;
   topLat   = topLat   * rad2Deg;

   var bounds = new google.maps.LatLngBounds(
      new google.maps.LatLng(botLat,leftLng),
      new google.maps.LatLng(topLat,rightLng));

   return bounds;
}

// ----------------------------------------------------------------------------
// The map was moved or zoomed - update the radar layer
// This is only called if the range has changed or a new radar is within range
// Update the image bounds position and the images themselves
//
// List of radars:     http://www.bom.gov.au/weather/radar/index.shtml
// and their details:  http://www.bom.gov.au/weather/radar/about/radar_site_info.shtml
//
// This is the source of the files - coming as json from BigYak:
//    ftp://ftp2.bom.gov.au/anon/gen/radar/
// ----------------------------------------------------------------------------

function updateRadarImageBoundsAndLocation(radar, radarRange)
{
   // exit if the layer isn't ready for us
   if ((pLayerWeatherRadar === null) || (pLayerWeatherRadar === undefined)) return;

   // exit if the layer is not selected
   if (!pLayerWeatherRadar.getMap()) return;

   // change the bounds of the radar image
   var imageBounds = getRadarImageBounds(radar, radarRange);

   pLayerWeatherRadar.setBounds(imageBounds);

   // note that this url is case sensitive
   var urlBase = 'http://www.bom.gov.au/radar/';

   // build the full urls for each image
   var radarUrls = [];
   for (var i=0; i<radar.ranges[radarRange].length; i++)
   {
      // get the next URL to display
      radarUrls[i] = urlBase + radar.ranges[radarRange][i];
   }

   // The browser may or may not cache the images - if not, the animation will continually
   // download them. For example: Firefox version from portableapps, sets this by default:
   //   browser.cache.disk.capacity default integer 50000 - refer web page: "about:config"
   // so watch out for that download limit on your plan!
   pLayerWeatherRadar.setImageUrls(radarUrls);
}

// ----------------------------------------------------------------------------
// The AJAX request has returned a result - the result contains: Location as
// lat,lng, Australian state, radar name and the image urls to animate for each range
//
// All calls to the server use this listener except the mapWasMoved call.
// ----------------------------------------------------------------------------

function listenerRadarDetailsFromServerA(textFromServer, xmlDoc)
{
   // allow future calls to the server for radar data
   gettingRadarInfo = false;

   var radarDetails = parseJson(textFromServer);

   if (radarDetails === null) return;

   // update the radar filenames - whether this be a new or old radar
   radar = radarDetails;

   updateRadarImageBoundsAndLocation(radar, radarRange);
}

// ----------------------------------------------------------------------------
// The AJAX request has returned a result - the result contains: Location as
// lat,lng, Australian state, radar name and the image urls to animate for each range
//
// This listener is used solely by the mapWasMoved call. The map gets moved a
// lot and we only want to change the image array and bounds when we move so
// far, that a new radar becomes in range ie the radar closest to the map center.
// ----------------------------------------------------------------------------

function listenerRadarDetailsFromServerB(textFromServer, xmlDoc)
{
   // allow future calls to the server for radar data
   gettingRadarInfo = false;

   var radarDetails = parseJson(textFromServer);

   if (radarDetails === null) return;

   // the map was moved - is the closest radar now a different radar?
   var newRadar = ((radar.lat !== radarDetails.lat) || (radar.lng !== radarDetails.lng));

   // this will (re)position the image at the radar location - the image size will depend on the range
   if (newRadar)
   {
      // update the radar filenames - whether this be a new or old radar
      radar = radarDetails;

      updateRadarImageBoundsAndLocation(radar, radarRange);
   }
}

// ----------------------------------------------------------------------------
// Get the radar data for the nearest radar from the server. This is called:
//   1a) on the precondition that the layer exists and is on display
//   1b) if the map is moved - the radar may have to be changed
//
//   2a) on the precondition that the layer exists and is on display
//   2b) every 3 minutes to get the updated image urls
//
// Note that zoom functionality does not need to ask the server for new
// data. It already has what it needs locally in the radar array.
// ----------------------------------------------------------------------------

function getRadarDetailsFromServerCheckForRadarChanged()
{
   var wasCalledBymapWasMoved = true;

   getRadarDetailsFromServer(wasCalledBymapWasMoved);
}

function getRadarDetailsFromServer(flag)
{
   // exit if the layer isn't ready for us
   if ((pLayerWeatherRadar === null) || (pLayerWeatherRadar === undefined)) return;

   // exit if the layer is not selected
   if (!pLayerWeatherRadar.getMap()) return;

   // don't fire this again if it's already underway
   if (gettingRadarInfo) return;

   // OK now we're getting some radar info from the server.
   // Lock out any further calls till it's done.
   gettingRadarInfo = true;

   // this listener always updates the radar array and is the default
   // it's fired by the animation updater
   var listener = listenerRadarDetailsFromServerA;

   // the flag indicates the map was moved - only update the radar if ncesssary
   if (flag !== undefined)
   {
      // this listener only updates the radar array when a new radar is detected
      listener = listenerRadarDetailsFromServerB;
   }

   var center = map.getCenter();

   // note that the ajax call adds the time as a parameter to the url, so the url is not cached
   var jsonFeedUrl = 'http://www.bigyak.net.au/trails/cd/getradarimageurls.php?fnc=g0&ll='+center.lat()+','+center.lng();

   jsonFeedUrl = encodeURI(jsonFeedUrl);

   // set the call back function - this waits for the
   // request to complete as a background thread

   var xmlhttp = ajaxRequest('GET', jsonFeedUrl, true, listener);

   // At this point nothing happens until the AJAX result is returned.
   // That event results in the listenerRadarDetailsFromServer function being called
}

// ----------------------------------------------------------------------------
// Sets the server
// ----------------------------------------------------------------------------

function setUpServer()
{
   // note that the ajax call adds the time as a parameter to the url, so the url is not cached
   var url = 'http://www.bigyak.net.au/trails/cd/seasalt.php?fnc=c0';

   url = encodeURI(url);

   // set the call back function - this waits for the
   // request to complete as a background thread
   var xmlhttp = ajaxRequest('GET', url, true, function(){});

   // At this point nothing happens until the AJAX result is returned.
}

// ----------------------------------------------------------------------------
// Add in the weather radar layer
// ----------------------------------------------------------------------------

function layerWeatherRadar()
{
   var layer = new animatedImages(
      'BOM weather radar',          // layer name
      1.0,                          // animation rate in seconds
      getRadarDetailsFromServer,    // image array updater function
      180.0);                       // update image array rate in seconds

   return layer;
}

// ----------------------------------------------------------------------------
// A new bike speed was selected
// ----------------------------------------------------------------------------

function speedChanged()
{
   speedIdx = document.getElementById("speedID").selectedIndex;
   speed    = document.getElementById("speedID").value;

   // (re)draw rings at the center point
   pTimeByBikeMarker.updateRings(bikeTimeLat, bikeTimeLng, speed);
}

// ----------------------------------------------------------------------------
// The Lock center/Track center button was toggled
// ----------------------------------------------------------------------------

function lockButtonClicked()
{
   if (!ringsLocked)  // lock reference point
   {
      ringsLocked = true;
      document.getElementById("lockButton").value = 'Unlock rings';
   }
   else  // change reference point
   {
      ringsLocked = false;
      document.getElementById("lockButton").value = 'Lock rings';
   }
}

// ----------------------------------------------------------------------------
// Make time by bike marker info window
// ----------------------------------------------------------------------------

function makeTimeByBikeMarkerInfoWindow(marker)
{
   var contentStr = '<h3>How long by bike?</h3><p>How long will it take to get there by bicycle? The rings increment by 5 minute intervals - the outer ring represents half an hour.<br/><br/>The times are based on: the average bicycle speed selected, the line of site distance and an efficiency factor of 80%, compensating for the fact that all routes are indirect to some degree.<br/><br/>Select speed to suit. The ring center can be locked to a location or track the map center.<br/><br/><select id="speedID" name="bicyclesAverageSpeed" onchange="speedChanged()">';

   var len = speeds.length;
   for (var i=0; i<len; i++)
      contentStr += '<option value="'+speeds[i]+'">'+speeds[i]+' kph</option>';

   contentStr += '</select>&nbsp;<input id="lockButton" type="button" value="" onclick="lockButtonClicked()"/><br/><br/>Fight heart disease, obesity, traffic congestion, peak oil, climate change and save money - ride a bike.</p>';

   infoWindow = new google.maps.InfoWindow({
      content: contentStr,
      maxWidth: 600
   });

   google.maps.event.addListener(marker, 'click', function()
   {
      if (infoWindow.getMap()) return;  // the window is already open

      infoWindow.open(map,marker);
   });

   google.maps.event.addListener(infoWindow, 'domready', function()
   {
      // set the speed in the pulldown list
      document.getElementById("speedID").selectedIndex = speedIdx;

      if (ringsLocked)
         document.getElementById("lockButton").value = 'Unlock rings';
      else
         document.getElementById("lockButton").value = 'Lock rings';
   });

   // close the info window when the map is clicked on
   google.maps.event.addListener(map, 'click', function()
   {
      if (infoWindow.getMap())
      {
         infoWindow.close();
         google.maps.event.trigger(infoWindow,'closeclick');
      }
   });
}

// ----------------------------------------------------------------------------
// Redraw all the rings at the specified center
// ----------------------------------------------------------------------------

function updateTimeByBike()
{
   // exit if the layers are not ready for us
   if ((pTimeByBikeMarker === null) || (pTimeByBikeMarker === undefined)) return;

   // don't update while the user is looking at the time by bike info window
   if (infoWindow.getMap()) return;

   // track center?
   if (!ringsLocked)
   {
      var center = map.getCenter();

      bikeTimeLat = center.lat();
      bikeTimeLng = center.lng();
   }

   // (re)draw rings at the center point
   pTimeByBikeMarker.updateRings(bikeTimeLat, bikeTimeLng, speed);
}

// ----------------------------------------------------------------------------
// Add in the time by bike marker
// ----------------------------------------------------------------------------

function markerTimeByBike(mapIn, locLatIn, locLngIn, speedIn)
{
   var timeByBikeCenter = new google.maps.LatLng(locLatIn,locLngIn);

   var markerImage = new google.maps.MarkerImage(
      'http://maps.google.com/mapfiles/kml/shapes/cycling.png',
      new google.maps.Size (64,64),   // actual size
      new google.maps.Point( 0, 0),   // origin
      new google.maps.Point(16,16),   // anchor
      new google.maps.Size (32,32)    // scaled size
   );

   this.name = 'Time by bike - click on icon';

   this.setIcon(markerImage);
   this.setPosition(timeByBikeCenter);

   // attach an info window to the marker
   makeTimeByBikeMarkerInfoWindow(this);

   var layerOptions = {
      strokeColor: 'red',
      strokeWeight: 2,
      strokeOpacity: 1.0,
      fillColor: 'red',
      fillOpacity: 0.0,
      clickable: false,
      map: null
   }

   // stick all the circle layers in this array
   this.layers = [];

   // create the six circle layers
   for (var i=0; i<6; i++)
   {
      var layer = new google.maps.Circle(layerOptions);
      this.layers.push(layer);
   }

   this.updateRings(locLatIn, locLngIn, speedIn);

   this.ourSetMap(mapIn);
}

// ----------------------------------------------------------------------------
// Our marker will be based on a normal marker and will be expanded
// so that it contains timing rings around it
// ----------------------------------------------------------------------------

   markerTimeByBike.prototype = new google.maps.Marker({
      map: null,
      title: 'Click me',
      flat: true
   });

// ----------------------------------------------------------------------------
// add or remove the marker and rings to/from the map
// ----------------------------------------------------------------------------

   markerTimeByBike.prototype.ourSetMap = function(mapIn)
   {
      // the marker
      this.setMap(mapIn);

      // the rings
      for (var i=0; i<6; i++)
      {
         this.layers[i].setMap(mapIn);
      }
   }

// ----------------------------------------------------------------------------
// A different speed was selected or the map was moved. This function expects the center to be in
// lat,lng units. Efficiency accounts for the fact that most routes are indirect to some extent.
// ----------------------------------------------------------------------------

   markerTimeByBike.prototype.updateRings = function(locLatIn,locLngIn, speedIn)
   {
      var timeByBikeCenter = new google.maps.LatLng(locLatIn,locLngIn);

      // set the marker's position
      this.setPosition(timeByBikeCenter);

      var efficiency = 0.80;  // guestimate

      // 6 rings at 5 minute intervals gives an outer ring of 30 minutes
      for (var i=0; i<6; i++)
      {
         this.layers[i].setCenter(timeByBikeCenter);
         this.layers[i].setRadius(((i+1) * 5 * 1000.0 * speedIn * efficiency)/60.0);
      }
   }

// ----------------------------------------------------------------------------
// The AJAX request has returned a result - the result contains the route
// Parse the json returned by the server and draw it as a route, on to the route layer
// ----------------------------------------------------------------------------

function listenerRouteDetailsFromServer(textFromServer, xmlDoc)
{
   // make a GPX file in addition to just drawing the route
   var GPXfile = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'+ "\n"
      + '<gpx xmlns="http://www.topografix.com/GPX/1/1" creator="Big Yak OZ bike trails" version="1.0">'
      + "\n<trk><name>BigYak track</name><trkseg>\n";

   //alert(textFromServer);

   var routeDetails = parseJson(textFromServer);

   if (routeDetails === null) return;
   if (!routeDetails.route_geometry) return;

   //examineObject(routeDetails);
   //examineObject(routeDetails.route_summary);

   // make all the points for the route from the input data
   var points = [];

   var numberOfpoints = routeDetails.route_geometry.length;
   for (var i=0; i<numberOfpoints; i++)
   {
      var lat = routeDetails.route_geometry[i][0];
      var lng = routeDetails.route_geometry[i][1];

      var point = new google.maps.LatLng(lat,lng);
      points.push(point);

      // add points to GPX file
      GPXfile += '<trkpt lon="'+lng+'" lat="'+lat+'"/>\n';
   }

   polylineRoute.setPath(points);

   // complete the GPX file
   GPXfile += "</trkseg></trk></gpx>\n";

   //alert(GPXfile);

   //alert(routeDetails.route_instructions);
   //alert(routeDetails.route_summary.total_distance);
   //alert(routeDetails.route_summary.total_time);

   var distanceKm = routeDetails.route_summary.total_distance/1000.0;
   //var timeMins = routeDetails.route_summary.total_time/60.0;
   //var speed = (distanceKm * 60.0) / timeMins;

   //alert('Distance = '+distanceKm+' km. Time at '+parseInt(speed)+' kph = '+parseInt(timeMins)+' mins');

   var timeMins = parseInt((distanceKm/speed)*60.0);
   //alert('Distance = '+distanceKm+' km. Time at '+speed+' kph = '+timeMins+' mins');
}

// ----------------------------------------------------------------------------
// Get the route from the routing service
// ----------------------------------------------------------------------------

function getRoute()
{
   var apiKey = '37f2e1a4bc5c4872ab90c51e54a27ac1';  // cloudmade: BC9A493B41014CAABB98F0471D759707

   var sLat = roundToSixDecimalPlaces(markerStartPoint.getPosition().lat());
   var sLng = roundToSixDecimalPlaces(markerStartPoint.getPosition().lng());
   var fLat = roundToSixDecimalPlaces(markerFinishPoint.getPosition().lat());
   var fLng = roundToSixDecimalPlaces(markerFinishPoint.getPosition().lng());
   var format = 'js';

   var redirect = 'cd/redirector.php?url='
   var baseURL = 'http://routes.cloudmade.com/'+apiKey+'/api/0.3/';

   // note that the ajax call adds the time as a parameter to the url, so the url is not cached
   var jsonFeedUrl = redirect+baseURL+sLat+','+sLng+','+fLat+','+fLng+'/bicycle.'+format+'?units=km&lang=en&translation=common';

   jsonFeedUrl = encodeURI(jsonFeedUrl);

   // set the call back function - this waits for the request to complete as a background thread
   var xmlhttp = ajaxRequest('GET', jsonFeedUrl, true, listenerRouteDetailsFromServer);

   // At this point nothing happens until the AJAX result is returned.
   // That event results in the listenerRouteDetailsFromServer function being called

   // get the link for the GPX file
   var format = 'gpx';
   var uri = baseURL+sLat+','+sLng+','+fLat+','+fLng+'/bicycle.'+format+'?units=km&lang=en&translation=common';

   document.getElementById("gpxDownload").innerHTML = '<a href="'+uri+'">get GPX</a>';
}

// ----------------------------------------------------------------------------
// The user stopped dragging either the start or finish feature - we need to update the route
// ----------------------------------------------------------------------------

function onDragComplete()
{
   getRoute();
}

// ----------------------------------------------------------------------------
// Place the start and finish markers on the map. Must be called
// after map is drawn and centred to ensure bounds are valid.
// ----------------------------------------------------------------------------

function drawStartFinish()
{
   // This is all in EPSG:900913 co-ordinates
   var bounds = map.getBounds();

   // make sure the bounds are valaid
   if ((bounds === null) || (bounds === undefined)) return;

   var west     = bounds.getSouthWest().lng();
   var westEast = bounds.toSpan().lng();

   // work out the two maker locations
   var sPointLat = bounds.getCenter().lat();
   var fPointLat = sPointLat;
   var sPointLng = west+(westEast*0.33333);
   var fPointLng = west+(westEast*0.66666);

   var startPoint  = new google.maps.LatLng(sPointLat,sPointLng);
   var finishPoint = new google.maps.LatLng(fPointLat,fPointLng);

   // HACK option to shrink the markers
   var markerImage = new google.maps.MarkerImage(
      'http://maps.google.com/mapfiles/kml/shapes/cycling.png',
      new google.maps.Size (64,64),   // actual size
      new google.maps.Point( 0, 0),   // origin
      new google.maps.Point(16,16),   // anchor
      new google.maps.Size (32,32)    // scaled size
   );

   markerStartPoint = new google.maps.Marker({
      map: map,
      position:  startPoint,
      title: 'Drag me',
      icon: 'http://maps.google.com/mapfiles/kml/paddle/A.png',
      clickable: false,
      draggable: true,
      flat: true
   });

   markerFinishPoint = new google.maps.Marker({
      map: map,
      position:  finishPoint,
      title: 'Drag me',
      icon: 'http://maps.google.com/mapfiles/kml/paddle/B.png',
      clickable: false,
      draggable: true,
      flat: true
   });

   google.maps.event.addListener(markerStartPoint,  'dragend', onDragComplete);
   google.maps.event.addListener(markerFinishPoint, 'dragend', onDragComplete);

   polylineRoute = new google.maps.Polyline({
      map: map,
      strokeColor: 'red',
      strokeWidth: 5,
      strokeOpacity: 1.0
   });
}

// ----------------------------------------------------------------------------
// The user clicked on the routing on/off button
// ----------------------------------------------------------------------------

function routerBtnClick()
{
   if (document.getElementById("routerBtn").value === 'Start routing')
   {
      drawStartFinish();

      //HACK  getRoute();

      document.getElementById("routerBtn").value = 'Stop routing';
   }
   else
   {
      // destroy all the routing features
      markerStartPoint.setMap(null); 
      markerStartPoint = null; 

      markerFinishPoint.setMap(null); 
      markerFinishPoint = null; 

      polylineRoute.setMap(null); 
      polylineRoute = null; 

      document.getElementById("gpxDownload").innerHTML = '';
      document.getElementById("routerBtn").value = 'Start routing';
   }
}

// ----------------------------------------------------------------------------
// We can scroll/wrap the whole world east or west indefinately but we can't
// scroll/wrap the world north and south. Wrap the coordinates to suit.
// ----------------------------------------------------------------------------

function wrapMap(loc, zoom)
{
  // wrap world left/right as needed
  var numTiles = 1 << zoom;

  var x = loc.x % numTiles;
  if (x < 0)
    loc.x = x + numTiles;  // wrap
  else
    loc.x = x;  // no wrap

  var scrollNorthSouth = false;

  // can't wrap the world up or down
  if (loc.y < 0 || loc.y >= numTiles)
     scrollNorthSouth = true;

  return scrollNorthSouth;
}

// ----------------------------------------------------------------------------
// Add in the NearMap base layer
// ----------------------------------------------------------------------------

function baseLayerNearMap()
{
   // make a base layer in the Google API
   var tiler = new google.maps.ImageMapType({
      getTileUrl: function(coord, zoom)
      {
         // clone the original, as if we alter it, that may be problem for the API?
         var loc = new google.maps.Point(coord.x, coord.y);

         if (wrapMap(loc, zoom)) return null;  // trying to scroll north/south

         // some sites use multiple tile servers for speed
         var serverIds = ['0', '1', '2', '3'];

         // step through the different servers for each
         // tile. Values should all be positive
         var idx = (loc.x + loc.y) % serverIds.length;

         // form the tile URL
         var server = serverIds[idx];
         return 'http://web'+server+'.nearmap.com/maps/' + 'x=' + coord.x + '&y=' + coord.y + '&z=' + zoom + '&hl=en&nml=vert&s=ga';
      },
      tileSize: new google.maps.Size(256, 256),
      name: 'NearMap',  // this goes in the map selection menu
      maxZoom: 22       // this is required for a Google base layer
   });

   map.mapTypes.set(NEARMAP, tiler);
}

// ----------------------------------------------------------------------------
// Add in the Osm Bike base layer
// ----------------------------------------------------------------------------

function baseLayerOsmBike()
{
   // make a base layer in the Google API
   var tiler = new google.maps.ImageMapType({
      getTileUrl: function(coord, zoom)
      {
         // clone the original, as if we alter it, that may be problem for the API?
         var loc = new google.maps.Point(coord.x, coord.y);

         if (wrapMap(loc, zoom)) return null;  // trying to scroll north/south

         // some sites use multiple tile servers for speed
         var serverIds = ['a', 'b', 'c'];

         // step through the different servers for each
         // tile. Values should all be positive
         var idx = (loc.x + loc.y) % serverIds.length;

         // form the tile URL
         var server = serverIds[idx];
         return 'http://'+server+'.tile.opencyclemap.org/cycle/' + zoom + '/' + loc.x + '/' + loc.y + '.png';
      },
      tileSize: new google.maps.Size(256, 256),
      name: 'OSM bike',  // this goes in the map selection menu
      maxZoom: 17        // this is required for a Google base layer
   });

   map.mapTypes.set(OSMBIKE, tiler);
}

// ----------------------------------------------------------------------------
// Add in the Osm Mapnik base layer
// ----------------------------------------------------------------------------

function baseLayerOsm()
{
   // make a base layer in the Google API
   var tiler = new google.maps.ImageMapType({
      getTileUrl: function(coord, zoom)
      {
         // clone the original, as if we alter it, that may be problem for the API?
         var loc = new google.maps.Point(coord.x, coord.y);

         if (wrapMap(loc, zoom)) return null;  // trying to scroll north/south

         // some sites use multiple tile servers for speed
         var serverIds = ['a', 'b', 'c'];

         // step through the different servers for each
         // tile. Values should all be positive
         var idx = (loc.x + loc.y) % serverIds.length;

         // form the tile URL
         var server = serverIds[idx];
         return 'http://'+server+'.tile.openstreetmap.org/' + zoom + '/' + loc.x + '/' + loc.y + '.png';
      },
      tileSize: new google.maps.Size(256, 256),
      name: 'OSM',  // this goes in the map selection menu
      maxZoom: 18   // this is required for a Google base layer
   });

   map.mapTypes.set(OSM, tiler);
}

// ----------------------------------------------------------------------------
// Add in the fusion table
// ----------------------------------------------------------------------------

function ftLayer(tableId, name, enableLayer)
{
  var ourMap = null;
  if (enableLayer) ourMap = map;

   var layer = new google.maps.FusionTablesLayer({
      clickable: false,
      map: ourMap,
      name: name,
      query: {
         select: 'geometry',
         from:    tableId
      },
      suppressInfoWindows: true
   });

   return layer;
}

// ----------------------------------------------------------------------------
// Add in the LGAs overlay
// ----------------------------------------------------------------------------

function layerLga()
{
   return ftLayer('486711' , 'LGA - check who to call', false);
}

// ----------------------------------------------------------------------------
// Add in the LGAs overlay
// ----------------------------------------------------------------------------

function layerOsmOffRoadBikeRoute()
{
   return ftLayer('465661', 'OSM off road', true);
}

// ----------------------------------------------------------------------------
// Add in the LGAs overlay
// ----------------------------------------------------------------------------

function layerOsmOnRoadBikeRoute()
{
   return ftLayer('465274', 'OSM on road', true);
}

// ----------------------------------------------------------------------------
// Add in the KML table
// ----------------------------------------------------------------------------

function kmlLayer(kmlUrl, name, enableLayer)
{
  var ourMap = null;
  if (enableLayer) ourMap = map;

   var layer = new google.maps.KmlLayer(kmlUrl, {
      clickable: true,
      map: ourMap,
      preserveViewport: true,
      suppressInfoWindows: false
   });

   // add on a new property - the layer name
   layer.name = name;

   return layer;
}

// ----------------------------------------------------------------------------
// Add in the Sydney campaigns overlay
// ----------------------------------------------------------------------------

function layerSydneyCampaigns()
{
   return kmlLayer('http://bigyak.net.au/trails/plc/nsw/kml/sydneycampaigns_NL.kml?', 'Sydney campaigns', true);
}

// ----------------------------------------------------------------------------
// Add in the Sydney Counters overlay
// ----------------------------------------------------------------------------

function layerSydneyCounters()
{
   return kmlLayer('http://bigyak.net.au/trails/cd/counters.php?fnc=k0&st=nsw', 'Sydney counters', false);
}

// ----------------------------------------------------------------------------
// Add in the Sydney RTA CTV overlay
// ----------------------------------------------------------------------------

function layerSydneyRtaCctv()
{
   return kmlLayer('http://bigyak.net.au/trails/cd/cctv.php?fnc=k0&st=nsw', 'Sydney RTA CCTV', false);
}

// ----------------------------------------------------------------------------
// Add in the Melbourne navigation overlay
// ----------------------------------------------------------------------------

function layerMelbourneNavigation()
{
   return kmlLayer('http://bigyak.net.au/vicbiketrails/vicbiketrails.php', 'Melbourne navigation', false);
}

// ----------------------------------------------------------------------------
// Add in the Melbourne Bike Share overlay
// ----------------------------------------------------------------------------

function layerMelbourneBikeShare()
{
   return kmlLayer('http://www.bigyak.net.au/trails/cd/melbikeshare.php?fnc=k0', 'Melbourne bike share', false);
}

// ----------------------------------------------------------------------------
// Add in the Melbourne campaigns overlay
// ----------------------------------------------------------------------------

function layerMelbourneCampaigns()
{
   return kmlLayer('http://bigyak.net.au/trails/plc/vic/kml/melNews.kml', 'Melbourne campaigns', false);
}

// ----------------------------------------------------------------------------
// Add in the Melbourne magpies overlay
// 2010: http://dl.dropbox.com/u/7372451/smagpie_sm_forGmap.gif
// 2011: VEDataType.GeoRSS http://www.dse.vic.gov.au/__data/assets/xml_file/0020/125174/magpiemap_master.xml
// 2011: http://www.dse.vic.gov.au/__data/assets/image/0012/122115/magpie1.gif
// ----------------------------------------------------------------------------

function layerMelbourneMagpies()
{
   return kmlLayer('http://www.dse.vic.gov.au/__data/assets/xml_file/0020/125174/magpiemap_master.xml', "DSE's magpies - spring 2011", false);
   //return kmlLayer('http://maps.google.com.au/maps/ms?ie=UTF8&hl=en&msa=0&msid=216539395879771034915.00048c55e42c5df4be696&source=embed&output=kml', "DSE's magpies - spring 2010", false);
}

// ----------------------------------------------------------------------------
// Add in the Adelaide campaigns overlay
// ----------------------------------------------------------------------------

function layerAdelaideCampaigns()
{
   return kmlLayer('http://maps.google.com.au/maps/ms?msa=0&msid=109500378803690202292.000467b495f9e3b8756b6&output=kml', 'Adelaide campaigns', true);
}

// ----------------------------------------------------------------------------
// Add in the Adelaide bakeries overlay
// ----------------------------------------------------------------------------

function layerAdelaideBakeries()
{
   return kmlLayer('http://maps.google.com.au/maps/ms?msa=0&msid=214383191864793567048.000495fd8a77a7de81ef8&output=kml', 'Adelaide bakeries', false);
}

// ----------------------------------------------------------------------------
// Jump to the location selected in the pulldown list
// ----------------------------------------------------------------------------

function jumpToPlace()
{
   var place = places[document.getElementById("places").selectedIndex];
   document.getElementById("places").selectedIndex = 0;

   // is this an invalid selection such as a list spacer?
   if (place == 'U')
      return;

   // get the coordinates and zoom level for the selected place
   var placeView  = place.split(",");
   mapCenterLat   = parseFloat(placeView[0]);
   mapCenterLng   = parseFloat(placeView[1]);
   mapZoom        = parseInt  (placeView[2],10);

   // update the map centre and zoom level
   centerAndZoom(mapCenterLat,mapCenterLng,mapZoom);
}

// ----------------------------------------------------------------------------
// Focus on the trail selected in the pulldown list
// ----------------------------------------------------------------------------

function jumpToTrail()
{
   var trail = trailData[document.getElementById("trails").selectedIndex];
   document.getElementById("trails").selectedIndex = 0;

   // is this an invalid selection such as a list spacer?
   if (trail.charAt(0) === 'U')
      return;

   // get the coordinates and zoom level for the selected trail
   var trailInfo = trail.split(",");
   mapCenterLat  = parseFloat(trailInfo[0]);
   mapCenterLng  = parseFloat(trailInfo[1]);
   mapZoom       = parseInt  (trailInfo[2],10);

   // update the map centre and zoom level
   centerAndZoom(mapCenterLat,mapCenterLng,mapZoom);
}

// ----------------------------------------------------------------------------
// The user moved the map
// ----------------------------------------------------------------------------

function mapWasMoved()
{
   // this removes the info window and marker shadows. Note that the
   // dom must be ready in order to have getPanes() return valid data
   // we stick here as just to trigger it - the user will move the map almost certainly
   if (accessPanesDummy.getPanes() !== undefined)
     if (accessPanesDummy.getPanes().floatShadow.style.display != 'none')
        accessPanesDummy.getPanes().floatShadow.style.display = 'none';

   var center = map.getCenter();
   mapCenterLat = center.lat();
   mapCenterLng = center.lng();

   updateTimeByBike();

   // go get the radar data from the server - the radar may have changed
   getRadarDetailsFromServerCheckForRadarChanged();

   // get the state
   var state = identifyState(center.lat(), center.lng());

   // do nothing if the state has not changed
   if (lastState == state) return;

   //alert(lastState + ' ----> ' + state);

   // stop all the layer switchers from showing
   // note this does not affect the visibility of the actual layers
   layerCtrlNSW.setVisibility(false);
   layerCtrlSA.setVisibility(false);
   layerCtrlVic.setVisibility(false);

   // now shows the layer switchers associated with this location
   switch (state)
   {
   case 'ACT':
      trailData = trailDataACT;
      break;
   case 'NSW':
      trailData = trailDataNSW;
      layerCtrlNSW.setVisibility(true);
      break;
   case 'NT':
      trailData = trailDataNT;
      break;
   case 'Qld':
      trailData = trailDataQld;
      break;
   case 'SA':
      trailData = trailDataSA;
      layerCtrlSA.setVisibility(true);
      break;
   case 'Tas':
      trailData = trailDataTas;
      break;
   case 'Vic':
      trailData = trailDataVic;
      layerCtrlVic.setVisibility(true);
      break;
   case 'WA':
      trailData = trailDataWA;
      break;
   default:
      break;
   }

   // fill the trail selection pulldown list with the trail names for each state
   var optionList = document.getElementById("trails").options;
   optionList.length = 0;

   var len = trailData.length;
   for (var i=0; i<len; i++)
   {
      // extract the name and add it to the pulldown list
      var trailInfo = trailData[i].split(",");
      optionList[i] = new Option(trailInfo[3],'');
   }

   lastState = state;
}

// ----------------------------------------------------------------------------
// The user zoomed the map
// ----------------------------------------------------------------------------

function mapWasZoomed()
{
   // Allow future calls to the server for radar data. We do it
   // here just in case it gets jammed in the set position. ie
   // just clear it out fairly regularly.
   gettingRadarInfo = false;

   mapZoom = map.getZoom();

   // remap the zoom to the radar range
   // a bigger zoom number is more close in
   switch (mapZoom)
   {
   // zoom levels 1 to 5 - the radar layer is off
   case 1:
   case 2:
   case 3:
   case 4:
   case 5:
   case 6:
   case 7:  //6
      radarRange = 'range512';
      break;
   case 8:  //7
      radarRange = 'range256';
      break;
   case 9:  //8
      radarRange = 'range128';
      break;
   default:
      radarRange = 'range64';

      // only some radars have the 64 km range but they all have 128 km
      if (radar.ranges[radarRange].length === 0)
         radarRange = 'range128';
      break;
   }

   // has the range changed after the zoom?
   if (lastRadarRange === radarRange) return;

   // we have the radar's range details already, so only need to update the images and their bounds
   updateRadarImageBoundsAndLocation(radar, radarRange);

   lastRadarRange = radarRange;
}

// ----------------------------------------------------------------------------
// The base layer was changed - up date the permalink url parameter
// ----------------------------------------------------------------------------

function baseLayerWasChanged()
{
   // the map type changed - save it
   var mapType = map.getMapTypeId();

   // find the matching url parameter
   var tmp = baseLayerLookup(true, mapType);

   if (tmp !== '')
      mapTypeText = tmp;
   else
      alert('URL parameter not found in look up table');

   //alert(mapTypeText);
}

// ----------------------------------------------------------------------------
// Make a layer switcher
// ----------------------------------------------------------------------------

function layerSwitcher(map, controlDiv, layers)
{
   // see: http://www.w3schools.com/jsref/dom_obj_style.asp
   // set CSS styles for the DIV containing the control
   // Setting padding to 5 px will offset the control
   // from the edge of the map
   controlDiv.style.padding = '5px';

   // scan through the table to find the matching value
   var layerTot = layers.length;
   for (var i=0; i<layerTot; i++)
   {
      // set CSS for the control border
      var layerButton = document.createElement('DIV');
      layerButton.id = ''+layerButton+i;
      layerButton.style.backgroundColor = '#F9F9F9';
      layerButton.style.borderColor = '#A9BBDF';
      layerButton.style.borderStyle = 'solid';
      layerButton.style.borderWidth = '1px';
      layerButton.style.cursor = 'pointer';
      layerButton.style.textAlign = 'center';
      layerButton.title = 'Click to toggle the '+layers[i].name+' layer';

      // add on an extra property to the 'div': the associated layer
      layerButton.layer = layers[i];

      controlDiv.appendChild(layerButton);

      // set CSS for the control interior
      var layerButtonText = document.createElement('DIV');
      layerButtonText.style.fontFamily = 'Arial,sans-serif';
      layerButtonText.style.fontSize = '12px';
      layerButtonText.style.paddingLeft = '4px';
      layerButtonText.style.paddingRight = '4px';

      var status = layers[i].getMap();
      if (status === undefined) {alert("The layer's map is undefined - it should be null or set to the map");}

      // show the overlay status - is it on or off?
      if (status === null)
         layerButtonText.innerHTML = layers[i].name;
      else
         layerButtonText.innerHTML = '<b>'+layers[i].name+'</b>';

      layerButton.appendChild(layerButtonText);

      // setup the click event listener for each list button. This is
      // passed on to the dom, where it effectively becomes a dom listener
      google.maps.event.addDomListener(layerButton, 'click', function(e) {

         //alert(this.firstChild.innerHTML);
         //examineObject(this.layer);

         // is layer enabled on the map?
         // 'this' references the button div that was clicked on
         var layerIsOnTheMap = ((this.layer.getMap() !== null) && (this.layer.getMap() !== undefined));

         // total hack to get the time by bike marker to fit with this control
         var layerIsTimeByBike = (typeof this.layer.ourSetMap !== 'undefined');

         // If the user has enabled the time by bike layer and it's locked in place, we
         // need to move the map to the time by bike marker's location, so they can see it.
         if (layerIsTimeByBike && !layerIsOnTheMap && ringsLocked)
            centerAndZoom(bikeTimeLat, bikeTimeLng, mapZoom);

         // toggle the overlays on and off
         if (!layerIsOnTheMap)  // put it on the map
         {
            if (layerIsTimeByBike)
               this.layer.ourSetMap(map);  // do the markerTimeByBike
            else
               this.layer.setMap(map);     // normal case

            // acccess the button text
            this.firstChild.innerHTML = '<b>'+this.layer.name+'</b>';
         }
         else // remove from map
         {
            if (layerIsTimeByBike)
               this.layer.ourSetMap(null);  // do the markerTimeByBike
            else
               this.layer.setMap(null);     // normal case

            // acccess the button text
            this.firstChild.innerHTML = this.layer.name;
         }
      });
   }

   // this only affects the visibility of the control, not the layers
   this.setVisibility = function(visible)
   {
      if (visible)
         controlDiv.style.display = 'inline';
      else
         controlDiv.style.display = 'none';
   };
}

// ----------------------------------------------------------------------------
// Add in the controls
// ----------------------------------------------------------------------------

function addControls()
{
   // Custom controls are just divs (or other HTML elements). They are added to the map by pushing them
   // on to the appropriate position array. You remove the control by removing it from the same array.
   // eg map.controls[google.maps.ControlPosition.RIGHT_TOP].removeAt(i)

   // create the 'div' to hold the control and then build the control itself
   var layerCtrlDivOz = document.createElement('div');
   var layerCtrlOz = new layerSwitcher(map,layerCtrlDivOz,[pLga,pOsmOnRoadBikeRoute,pOsmOffRoadBikeRoute]);
   layerCtrlDivOz.index = 1;
   map.controls[google.maps.ControlPosition.RIGHT_TOP].push(layerCtrlDivOz);

   var layerCtrlDivNSW = document.createElement('div');
   layerCtrlNSW = new layerSwitcher(map,layerCtrlDivNSW,[pSydneyCampaigns,pSydneyCounters,pSydneyRtaCctv]);
   layerCtrlNSW.index = 2;
   map.controls[google.maps.ControlPosition.RIGHT_TOP].push(layerCtrlDivNSW);

   var layerCtrlDivSA = document.createElement('div');
   layerCtrlSA = new layerSwitcher(map,layerCtrlDivSA,[pAdelaideCampaigns,pAdelaideBakeries]);
   layerCtrlSA.index = 3;
   map.controls[google.maps.ControlPosition.RIGHT_TOP].push(layerCtrlDivSA);

   var layerCtrlDivVic = document.createElement('div');
   layerCtrlVic = new layerSwitcher(map,layerCtrlDivVic,[pMelbourneNavigation,pMelbourneBikeShare,pMelbourneCampaigns,pMelbourneMagpies]);
   layerCtrlVic.index = 4;
   map.controls[google.maps.ControlPosition.RIGHT_TOP].push(layerCtrlDivVic);

   var layerCtrlDivMisc = document.createElement('div');
   layerCtrlMisc = new layerSwitcher(map,layerCtrlDivMisc,[pTimeByBikeMarker,pLayerWeatherRadar]);
   layerCtrlMisc.index = 5;
   map.controls[google.maps.ControlPosition.RIGHT_TOP].push(layerCtrlDivMisc);

   return ftLayer('486711' , 'LGA - check who to call', false);

}

// ----------------------------------------------------------------------------
// Don't name this 'onload' as it's a reserved word in Firefox
// ----------------------------------------------------------------------------

function load()
{
   setUpServer();

   // September 2011 = ver 3.6.5b
   document.getElementById("apiVersionNumber").innerHTML = google.maps.version;

   // get the cookie if there is one, otherwise defaults are set up
   getCookieParms();

   // extract the parms from the URL - these will override the matching parms in the cookie if it's present
   parseParms();

   if (showAboutMsg)
      aboutMsgOpenBtnClick();
   else
      aboutMsgCloseBtnClick();

   // set the speed from the idx held in the cookie
   speed = speeds[speedIdx];

// ----------------------------------------------------------------------------

   var ourMapTypeIds = [];

   // load up the baselayer ids
   var layerTot = baseLayersInfo.length;
   for (var i=0; i<layerTot; i++)
      ourMapTypeIds.push(baseLayersInfo[i][0]);

   // find the map type id from the url parameter
   var tmp = baseLayerLookup(false, mapTypeText);

   var mapType = NEARMAP;  // this should match the default value of mapTypeText

   if (tmp !== '')
      mapType = tmp;
   //else
   //   alert('mapType not found in look up table');

   map = new google.maps.Map(document.getElementById('mapDiv'),
   {
      center: new google.maps.LatLng(0.0, 0.0),
      zoom: 4, //zoom
      mapTypeControlOptions:
      {
         mapTypeIds: ourMapTypeIds,
         style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
      },
      mapTypeId: mapType
   });

   // setup base layers
   baseLayerNearMap();
   baseLayerOsmBike();
   baseLayerOsm();

   accessPanesDummy = new dummyOverlayView(map);

   // Australia wide overlays
   pLga                 = layerLga();
   pOsmOnRoadBikeRoute  = layerOsmOnRoadBikeRoute();
   pOsmOffRoadBikeRoute = layerOsmOffRoadBikeRoute();

   // Sydney specific ovelays
   pSydneyCampaigns     = layerSydneyCampaigns();
   pSydneyCounters      = layerSydneyCounters();
   pSydneyRtaCctv       = layerSydneyRtaCctv();

   // Melbourne specific ovelays
   pMelbourneNavigation = layerMelbourneNavigation();
   pMelbourneBikeShare  = layerMelbourneBikeShare();
   pMelbourneCampaigns  = layerMelbourneCampaigns();
   pMelbourneMagpies    = layerMelbourneMagpies();

   // Adelaide specific ovelays
   pAdelaideCampaigns   = layerAdelaideCampaigns();
   pAdelaideBakeries    = layerAdelaideBakeries();

   // weather radar layer
   pLayerWeatherRadar = layerWeatherRadar();

   // layer for the time by bike circles and marker
   pTimeByBikeMarker = new markerTimeByBike(null, bikeTimeLat, bikeTimeLng, speed);

   if (layerFt0Id !== -1) ftLayer(''+layerFt0Id , 'FT layer: id = '+layerFt0Id, true);
   if (layerFt1Id !== -1) ftLayer(''+layerFt1Id , 'FT layer: id = '+layerFt1Id, true);

   //examineObject(pTimeByBikeMarker);
   //examineObject(map.overlayMapTypes);
   //examineObject(map.overlayMapTypes.getArray()[0]);

   addControls();

   // hook up some map events
   google.maps.event.addListener(map, 'center_changed',    mapWasMoved);
   google.maps.event.addListener(map, 'zoom_changed',      mapWasZoomed);
   google.maps.event.addListener(map, 'maptypeid_changed', baseLayerWasChanged);

   // trigger the map events to get an initial update
   centerAndZoom(mapCenterLat,mapCenterLng,mapZoom);

   document.getElementById("loadingMsg").innerHTML = '';
   document.getElementById("routerBtn").value   = "Start routing";
   document.getElementById("routerBtn").onclick = routerBtnClick;
}

// execute this
window.onload = load;

window.onunload = function()
{
   if (map === null) return;

   setCookieParameters(3);
};

// ----------------------------------------------------------------------------

