/** Defines an itinerary object which provides tools for configuring an itinerary interactively on a journey planner page.
 *
 * Outline of the control flow on the JP page
 * ==========================================
 *
 * Page initialises with the map ready to receive clicks, and the "Choose start point" focus box ready to receive typed place names.
 *
 *
 *  Page Initialises
 *        |
 *        | CS object created
 *        | itinierary object created.
 *        | searchAsYouType attaches to input element.
 *        v
 * itinerary.initialize();
 *        .
 *        . . . . . . . . . . . . . . .
 *        .                           .
 *        v                           v
 *    clickHandler                dragHandler                      search-as-you-type
 * OpenLayers.Control.Click      OpenLayers.Control.DragFeature    select suggestion
 * handleMapClick()              handleMapDrag()
 *        |                           |                                   |
 *        +- - - - - - - - - - - - - -+                                   |
 *        v                                                               |
 * handleMapEvent(Click|Drag)                                             |
 *        |                                                               |
 *        +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+
 *        |
 *        v
 * requestNearestPoint()
 *        |
 *        |
 *        v
 *      ajax()          -->     anticipate(action)
 *        :                     deactivates click
 *        :                     and drag handlers
 *        :
 *        :
 *        v
 *  OpenLayers
 *  request handler     -->     fail()
 *        |
 *        |
 *        v
 *     succeed()       <-->     checkResponse()   --> fail()
 *        |
 *        |
 *        |            <-->     responseNotedIssues()
 *        |
 *        v
 *    despatch  <--+--> respondNearestPointClick(notedIssues) - - - - - -+
 *        |        |                                                     |
 *        |        +--> respondNearestPointDrag(notedIssues)  - - - - - -+
 *        |        |                                                     |
 *        |        +--> plan(notedIssues)                                |
 *        |                                                              v
 *        |                                                     respondNearestPoint()
 *        |                                                              |
 *        |                                                              v
 *        |                                                          addPoint()
 *        |                                                              |
 *        |                                                              v
 *        |                                                           setEnd()
 *        |                                                              |
 *        |                                                              v
 *        v                                                        redrawPanel()
 *  unAnticipate()
 *  reactivates click
 *  and drag handlers
 *
 */
var itinerary = {

  // This method makes this object easier to identify in firebug.
 toString: function () {
    var string = 'Itinerary(' + (this.start ? this.start.attributes.name : '?') + ' ==>> ' + (this.finish ? this.finish.attributes.name : '?') + ')';
    return string;
  },

 // The map widget
 map: false,

  // These are the two ends of the itinerary, which become bound to OpenLayers.Feature objects.
 start:  false,
 finish: false,

 // Click control for adding markers.
 click: null,
 
 // Drag control for moving the markers.
 drag: null,
 
 // Set to the feature being dragged (at the end of the drag).
 draggedFeature: null,
 
 // For reading GML data - gets initialised with map projections.
 formatGML: null,
 
 // This will containg the ajax request object, only used to provide firebug with a way to inspect this object.
 request: null,
 
 // XML Namespaces - see meta.php where these are defined.
 csNS: null,
 gmlNS: null,

 // The centre of the map.
 longitude: 0,
 latitude: 0,

 // The front page version of the JP is expandable, but the full page version is not.
 expandable: false,

 // Whether there is an interactive panel attached.
 panel: false,

 // Vector layers
 markerLayer: null,
 routeLayer: null,
 pathsLayer: null,
 plannedRoutes: {}, 
 selectControl: null,

 // Whether the user has changed the value of the 'when' field.
 userChangedWhence: false,

 // Below this zoom level clicks zoom the map in a bit more.
 minimimZoomForStreetSelection: 12,

 // The distance betwee the ends in metres.
 crow_fly_distance: 0,

 // Set on instantiation.
 max_crow_fly: null,

 pathsLayerIntroductoryHtml: '',

 // Set if the start or finish has moved since the last plan.
 markersMoved: false,

 // Click routing plans a route each time markers are moved.
 clickRouting: false,

 // Sets up the Map to define the itinerary.
 // Args are Namsespaces used to interpret XML returned by AJAX calls.
 initialize: function (csNS, gmlNS, max_crow_fly, crow_fly_distance, longitude, latitude, zoom, expandable, start, finish, panel, pathsLayer, pathsLayerIntroductoryHtml)
 {
    this.csNS  = csNS;
    this.gmlNS = gmlNS;
    this.max_crow_fly = max_crow_fly;
    this.crow_fly_distance = crow_fly_distance;
    this.panel = panel;
    this.pathsLayerIntroductoryHtml = pathsLayerIntroductoryHtml;

    var centre = false;

    // Use a restricted extent (i.e. UK) for journey planning.
    this.map = CS.createMap('map', false, 0, (panel ? true : false), true);

    if(start)  {this.hash2feature(start, 'start');}
    if(finish) {this.hash2feature(finish, 'finish');}

    // Choosing the centre an zoom
    // If neither start nor finish are set, use the parameters given.
    // If only start is set use it as the centre, ditto finish.
    // If both start and finish are set then work out the centre and zoom to accommodate both.

    var startStreet = 0;

    if(this.start && this.finish) {
      
      centre = new OpenLayers.LonLat(0,0);
      
      zoom = this.twoPointsCentreAndZoom(start.longitude, start.latitude, finish.longitude, finish.latitude, centre);
      
      longitude = centre.lon;
      latitude  = centre.lat;
    }

    if(this.start && !this.finish) {
      longitude = start.longitude;
      latitude  = start.latitude;
    } else if(!this.start && this.finish) {
      longitude = finish.longitude;
      latitude  = finish.latitude;
    }

    this.expandable = expandable;
    this.longitude  = longitude;
    this.latitude   = latitude;

    if(!centre) {centre = CS.lonLatToMercator(new OpenLayers.LonLat(longitude, latitude));}

    this.map.setCenter(centre, zoom);

    // This object is used to read the GML objects directly and translates to the map projection.
    this.formatGML =  new OpenLayers.Format.GML({internalProjection: this.map.getProjectionObject(),
	  externalProjection: new OpenLayers.Projection("EPSG:4326")});

    this.setupMarkerLayer();

    // When doing click routing, need to be able to see the route more clearly.
    this.setupRouteLayer(this.clickRouting ? 0.7 : 0.42);

    
    // Conditionally add click and drag drop etc.
    if(this.panel) {
      this.addInteractivity();

      // This should only be available to privileged users.
      if(pathsLayer) {this.setupPathsLayer();}
    }

    // Setup search as you type
    if(searchAsYouType) {this.setupSAYT();}

    //  There is a general library.js method that does a setFocus(); - maybe we should turn that off for this page?
    if(document.forms.streetSearchStart) {document.forms.streetSearchStart.jpStartStreetSearch.focus();}

    // A temporary marker that gets moved around.
    this.temporaryMarker = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(0, 0), {type: 'temporary'});
    
    this.redrawPanel();
    
    // If a start street has been suggested, feed it into the street search.
    /*    if(startStreet) {
      document.forms.streetSearchStart.jpStartStreetSearch.value = startStreet;
      searchAsYouType.cleanedInputChanged(true);
      }*/
    if(this.start) {
      // Rather than use addPoint 
      this.markerLayer.addFeatures(this.start);
    }
    if(this.finish) {
      // Rather than use addPoint 
      this.markerLayer.addFeatures(this.finish);
    }
 },
 
 
 addInteractivity: function ()
 {
   // Add marker dragging (from drag-feature example)
   this.drag = new OpenLayers.Control.DragFeature(this.markerLayer, {
       
     onDrag: function(feature, pixel) {
	 
	 // Cannot use 'this' in this closure.
	 
	 // Make a note of the offset of the mouse from the marker's hotspot.
	 var markerPixel = this.map.getPixelFromLonLat(new OpenLayers.LonLat (feature.geometry.x, feature.geometry.y));
	 feature.attributes.hotspotOffsetX = markerPixel.x - pixel.x;
	 feature.attributes.hotspotOffsetY = markerPixel.y - pixel.y;
	 
	 var end = feature.attributes.end;
	 itinerary.draggedFeature = feature;
	 
	 itinerary.clearEnd(end, false);
       },
	 
	 onComplete: function(feature, pixel) {
	 
	 // Cannot use 'this' in this closure.
	 
	 // Ignore any drags that are not to do with us.
	 if (!feature || feature != itinerary.draggedFeature) {return;}
	 
	 // Add in the offset of the mouse from the marker's hotspot.
	 pixel.x += feature.attributes.hotspotOffsetX;
	 pixel.y += feature.attributes.hotspotOffsetY;
	 
	 // Hide the feature now - the temporary feature will be displayed in its place.
	 itinerary.markerLayer.removeFeatures(feature);
	 
	 itinerary.handleMapDrag(pixel);}
     });
   this.map.addControl(this.drag);
   this.drag.activate();
      
   this.click = new OpenLayers.Control.Click();
   this.map.addControl(this.click);
   this.click.activate();
   
   this.map.events.register("mousedown", null, function (event) {itinerary.mayExpandJPsection();});
   
   this.map.events.register("moveend", null, function (event) {itinerary.mapMoveEnd();});
 },
 
 
 /**
  * Setup searchAsYouType
  */
 setupSAYT: function () {
    searchAsYouType.initialize(CS.baseUrl,
     'jpStartStreetSearch', 'jpStartCue', true,
     function (selectedResult) {
       itinerary.saytComplete(selectedResult.longitude, selectedResult.latitude, selectedResult.name);
     },
     function () {
       // Make sure the JP section is fully visible
       if(itinerary.mayExpandJPsection()) {
	 // If it expanded recalc dimensions
	 searchAsYouType.handleResize();
       }
     },
     // The return value of this 'anticipate callback' determines whether SAYT can issue its ajax requests.
     function () {
       // Try to lock out any clicks or drags on the itinerary page.
       if(!itinerary.anticipate('sayt', true)) {
	 // If the lock couldn't be made then stop, unless the lock is for sayt.
	 if(itinerary.anticipating != 'sayt') {return false;}
       }
       itinerary.mapPrompt(CS.icon_tag('extras/amber') + ' Searching CycleStreets &hellip;');
       return true;
     },
     function () {
       itinerary.unAnticipate();
       itinerary.mapPrompt(CS.icon_tag('extras/go') + ' OK');

       // Experimental - auto selection of the first result.
       // searchAsYouType.selectActiveResult(0);

     });
    },
 
 hash2feature: function(hash, end)
 {
   this[end] = this.makeFeature(hash.longitude, hash.latitude, end, hash.name, hash.island, hash.id);
 },
 
 makeFeature: function (longitude, latitude, end, name, island, id)
 {
   var merc = CS.lonLatToMercator(new OpenLayers.LonLat(longitude, latitude));
   return new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(merc.lon, merc.lat),
  {type: end, end: end, name: name, island: island, id: id, longitude: longitude, latitude: latitude});
 },

 /**
  *
  */
 addPlannedRoute: function (plan, coordinates)
 {
   var feature = this.drawPlanCoordinates(plan, coordinates);
   this.plannedRoutes[plan] = feature;
 },


 selectPlan: function (plan)
 {
   // Do nothing if the plan is not yet present.
   if(!this.plannedRoutes[plan]) {return;}

   // Unselect everything else first
   for(var p in this.plannedRoutes) {
     this.selectControl.unselect(this.plannedRoutes[p]);
   }
   this.selectControl.select(this.plannedRoutes[plan]);
 },
 

 drawPlanCoordinates: function (plan, coordinates)
 {
   var str = coordinates;
   var pointList = str.split(" ");
   var coords;
   var numPoints = pointList.length;
   var points = new Array(numPoints);
   var merc;
   for(var i=0; i<numPoints; ++i) {
   //points[i] = OpenLayers.LonLat.fromString(pointList[i]).transform(this.map.getProjectionObject(), this.map.displayProjection);
     merc = CS.lonLatToMercator(OpenLayers.LonLat.fromString(pointList[i]));
     points[i] = new OpenLayers.Geometry.Point(merc.lon, merc.lat);
   }
   var linestring = new OpenLayers.Geometry.LineString(points);
   var routeFeature = new OpenLayers.Feature.Vector(linestring, {type: 'route'});
   
   var color = this.planColor(plan);
   
   // Draw a thick line showing the route.
   
   // The styleMap for the layer uses this field.
   routeFeature.attributes.routeColor = color;
   routeFeature.type = 'route';
   
   this.routeLayer.addFeatures(routeFeature);

   return routeFeature;
 },
  
 /**
  * Setus up a style lookup table based on...
  * @link http://london.cyclestreets.ibsen/openlayers/examples/styles-unique.html
  */
 setupMarkerLayer: function ()
 {

   // To make sure markers are always in front of shadows.
   var SHADOW_Z_INDEX = 10000;
   var MARKER_Z_INDEX = 11000;

   // create a styleMap with a custom default symbolizer
   var styleMap = new OpenLayers.StyleMap({
       
       // Shadows are explained in ...
       // http://london.cyclestreets.ibsen/openlayers/examples/marker-shadow.html
     backgroundGraphic: CS.baseUrl + '/' + CS.themeLocation + 'wisp_shadow_36x30.png',
	 
	 graphicWidth:  51, // (* 36 1.414)
	 graphicHeight: 42, // (* 30 1.414)
	 
	 // Set the z-indexes of both graphics to make sure the background
	 // graphics stay in the background (shadows on top of markers looks
	 // odd; let's not do that).
	 graphicZIndex:           MARKER_Z_INDEX,
	 backgroundGraphicZIndex: SHADOW_Z_INDEX,
	 
	 // Hotspot     
	 graphicXOffset: -10, // (* 7 1.414)
	 graphicYOffset: -41, // (* 29 1.414)

	 // Offset the background by the same amount.
	 backgroundXOffset: -10,
	 backgroundYOffset: -41
	 });
   
   // create a lookup table with different symbolizers for 0, 1 and 2
   var lookup = {
   start: {
     externalGraphic: CS.baseUrl + '/' + CS.themeLocation + 'green_wisp_36x30.png'
   },
   finish: {
     externalGraphic: CS.baseUrl + '/' + CS.themeLocation + 'red_wisp_36x30.png'
   },
   temporary: {
     externalGraphic: CS.baseUrl + '/' + CS.themeLocation + 'gray_wisp_36x30.png'
   }
   };
   
   
   // add rules from the above lookup table, with the keyes mapped to
   // the "type" property of the features, for the "default" intent
   styleMap.addUniqueValueRules("default", "type", lookup);
   
   this.markerLayer = new OpenLayers.Layer.Vector('Itinerary', {
     styleMap: styleMap,
	 rendererOptions: {yOrdering: false}
	 });
   
   //   this.widgetLayer.addFeatures(features);
   this.map.addLayer(this.markerLayer);
   
 },
 
 hideMarkerLayer: function ()
 {
   this.markerLayer.setVisibility(false);
   this.drag.deactivate();
   this.click.deactivate();
 },
 
 showMarkerLayer: function ()
 {
   this.markerLayer.setVisibility(true);
   this.drag.activate();
   this.click.activate();
 },
 
 /**
  * Setus up a style lookup table based on...
  * @link http://london.cyclestreets.ibsen/openlayers/examples/styles-unique.html
  */
 setupRouteLayer: function (defaultOpacity)
 {
   // create a styleMap with a custom default symbolizer
   var styleMap = new OpenLayers.StyleMap({

                "default": new OpenLayers.Style({
		      strokeOpacity: defaultOpacity,
		      // strokeDashstyle: "dashdot",
		      strokeWidth: 7
                }),
                "select": new OpenLayers.Style({
		      strokeOpacity: 0.42,
		      strokeWidth: 16
                })       
	 });
   
   // create a lookup table with different symbolizers
   var lookup = {
   route: {	   
     strokeColor: "\$\{routeColor\}"
   }
   };
   
   // add rules from the above lookup table, with the keyes mapped to
   // the "type" property of the features, for the "default" intent
   styleMap.addUniqueValueRules("default", "type", lookup);
   
   this.routeLayer = new OpenLayers.Layer.Vector('Routes', {
     styleMap: styleMap,
	 rendererOptions: {yOrdering: false}
     });
   
   // Create a select feature control and add it to the map.
   if(!this.panel) {
     this.selectControl = new OpenLayers.Control.SelectFeature(this.routeLayer, {hover: true, onSelect: onRouteSelect});

     //     this.selectControl.clickFeature = function (feature) {alert("Yah, ok" + feature);}

     this.map.addControl(this.selectControl);
     this.selectControl.activate();
   }


   //   this.widgetLayer.addFeatures(features);
   this.map.addLayer(this.routeLayer);


   },

 /**
  */
 setupPathsLayer: function ()
 {
   // create a styleMap with a custom default symbolizer
   var styleMap = new OpenLayers.StyleMap({

                "default": new OpenLayers.Style({
		      // Paths
		      strokeOpacity: 0.6,
		      // strokeDashstyle: "dashdot",
		      strokeWidth: 7,
		      // Nodes
		      fillOpacity: 0.9,
		      pointRadius: 6
		      }),
                "select": new OpenLayers.Style({
		      strokeOpacity: 1,
		      fillOpacity: 1,
		      pointRadius: 9
		      }),
                "hover": new OpenLayers.Style({
		      strokeOpacity: 1,
		      fillOpacity: 1,
		      pointRadius: 7
		      }),
                "hoverSelect": new OpenLayers.Style({
		      strokeOpacity: 1,
		      fillOpacity: 1,
		      pointRadius: 8
		      })
	 });
   
   // create a lookup table with different symbolizers
   var lookup = {
   path: {	   
     strokeColor: "\$\{color\}"
   },
   point: {
     fillColor: "\$\{snookerColor\}",
     strokeColor: "black",
     strokeWidth: 1,
     strokeOpacity: 0.8
   }
   };
   
   // add rules from the above lookup table, with the keyes mapped to
   // the "type" property of the features, for the "default" intent
   styleMap.addUniqueValueRules("default", "type", lookup);
   
   this.pathsLayer = new OpenLayers.Layer.Vector('Ways (for OSM users)', {

     visibility: false, styleMap: styleMap, rendererOptions: {yOrdering: false},

     // Ratio is the bounding box size relative to the viewport size from which to collate features.
     strategies: [new OpenLayers.Strategy.BBOX({ratio: 1, resFactor: 1})],
       protocol: new OpenLayers.Protocol.HTTP({
			url: CS.baseUrl + "/api/paths.xml",
			// Dummy values to trick ajax into giving all paths.
			params: {zoom: 18, useDom: 1},
			format: this.formatGML})
     });
   
   // Create a select feature control and add it to the map.
   this.selectPathsControl = new OpenLayers.Control.HoverSelectFeature(this.pathsLayer, {onSelect: itinerary.onPathSelect, onUnselect: itinerary.onPathUnSelect});
   // this.selectPathsControl = new OpenLayers.Control.SelectFeature(this.pathsLayer, {onSelect: itinerary.onPathSelect, hover: true, highlightOnly: true});

   //     this.selectControl.clickFeature = function (feature) {alert("Yah, ok" + feature);}

   this.map.addControl(this.selectPathsControl);
   // this.selectPathsControl.activate();
 

   this.pathsLayer.events.on({
       "visibilitychanged": this.manageLayers,
	 scope: this
	 });



   //   this.widgetLayer.addFeatures(features);
   this.map.addLayer(this.pathsLayer);

   // Force an initial draw of the layer
   this.pathsLayer.strategies[0].update({force: true});

   },

 manageLayers: function (e)
 {
   CS.e = e;
   if(!e) {return;}   
   if(!e.object) {return;}   

   if(e.type == "visibilitychanged") {

     if(e.object.visibility) {
       document.getElementById('jpStart').className = 'inactive';
       document.getElementById('jpFinish').className = 'inactive';
       if(this.planRouteButtonsCreated) {
	 document.getElementById('jpPlans').className = 'inactive';
	 document.getElementById('jpOptions').className = 'inactive';
       }
       this.hideMarkerLayer();
       this.selectPathsControl.activate();
       document.getElementById('jpInfo').innerHTML = this.pathsLayerIntroductoryHtml;
     } else {
       document.getElementById('jpInfo').innerHTML = '';
       document.getElementById('jpStart').className = 'focussed';
       this.showMarkerLayer();
       this.selectPathsControl.deactivate();
     }
   }
 },

 onPathSelect: function(feature) {

    // CS.sf = feature;
    var attribs = feature.attributes;
    var pathDescribed = '';

    document.getElementById('jpInfo').innerHTML = attribs.htmlDescription;
  },

 onPathUnSelect: function(feature) {
    document.getElementById('jpInfo').innerHTML = itinerary.pathsLayerIntroductoryHtml;
  },



 // Directs output to the current place on the screen where the user is looking.
 // This can be either of the start/finish panels, or the map.
 prompt: function (message)
 {
   if(!this.panel) {
     alert(message);
     return;
   }
   
   // Default to the map message
   var promptElementId = 'message';
   
   if (document.getElementById('jpStart').className == 'focussed') {
     promptElementId = 'jpStartCue';
   } else if (document.getElementById('jpFinish').className == 'focussed') {
     promptElementId = 'jpFinishCue';
   }
   this.promptAux(promptElementId, message);
 },

 /**
  * Puts the message in to the tag that normally appears below the map.
  */
 mapPrompt: function (message)
 {
  return this.promptAux('message', message);
 },

 promptAux: function (elementID, message)
 {
   var promptElement = document.getElementById(elementID);
   if(!promptElement) {
     alert('Prompt: ' + message);
     return false;
   }
   promptElement.innerHTML = message;
   return true;
 },

 // The lonlat here is in transformed coordinates - usually very big numbers.
 moveTemporaryMarker: function (lonlat)
 {
   this.hideTemporaryMarker();
   this.temporaryMarker.geometry = new OpenLayers.Geometry.Point(lonlat.lon, lonlat.lat);
   this.markerLayer.addFeatures(this.temporaryMarker);
 },

 hideTemporaryMarker: function (lonlat)
 {
    this.markerLayer.removeFeatures(this.temporaryMarker);
 },


 // Called when the middle of the map has moved.
 // Clears the search cache if the middle has moved by more than a set amount.
 mapMoveEnd: function () {

    // The amount of shift allowed in degrees when measured equivalent to latitudinal shift.
    // A town is about 10 miles across, which corresponds to about 0.15 degrees
    var hysteresis = 0.15;

    var newMapCenter = this.map.getCenter().clone().transform(this.map.getProjectionObject(), this.map.displayProjection);

    // Create a bounds object that is used to calculate the size of the offset.
    var bounds = new OpenLayers.Bounds();
    bounds.extend(new OpenLayers.LonLat(this.longitude, this.latitude));
    bounds.extend(newMapCenter);

    var size = bounds.getSize();

    var significant_move = false;
    if (size.h > hysteresis) {
      significant_move = true;
    } else {
      
      // Acount for foreshortening.
      // If the hysteresis is small then we can use either latitude to work out the foreshortening.
      // alert(this.latitude + 'cos=' + Math.cos(this.latitude * Math.PI / 180));
      
      if (size.w * Math.cos(this.latitude * Math.PI / 180) > hysteresis) {
	significant_move = true;
      }
    }

    // Update the center
    this.longitude = newMapCenter.lon;
    this.latitude  = newMapCenter.lat;

    if (!significant_move) {return;}
    
    if(searchAsYouType) {searchAsYouType.clearCache();}

  },
 
 // Widen the initial view on the front page.
 // @return bool True if it expanded, false if nothing changed.
 mayExpandJPsection: function () {
    
    if(!this.expandable) {return false;}
    var jpElement = document.getElementById('journeyplanner');

    if(!jpElement) {return false;}
    if(jpElement.className == 'expanded') {return false;}

    // Scroll to the JP section.
    var el = jpElement;
    // Find the y index
    var top = el.offsetTop;
    if (el.offsetParent) {
      while (el.offsetParent) {
	el = el.offsetParent;
	top += el.offsetTop;
      }
    }
    // Deactivated as it seems to be more annoying than useful.
    // window.scrollTo(0, top);

    // Do it.
    jpElement.className = 'expanded';
    document.getElementById('photomap').className = 'collapsed';
    
    return true;
  },
 
 mayCollapseJPsection: function () {
   
    if(!this.expandable) {return;}
    var jpElement = document.getElementById('journeyplanner');

    if(!jpElement) {return;}
    if(jpElement.className == 'narrow') {return;}

    // Do it.
    document.getElementById('journeyplanner').className = 'narrow';
    document.getElementById('photomap').className = '';
    
  },

 
 // Called by the search-as-you-type system.
 saytComplete: function (longitude, latitude, name) {

    this.requestNearestPoint(longitude, latitude, 'Click', name);

    return false;
  },


 // Ensures the panel displays the right message according to the state of the start and finish fields.

 // The main states are...
 // 
 //  Start  Finish  Start form  Finish form  When form  Note
 //  -----  ------  ----------  -----------  ---------  ----
 //  null   null    focussed    inactive     inactive   This is the starting position.
 //
 //  set    null    complete    focussed     inactive   The most common next position.
 //
 //  set    set     complete    complete     focussed   The most common way of getting to a route.
 //
 //  null   set     focussed    complete     inactive   Rare

 redrawPanel: function () {

    if(!this.panel) {return;}
    
    this.drawStartForm(this.start ? 'complete' : 'focussed');
    this.drawFinishForm(this.finish ? 'complete' : this.start ? 'focussed' : 'inactive');

    // this.prompt("Crow fly = " + this.crow_fly_distance + " metres, max= " + this.max_crow_fly + " metres.");

    // If both ends are now set...
    if(this.start && this.finish) {

      var prompt = '';

      // Check both ends are on the same routing island.
      if(this.start.attributes.island != this.finish.attributes.island) {

	prompt = "<h2>Unrouteable</h2><p class=\"warning\">The selected start and finish points are on isolated parts of the network and cannot be routed.</p><p>Please change the points and try again.</p>";

      }

      if(this.crow_fly_distance > this.max_crow_fly) {
	prompt = "<h2>Too far </h2><p class=\"warning\">The selected start and finish points are too far (" + Math.round(this.crow_fly_distance / 1000) + " km) apart (i.e. more than " + Math.round(this.max_crow_fly / 1000) + " km). This is a limitation we have in place during this beta test phase and we hope to extend the limit in future.</p>";
      }
      if(this.crow_fly_distance < 100) {
	prompt = "<h2>Too close</h2><p class=\"warning\">The selected start and finish points are too close together.</p>";
      }
      
      if(prompt) {
	document.getElementById('jpPrompt').innerHTML = prompt;
	document.getElementById('jpPrompt').className = 'active';
	return;
      } else {
	document.getElementById('jpPrompt').className = 'inactive';
      }
      

      this.ensurePlanRouteButtons();
      document.getElementById('jpPlans').className = 'focussed';
      document.getElementById('jpOptions').className = 'focussed';

      //      document.getElementById('jpQuietestButton').focus();
      document.getElementById('jpPlanButton').focus();

    } else {

      if(this.planRouteButtonsCreated) {
	document.getElementById('jpPlans').className = 'inactive';
	document.getElementById('jpOptions').className = 'inactive';
      }
    }
    
  },


 centerEnd: function (end) {

    end = this[end];
    if(!end) {return;}
    
    this.map.setCenter(CS.lonLatToMercator(new OpenLayers.LonLat(end.attributes.longitude, end.attributes.latitude)), this.map.getZoom());
    
  }, 
 
 drawStartForm: function(state)
 {
    var z = this.map.getZoom();
    var prompt = (z < this.minimimZoomForStreetSelection ? 'Click the map to zoom in, or search' : 'Click on the map or search:');

    switch (state) {
      
    case 'complete':
    document.getElementById('jpStart').className = 'complete';
    document.getElementById('jpStartTitle').innerHTML = 'Starting from:';
    document.getElementById('jpStartStreetSearch').value = this.start.attributes.name;
    document.getElementById('jpStartCue').innerHTML = this.markerTag('green_wisp_shadow_36x30.png') + ' <strong>' + this.start.attributes.name + '</strong> <a href="javascript:void itinerary.clearEnd(\'start\');">change</a>';
    document.getElementById('jpStartStreetSearch').disabled = true;

    //    alert("Completed Value: " + document.getElementById('jpStartStreetSearch').value);


    break;
    
    case 'focussed':

    //   alert("Focussed Value: " + document.getElementById('jpStartStreetSearch').value);

    document.getElementById('jpStart').className = 'focussed';
    document.getElementById('jpStartTitle').innerHTML = 'Choose your start point';
    document.getElementById('jpStartCue').innerHTML = prompt;
    document.getElementById('jpStartStreetSearch').disabled = false;

    
    if(searchAsYouType) {searchAsYouType.moveToField('jpStartStreetSearch', 'jpStartCue', true);}

    /* if(this.startText) {
      document.getElementById('jpStartStreetSearch').value = this.startText;
      document.getElementById('jpStartStreetSearch').select();
      }*/


    break;
    }
    
  },
 
 
 drawFinishForm: function(state) {
    
    switch (state) {
      
    case 'inactive':
    document.getElementById('jpFinish').className = 'inactive';
    document.getElementById('jpFinishTitle').innerHTML = 'Going to:';
    document.getElementById('jpFinishCue').innerHTML = '';
    document.getElementById('jpFinishStreetSearch').disabled = true;
    break;
    
    case 'focussed':
    document.getElementById('jpFinish').className = 'focussed';
    document.getElementById('jpFinishTitle').innerHTML = 'Going to';
    document.getElementById('jpFinishCue').innerHTML = 'Click on the map or search:';
    document.getElementById('jpFinishStreetSearch').disabled = false;
   
    if(searchAsYouType) {searchAsYouType.moveToField('jpFinishStreetSearch', 'jpFinishCue', true);}
    break;
    
    case 'complete':
    document.getElementById('jpFinish').className = 'complete';
    document.getElementById('jpFinishTitle').innerHTML = 'Going to:';
    document.getElementById('jpFinishStreetSearch').value = this.finish.attributes.name;
    document.getElementById('jpFinishCue').innerHTML = this.markerTag('red_wisp_shadow_36x30.png') + ' <strong>' + this.finish.attributes.name + '</strong> <a href="javascript:void itinerary.clearEnd(\'finish\');">change</a>';
    document.getElementById('jpFinishStreetSearch').disabled = true;
    break;
    
    }
  },
 
 // *******************************************************************************************
 // *                                                                                         *
 // *                      Nearest point and dragging markers                                 *
 // *                                                                                         *
 // *                                                                                         *
 // *******************************************************************************************
 
 setEnd: function (end, feature) {
    
    this[end] = feature;
    
    this.mayPanZoom(feature);

    this.redrawPanel();

  },

 clearEnd: function (end, destroy) {

    if(this.impatient(false)) {return false;}

    if(!this[end]) {return;}

    // Default destroy
    if(arguments.length < 2) {destroy=true;}
    
    if(destroy) {
      this[end].destroy();
    }
    
    this[end] = false;

    this.redrawPanel();
  },
 
 /**
  * Unless the zoom >=13 
  * Bring the map to at least zoom 13, with the point in the middle.
  */
 mayPanZoom: function (feature)
 {
   // Zoom 
   if(this.map.getZoom() <= 13) {
     this.map.zoomTo(13);
   }

   // Determine if the point needs to be brought into view.
   var bounds = this.map.getExtent().transform(this.map.getProjectionObject(), this.map.displayProjection);
   
   this.shrinkBounds(bounds, 17);
   
   var centre = new OpenLayers.LonLat(feature.attributes.longitude, feature.attributes.latitude);
   
   // alert(bounds.toBBOX() + ' ' + centre);
   
   if(!bounds.containsLonLat(centre, false)) {
      
     // Set a timeout to do the panning, this allows the marker to appear.
     // Uses the bind function defined in openlayers Basetypes.js to create the closure.
      setTimeout(OpenLayers.Function.bind(this.pan, this, centre), 500);
      
   }
 },
 
 /**
  * Pan the map to the location given. This function was created so that it could be used in timeouts.
  */ 
 pan: function (to) {
    this.map.panTo(CS.lonLatToMercator(to));
  },
 

 /**
  * Decides and sets which end of the route to start or finish.
  */
 addPoint: function (feature) {

    // If both are set already then abandon.
    if(this.start && this.finish) {return;}

    // If the start point has not been set, then we must be setting the finish.
    var end = 'start';
    if(this.start) {end = 'finish';}

    // Open layers use the term 'feature' to describe what looks like a 'marker' in this case.
    feature.attributes.type = end;
    feature.attributes.end = end;

    this.hideTemporaryMarker();
    this.markerLayer.addFeatures(feature);

    this.setEnd(end, feature);
  },



  // *******************************************************************************************
  // *                                                                                         *
  // *                     HTML5 Geolocation API functions                                     *
  // *                                                                                         *
  // *                                                                                         *
  // *******************************************************************************************

	geolocationGetLocation: function () {

		// This is apparently necessary to stop a double-fire of the geolocationSetLocation()
		itinerary.geoLocating = false;

		// Get location no more than 10 minutes old. 600000 ms = 10 minutes.
		navigator.geolocation.getCurrentPosition(this.geolocationSetLocation, this.geolocationShowError, {enableHighAccuracy:true,maximumAge:600000});
	},
	
	geolocationShowError: function (error) {
		// Skip blank error messages.
		if(!error.message) {return;}
		alert('Error: code = ' + error.code + ', message = ' + error.message);
	},
	
	// Centre the map and set the start
	geolocationSetLocation: function (position) {

		// Avoid reentry.
		if(itinerary.geoLocating) {return;}
		itinerary.geoLocating = true;

		// Uncomment this line to confirm that the browser has correctly determined the lat/long of your computer
		// alert ('Latitude: ' + position.coords.latitude + '; Longitude: ' + position.coords.longitude);

		// Place the marker on the map.
		itinerary.requestNearestPoint(position.coords.longitude, position.coords.latitude , 'Click', false);
	},
	
	
 // *******************************************************************************************
 // *                                                                                         *
 // *                      The AJAX request and response system                               *
 // *                                                                                         *
 // *                                                                                         *
 // *******************************************************************************************
 
 // While expecting a reply to an XML query
 anticipating: false,

 
 // Switch the anticipating state, during which drags and clicks are deactivated.
 unAnticipate: function () {
    this.anticipating = false;
    this.drag.activate();
    this.click.activate();
  },
 
 /**
  * Sets the page up to expect a result from a certain action.
  * During this time various events are deactivated.
  * @return bool False on failure.
  */
 anticipate: function (action, quiet) {

    if(!action) {
      alert("No action to anticipate has been given."); 
      return false;
    }

    if(this.impatient(quiet)) {return false;}

    this.anticipating = action;
    this.click.deactivate();
    this.drag.deactivate();

    return true;
  },

 /**
  * Returns true, and (optionally) alerts if the user is being impatient.
  * @param quiet
  * @return bool
  */
 impatient: function (quiet) {
    
    if(this.anticipating) {
      if(!quiet) {alert("The system is anticipating a response to: " + this.anticipating + "\n\nPlease wait until that has finished."); }
      return true;
    }
    return false;
  },

 ajax: function(url, anticipation, prompt)
 {

   // Only allow one ajax call at a time.
   if(!this.anticipate(anticipation)) {return;}

   // Useful for debugging.
   CS.debugtagmsg('<a href="' + url + '" target="_blank">' + url + '</a>');
   
   this.mapPrompt(CS.icon_tag('extras/amber') + ' Searching CycleStreets &hellip;');

   if (prompt) {this.prompt(prompt);}

   // This bit dumps the features in the URL straight onto a new layer and is useful for debugging.
   if(false) {
     var gmlLayer = this.map.addLayer(new OpenLayers.Layer.GML("GML", url, {
	 projection: new OpenLayers.Projection("EPSG:4326")}));
   }
   
   // Defined in openlayers/lib/OpenLayers/Ajax.js
   OpenLayers.loadURL(url, null, this, itinerary.succeed, itinerary.fail);
 },

 /**
  * Called by a successful ajax request.
  * @return void
  */
 succeed: function (request)
 {
   // Verify the response.
   if(!this.checkResponse(request)) {
     this.prompt(CS.icon_tag('delete') + ' The system could not connect with the map server.');
     return;
   }
   
   var notedIssues = false;
   if(this.responseNotedIssues(request)) {
     notedIssues = this.getNotedIssues(request);
     // The detail of the problems should be reported by the despatching functions.
     this.mapPrompt(CS.icon_tag('stop') + ' Note: ' + notedIssues.summary);
   }
   
   // Bound to the return value of each of these despatching functions.
   var success = false;
   switch (this.anticipating) {
     
   case 'nearestPointClick':
     success = this.respondNearestPointClick(request, notedIssues);
     break;
     
     
   case 'nearestPointDrag':
     success = this.respondNearestPointDrag(request, notedIssues);
     break;

   case 'plan':
     success = this.respondPlan(request, notedIssues);
     break;
     
   default:
     alert('Unhandled request: ' + this.anticipating);
     break;     
   }
   
   if(!notedIssues) {
     if(success) {
       this.mapPrompt(CS.icon_tag('extras/go') + ' OK');
     } else {
       this.prompt(CS.icon_tag('exclamation') + ' An unknown error occured. Please refresh the page and try again.');
       this.mapPrompt(CS.icon_tag('stop') + ' not OK');
     }
   }

   // Release the locks to allow further requests.
   this.unAnticipate();

   // !! Not really the right place ?
   this.mayPlanRoute();

 },
 
 
 /**
  * I think this is called by the ajax system if the response.status is not in the range 200-299.
  */
 fail: function (request) {
    this.unAnticipate();
    this.mapPrompt(CS.icon_tag('stop'));

    // Put this prompt after the map prompt in case prompt output is currently directed to the map prompt.
    this.prompt(CS.icon_tag('exclamation') + ' <span title="The return status code from the server was: ' +  request.status + '. ' + request.statusText + '">An unhandled problem interrupted normal processing.' + '</span>');
  },

 

 /**
  * Usually called by all ajax completion functions on success to scrutinize the request response. 
  * If there are detected errors these are reported and the function returns false.
  * Takes the system out of its wait state.
  * @param object request with fields such as status as filled by the AJAX system.
  * @return bool False on failure.
  */
 checkResponse: function (request) 
 {
   this.mapPrompt(CS.icon_tag('extras/amber') + ' Received response to: ' + this.anticipating);

   // Attach to local object to allow easier inspection from a debugger.
   this.request = request;
   
   // OpenLayers considers a null request.status as success (case it adopts for handling a file protocol (xhr)).
   // See: http://trac.openlayers.org/ticket/1638
   // However we should treat it as a failure.
   if(!request.status) {
     this.fail(request);
     return false;
   }
      
   return true;
 },
 
 
 /**
  * Determine whether there are any notedIssues in the response.
  * @return bool False if there are none.
  */
 responseNotedIssues: function (request)
 {
   return this.formatGML.getElementsByTagNameNS(request.responseXML, this.csNS, 'notedIssue').length;
 },
 
 /**
  * The cyclestreets ajax server can send several notedIssues back, even among other valid data.
  * This assembles a list of the user-friendly texts in those notedIssues.
  *
  * @return A hash table containing the extracted results.
  */
 getNotedIssues: function (request)
 {
   // Detect known error situations.
   var notedIssueNodes = this.formatGML.getElementsByTagNameNS(request.responseXML, this.csNS, 'notedIssue');

   // This is the object that will be returned, containing arrays of the contents of the issues.
   var notedIssues = { text: [], details: [] };
   
   // this.notedIssueNodes = notedIssueNodes;
   if(notedIssueNodes.length > 0) {
     
     var notedIssueDetails;
     for(var i=0; i < notedIssueNodes.length; ++i) {
       
       var node = notedIssueNodes[i];
       notedIssueDetails = {};
       
       var childNode = node.firstChild;
              while(childNode) {
	 if(childNode.nodeType == 1 && childNode.firstChild) {
	   
	   // Clump the user friendly texts together, putting everything else in the details field.
	   if(childNode.localName == 'text') {
	     notedIssues.text.push(childNode.firstChild.nodeValue);
	   } else {
	     notedIssueDetails[childNode.localName] = childNode.firstChild.nodeValue;
	   }
	 }
	 
	 childNode = childNode.nextSibling;
       }
	 notedIssues.details.push(notedIssueDetails);
     }
     // Note these other techniques that might be considered:
     // pieces.push( this.formatGML.read(notedIssueNodes[i]));
     // pieces.push(this.formatGML.concatChildValues(notedIssueNodes[i]));
   }

   // Summarise the friendly messages into a convenient single string.
   notedIssues.summary = notedIssues.text.join(' ');

   return notedIssues;
 },
 

 getPointFeature: function (request) {

    var feature = false;

    // Parse the XML into GML nodes.
    this.features = this.formatGML.read(request.responseText);

    return this.getFeature('point');
  },

 respondNearestPointClick: function (request, notedIssues)
 {
   // Clean up on error
   if(notedIssues) {
     this.prompt(CS.icon_tag('exclamation') + ' ' + notedIssues.summary);

     // !! Some errors are informative.
     // this.hideTemporaryMarker();
     // return false;
   }
   
   return this.respondNearestPoint(request, notedIssues);
 },
 
 respondNearestPointDrag: function (request, notedIssues)
 {
   if(!this.draggedFeature) {alert("The dragged feature has gone missing.");return false;}
   
   var end = this.draggedFeature.attributes.end;
   this.draggedFeature = null;
   
   if(notedIssues) {
     
     this.prompt(CS.icon_tag('exclamation') + ' ' + notedIssues.summary);
     
     // clean up.
     this.hideTemporaryMarker();
     this.clearEnd(end, true);
     
     return false;
   }
   
   return this.respondNearestPoint(request, notedIssues);
 },

 
 respondNearestPoint: function (request, notedIssues)
 {
   // The temporary marker is placed where the user clicked.
   // If a valid location has not come back hide it now.
   var feature = this.getPointFeature(request);
   if(!feature) {
     // If there are noted issues they will already be displayed, but if not show this prompt.
     if(!notedIssues) {
       this.mapPrompt(CS.icon_tag('exclamation') + ' An invalid response was received from the map server.');
     }
     this.hideTemporaryMarker();
     return false;
   }
   
   // If this information comes back set it into itinerary field.
   if(feature.attributes.crow_fly_distance) {
     // alert("The crow flies: " + feature.attributes.crow_fly_distance + " metres.");
     this.crow_fly_distance = feature.attributes.crow_fly_distance;
   }

   this.addPoint(feature);
   this.markersMoved = true;
   return true;
 },
 
 
 handleMapClick: function (pixel) {this.handleMapEvent(pixel, 'Click');},
 
 handleMapDrag:  function (pixel) {this.handleMapEvent(pixel, 'Drag');},
 
 handleMapEvent: function (pixel, eventType)
 {
   this.mayExpandJPsection();
   
   // if both ends set, this does nothing more.
   if(this.start && this.finish) {return;}

   var lonlat = this.map.getLonLatFromPixel(pixel);

   // Zoom if too far out
   if(this.map.getZoom() < this.minimimZoomForStreetSelection) {

     this.map.setCenter(lonlat, this.minimimZoomForStreetSelection);
     return;
   }
   
   
   this.moveTemporaryMarker(lonlat);
   
   if (this.map.displayProjection) {
     lonlat.transform(this.map.getProjectionObject(), this.map.displayProjection );
   }
   
   this.requestNearestPoint(lonlat.lon, lonlat.lat, eventType, false);
 },
 
 
 /**
  * Sets up the ajax system to request nearest point to the arguments.
  */
 requestNearestPoint: function (longitude, latitude, eventType, name)
 {
   var url =  CS.baseUrl + '/api/nearestpoint.xml?layer=' + CS.layer + '&useDom=1&longitude=' + longitude + '&latitude=' + latitude;

   if(name) {url += '&name=' + name;}

   // Include details of the other end, so that crow_fly_distance can be returned.
   var end = (this.start ? this.start : this.finish);
   if(end) {
     url += '&other_longitude=' + end.attributes.longitude + '&other_latitude=' + end.attributes.latitude;
   }
   
   var prompt = CS.icon_tag('magnifier') + ' Looking for nearest point on the cycle network...';
   
   this.ajax(url, 'nearestPoint' + eventType, prompt);
 },
  
 
  // *******************************************************************************************
  // *                                                                                         *
  // *                      Planning a route                                                   *
  // *                                                                                         *
  // *                                                                                         *
  // *******************************************************************************************


 planRouteButtonsCreated: false,

 ensurePlanRouteButtons: function()
 {
    // Only create them once - i.e. make this idem potent
    if(this.planRouteButtonsCreated) {return;}
    this.planRouteButtonsCreated = true;

    var timenow = new Date();
    var h = timenow.getHours();
    var m = timenow.getMinutes();
    m=5 * Math.floor(m/5);
    if(m>=55) {
      m = 0;
      h=(h ==23 ? 0 : h+1);
    } else {
      m=m+5;
    }
    var hours_html = '<select id="hour"' + this.touch('leavingArrivingLI') + '>';
    for(var hour = 0; hour < 24; hour++) {
      hours_html += '<option value ="' + hour + '"' + (hour==h ? ' selected="selected"': '') + '>' + hour + '</option>';
    }
    hours_html+= '</select>';
    
    var minutes_html='<select id="minute"' + this.touch('leavingArrivingLI') + '>';
    for(var minute = 0; minute < 60; minute = minute + 5) {
      minutes_html+= '<option value="' + minute + '"' + (minute==m ? ' selected="selected"': '') + '>' + (minute<10 ? '0': '') + minute + '</option>';
    }
    minutes_html+= '</select>';

    // var whence_html = ' at ' + hours_html + ':' + minutes_html + ' (hh:mm)';
    var whence_html = ' <input type="text" id="whence" value="now" onchange="javascript: void itinerary.whenceChanged()" />';


    var selected_speed =20;
    
    // Note the (slightly painful) use of \ in the following html which is split over several lines for readability.
    // http://www.nczonline.net/archive/2006/12/403
    var html = '\
<form id="itinerary" class="inactive" action="javascript: void itinerary.plan();">\
<ul class="nobullet spaced"><li id="leavingArrivingLI" class="untouched"><select id="event" onchange="javascript: void itinerary.changeEvent()"' + this.touch('leavingArrivingLI') + '">\
	<option value ="Leaving">Leaving</option>\
	<option value ="Arriving">Arriving</option>\
</select>' + whence_html + '</li>\
<li id="speedLI" class="untouched">Speed <select id="speed"' + this.touch('speedLI') + '/>\
	<option value ="16"' + (selected_speed==16 ? ' selected="selected"' : '') + '>Unhurried 10mph (16km/h)</option>\
	<option value ="20"' + (selected_speed==20 ? ' selected="selected"' : '') + '>Cruising 12mph (20km/h)</option>\
	<option value ="24"' + (selected_speed==24 ? ' selected="selected"' : '') + '>Quick 15mph (24 km/h)</option>\
</select></li></ul>\
</form>\
';

    document.getElementById('jpOptions').innerHTML = html;

  },

 // Helper function used in building the JP options form, so that touched widgets call through a common method 'touched'.
 touch: function(tagId) {
    return ' onclick="itinerary.touched(' + "'" + tagId + "'" + ')"';
},

 // Called when a JP options widget changes.
 touched: function(x) {
    var element= document.getElementById(x);
    if(!element) {
      alert('touched could not find element named: ' + x);
      return;
    }
    element.className = 'touched';
},
 
 whenceChanged: function() {
    this.userChangedWhence = true;
  },
 
 changeEvent: function () {
    if(this.userChangedWhence) {return;}
    document.getElementById('whence').value = (document.getElementById('event').options.selectedIndex == 1 ? '1 hour' : 'now');
  },

 plan: function ()
 {
   var plan = 'newItinerary';

   // Currently start_point / finish_point are ignored by the planner, but this needs fixing, as it is bonkers to throw away that working out.
   // The rule should be that if the points are supplied they beat the long,lat.
   var queryString = '?useDom=1&layer=' + CS.layer + '&plan=' + plan + '&start_point=' + this.start.attributes.id + '&finish_point=' + this.finish.attributes.id;
   
   // Use of start / finish longitudes and latitudes is now preferred over start_point / finish_point.
   // var queryString = '?useDom=1&layer=' + CS.layer + '&plan=' + plan + '&start_longitude=' + this.start.attributes.longitude + '&start_latitude=' + this.start.attributes.latitude + '&finish_longitude=' + this.finish.attributes.longitude + '&finish_latitude=' + this.finish.attributes.latitude;
   
   if (document.getElementById('whence')) {
     queryString += '&event=' + (document.getElementById('event').options.selectedIndex == 1 ? 'arrive' : 'depart');
     queryString += '&whence=' + document.getElementById('whence').value;
   }
   
   // Options
   var speed = document.getElementById('speed').value;
   // Now effectively ignored
   var dismount = 0;//(document.getElementById('dismount').checked ? 1 : 0);
   
   queryString += '&speed=' + speed + '&dismount=' + dismount;
  
   queryString += '&start=' + encodeURI(this.start.attributes.name) + '&finish=' + encodeURI(this.finish.attributes.name);
   queryString += '&start_longitude=' + encodeURI(this.start.attributes.longitude) + '&finish_longitude=' + encodeURI(this.finish.attributes.longitude);
   queryString += '&start_latitude=' + encodeURI(this.start.attributes.latitude) + '&finish_latitude=' + encodeURI(this.finish.attributes.latitude);
   
   window.location = CS.baseUrl + '/journey/plan.html' + queryString;
 },

 /**
  * Try to bring the route into view.
  */
 panToStartFinish: function ()
 {
   
   if(!this.start || !this.finish) {return;}
   
   var centre = new OpenLayers.LonLat(0,0);
   var zoom = this.twoPointsCentreAndZoom(this.start.attributes.longitude, this.start.attributes.latitude, this.finish.attributes.longitude, this.finish.attributes.latitude, centre);
   
   this.map.zoomTo(zoom);
   this.map.panTo(centre);
   
 },
 /**
  * Try to bring the route into view.
  */
 focusMap: function (lon1, lat1, lon2, lat2)
 {
   var centre = new OpenLayers.LonLat(0,0);
   var zoom = this.twoPointsCentreAndZoom(lon1, lat1, lon2, lat2, centre);
   
   this.map.zoomTo(zoom);
   this.map.panTo(centre);
   
 },

  /**
   * Given the lon and lat points fills centre and returns zoom level that should ensure both points are visible.
   */
  twoPointsCentreAndZoom: function (lon1, lat1, lon2, lat2, centre)
  {
    bounds = new OpenLayers.Bounds();
    bounds.extend(CS.lonLatToMercator(new OpenLayers.LonLat(lon1, lat1)));
    bounds.extend(CS.lonLatToMercator(new OpenLayers.LonLat(lon2, lat2)));
    // bounds.toBBOX(); // returns 4,5,5,6
    
    //This gives a small margin around the whole route.
    this.shrinkBounds(bounds, -17);
    
    var boundsCentre = bounds.getCenterLonLat();
    centre.lon = boundsCentre.lon;
    centre.lat = boundsCentre.lat;
    
    return this.map.getZoomForExtent(bounds, false);
  },
  

 /**
  * Shrink the bounds a bit.
  * The bounds are made narrower by a margin on both sides
  * The margin size is calculated as width/amount and height/amount.
  * Specify a negative amount to grow the bounds.
  */
 shrinkBounds: function (bounds, amount) {

    var margin = (bounds.right - bounds.left) / amount;
    bounds.left  += margin;
    bounds.right -= margin;
    
    margin = (bounds.top - bounds.bottom) / amount;
    bounds.bottom += margin;
    bounds.top    -= margin;

    return bounds;
  },
  
 respondPlan: function (request, notedIssues)
 {
   // Make the animation disappear
   if(document.getElementById('jpAnimation')) {document.getElementById('jpAnimation').className = 'inactive';}
   
   // Returned issues don't necessarily mean a route was not computed.
   if(notedIssues) {
     this.prompt(CS.icon_tag('exclamation') + ' ' + notedIssues.summary);
   }
   
   this.drawRoute(request, notedIssues.summary);
   
   return true;
 },
 
 
 failDrawRoute: function (request) {},
 drawRoute: function (request, notedIssuesSummary)
 {

   // Add the features
   this.features = this.formatGML.read(request.responseText);
   
   var feature = this.getFeature('route');
   if(!feature) {
     this.prompt(CS.icon_tag('exclamation') + ' No route. ' + notedIssuesSummary);
     return false;
   }

   // Choose a color for the route based on the plan.
   var routeFeature = feature;
   
   this.addRouteSummary(routeFeature);
   
   var color = this.planColor(routeFeature.attributes.plan);
   
   // Draw a thick line showing the route.
   
   // The styleMap for the layer uses this field.
   routeFeature.attributes.routeColor = color;
   routeFeature.type = 'route';

   // Remove any existing route.
   this.routeLayer.removeFeatures(this.routeLayer.features);

   this.routeLayer.addFeatures(routeFeature);
   
 },
 
 planColor: function (plan)
 {
   var color;
   switch (plan) {
   case 'quietest':
     color = "#00CC00";
     break;
   case 'fastest':
     color = "#CC0000";
     break;
   case 'shortest':
     color = "#5555DD";
     break;
   case 'manchest':
     color = "#d09800";
     break;
   default:
     color = "#000000";
     break;
   }
 return color;
 },


 /**
  * Used by click routing to display a short summary of the planned route.
  */
 addRouteSummary: function(routeFeature)
 {
   // Make the animation visible
   if(document.getElementById('jpAnimation')) {document.getElementById('jpAnimation').className = 'inactive';}
   
   var url = CS.baseUrl + '/journey/' + routeFeature.attributes.itinerary + '/';
   
   var html = '';
   html += '<h1>Route #' + routeFeature.attributes.itinerary + '</h1>';
   html += '\n<p>Showing ' + routeFeature.attributes.plan + ' <acronym title="Based on ' + routeFeature.attributes.layer + ' data">known</acronym> route from:</p>';
   html += '\n<p>' + this.oldmarkerTag('green', 15) + '  <strong>' + routeFeature.attributes.start + ' </strong> to<br />' + this.oldmarkerTag('red', 15) + ' <strong>' + routeFeature.attributes.finish + ' </strong></p>';
   html += '\n<p>Leaving: ' + routeFeature.attributes.leaving + '<br />Arriving: ' + routeFeature.attributes.arriving + '</p>';
   html += '\n<p class="routedetail"><a href="' + url + '">See this route in detail &raquo;</a></p>';
   
   document.getElementById('jpInfo').innerHTML = html;
   document.getElementById('jpInfo').title = "It took " + routeFeature.attributes.cpu + " seconds to find the route";
   
   // Redirect
   // window.location = url;
 },

 /**
  * New function to handle dynamic route planning.
  */
 mayPlanRoute: function ()
 {
   // Is this feature available?
   if(!this.clickRouting) {return;}

   // Both ends are needed.
   if(!this.start || !this.finish) {return;}

   var plan = 'balanced';

   var queryString = '?useDom=1&layer=' + CS.layer + '&plan=' + plan + '&start_point=' + this.start.attributes.id + '&finish_point=' + this.finish.attributes.id;

   // !! Deprecate these, as we have the point ids.
   queryString += '&start_longitude=' + encodeURI(this.start.attributes.longitude) + '&finish_longitude=' + encodeURI(this.finish.attributes.longitude);
   queryString += '&start_latitude=' + encodeURI(this.start.attributes.latitude) + '&finish_latitude=' + encodeURI(this.finish.attributes.latitude);

   // If the markers have moved, plan a route.
   if(!this.markersMoved) {return;}
   this.markersMoved = false;

   var prompt = CS.icon_tag('magnifier') + ' Planning ' + plan + ' route...';
   
   var url = CS.baseUrl + '/api/journey.xml' + queryString;

   this.ajax(url, 'plan', prompt);
   
   // Make the animation visible
   if(document.getElementById('jpAnimation')) {document.getElementById('jpAnimation').className = 'focussed';}
   
   // Pan now, so it looks like something is happening.
   // Not helpful with clickRouting.
   // this.panToStartFinish();

 },


  // *******************************************************************************************
  // *                                                                                         *
  // *                     Helper Functions                                                    *
  // *                                                                                         *
  // *                                                                                         *
  // *******************************************************************************************


 markerTag: function(name) {
    return '<img class="marker_tag" src="' + CS.baseUrl + '/' + CS.themeLocation + name + '" alt="' + name + ' marker" />';
  },

 debug: function() {
    alert('Redraws the panel');
    this.redrawPanel();
  },

 /**
  * Deprecated
  */
 oldmarkerTag: function(color, size) {
    return '<img class="marker_tag" src="' + CS.baseUrl + '/' + CS.themeLocation + 'mm_' + size + '_' + color + '.png" alt="' + color + ' marker" />';
  },


 // Seeks the first feature with the requested featureType.
 getFeature: function (featureType) {

    var feature = false;

    for(var i=0; i < this.features.length; i++) {
      
      feature = this.features[i];
      
      // Look for the cs:route tag under the gml.
      // [An alternative would be to use: feature.attributes.type == 'route']
      if(feature.gml && feature.gml.featureType == featureType) {break;}
      feature = false;
    }
    return feature;
  }
};



/**
 * Define a click control class, which requests the nearest point.
 * @see this same function in olmapwidget.js
 */

OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {                
  defaultHandlerOptions: {
      'single': true,
	'double': false,
	// This allows there to be a little 'hysteresis' on the click. I.e. a little drag on click will be interpreted as a click rather than a drag, which is kinder to twitchy hands.
	'pixelTolerance': 6,
	'stopSingle': false,
	'stopDouble': false
	},
      
      initialize: function(options) {
      this.handlerOptions = OpenLayers.Util.extend({}, this.defaultHandlerOptions);
      OpenLayers.Control.prototype.initialize.apply(this, arguments); 
      this.handler = new OpenLayers.Handler.AbsorbClick(this, {'click': this.trigger}, this.handlerOptions);
    },
      
      trigger: function(e) {itinerary.handleMapClick(e.xy);}
    
  });

////Ends
