/** Defines an itinerary object which provides tools for configuring an itinerary interactively on a journey planner page.
 * 
 * 
 * NOTE: a minified version of this is used on www. If you change the code update the minified version too using:
 * http://www.minifyjavascript.com/
 * 
 * 
 *
 * 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
 *        | itinerary 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;
  },

  // These are the two ends of the itinerary, which become bound to OpenLayers.Feature objects.
 start:  false,
 finish: false,

 // Contains all the waypoint features on the itinerary, including start and finish.
 waypoints: false,

 // In kilometres per hour - set on initialization.
 speed: 20,

 // 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
 csNS: 'http://www.cyclestreets.net/schema/xml/',
 gmlNS: 'http://www.opengis.net/gml',

 // 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.
 minimumZoomForStreetSelection: 13,

 // 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,

 // If set, names the route to plan each time markers are moved.
 clickRouting: false,

 // If set this can generate /journey/from/lat,lon/ links in the start panel.
 generateCustomLink: false,

 // Key to use for xml services.
 apiKey: 'invalidKey',

 // #hoverPlay This mode was added to allow testing of the new nearestPoint function. It should be false for production use.
 hoverPlay: false,

 // Sets up the Map to define the itinerary.
 // Args are Namsespaces used to interpret XML returned by AJAX calls.
 initialize: function (longitude, latitude, zoom, expandable, waypoints, speed, panel, pathsLayerIntroductoryHtml)
 {
		// Harsh sanity check to make sure the map exists already
		/*global CS */
		if(!CS || !CS.map) {
			alert('Itinerary layer: No map has been initialised.');
			return;
		}
		
    this.panel = panel;

    var centre = false;

     // Endpoints are now passed in via the waypoints argument. This will bind the this.start and this.finish properties.
    if(waypoints) {this.waypoints2features(waypoints);}

    // Choosing the centre and zoom
    // If both start and finish are set then work out the centre and zoom to accommodate both.
    if(this.start && this.finish) {

      centre = new OpenLayers.LonLat(0,0);
      zoom   = this.twoPointsCentreAndZoom(this.start.attributes.longitude, this.start.attributes.latitude, this.finish.attributes.longitude, this.finish.attributes.latitude, centre);

      longitude = centre.lon;
      latitude  = centre.lat;

    } else if (this.start && !this.finish) {

      // If only start is set use it as the centre.
      longitude = this.start.attributes.longitude;
      latitude  = this.start.attributes.latitude;

    } else if (!this.start && this.finish) {

      // If only finish is set use it as the centre.
      longitude = this.finish.attributes.longitude;
      latitude  = this.finish.attributes.latitude;
    }

    // If neither start nor finish are set, use the parameters given.
    this.longitude  = longitude;
    this.latitude   = latitude;

    // Apply the calucated area.
    if(!centre) {centre = CS.lonLatToMercator(new OpenLayers.LonLat(longitude, latitude));}
    CS.map.setCenter(centre, zoom);

    // The front page is expanable.
    this.expandable = expandable;

    // Bind the speed
    this.speed = speed;

    // This object is used to read the GML objects directly and translates to the map projection.
    this.formatGML =  new OpenLayers.Format.GML({internalProjection: CS.map.getProjectionObject(),
	  externalProjection: new OpenLayers.Projection("EPSG:4326")});

    // When doing click routing, need to be able to see the route more clearly.
    this.setupRouteLayer(this.clickRouting ? 0.7 : 0.42);

    // Add the markers layer after the routes layer so that markers are always on top of the routes.
    this.setupMarkerLayer();

    // Conditionally add click and drag drop etc.
    if(this.panel) {
      this.addInteractivity();

      // This should only be available to privileged users.
      if(pathsLayerIntroductoryHtml) {
	  this.pathsLayerIntroductoryHtml = pathsLayerIntroductoryHtml;
	  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);
      }*/

    // Add any waypoint features
    if(this.waypoints) {
	// Must use this way of iterating through javascript arrays, do not make the mistake of using for(w in ...)
	var waypointsLength = waypoints.length;
	for(var w = 0; w < waypointsLength; w++) {
	    if(!this.waypoints[w]) {continue;}
	    this.markerLayer.addFeatures(this.waypoints[w]);
	}
    }
 },

 // When the panel is used for setting up an itinerary, these are needed.
 setCrowFlyParameters: function (min_crow_fly, max_crow_fly, crow_fly_distance)
 {
    this.min_crow_fly = min_crow_fly;
    this.max_crow_fly = max_crow_fly;
    this.crow_fly_distance = crow_fly_distance;
 },
 
 setApiKey: function (apiKey) {
    this.apiKey = apiKey;
  },
 setClickRouting: function (value) {
    this.clickRouting = value;
  },
 setGenerateCustomLink: function (value) {
    this.generateCustomLink = value;
  },
 
 addInteractivity: function ()
 {
   // Add marker dragging (from drag-feature example)
   this.drag = new OpenLayers.Control.DragFeature(this.markerLayer, {
       
     onDrag: function(feature, pixel) {

	 // Make a note of the offset of the mouse from the marker's hotspot.
	 var markerPixel = CS.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;
	 
	 // Cannot use 'this' in this closure.
	 itinerary.draggedFeature = feature;
	 
       },
	 
	 onComplete: function(feature, pixel) {
	 	 
	 // 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);
	 
	 // Clear the end, but don't redraw the panels as a new search will be triggered that will redraw it.
	 itinerary.clearEnd(feature.attributes.end, false, false);

	 itinerary.handleMapDrag(pixel);}
     });
   CS.map.addControl(this.drag);
   this.drag.activate();

   // The examples/highlight-feature.html example suggests the use of separate controls for highlighting and selecting the features.
   this.highlightWispsControl = new OpenLayers.Control.SelectFeature(this.markerLayer, {hover: true, highlightOnly: true});
   CS.map.addControl(this.highlightWispsControl);
   this.highlightWispsControl.activate();

   // Add a handler for clicks - which sets the point down.
   this.click = new OpenLayers.Control.Click();
   CS.map.addControl(this.click);
   this.click.activate();

   CS.map.events.register("mousedown", null, function (event) {itinerary.mayExpandJPsection();});
   
   CS.map.events.register("moveend", null, function (event) {itinerary.mapMoveEnd();});

   // Code based on hover-handler.html example
   if(this.hoverPlay) {
            OpenLayers.Control.Hover = OpenLayers.Class(OpenLayers.Control, {
                defaultHandlerOptions: {
                    'delay': 500,
                    'pixelTolerance': null,
                    'stopMove': false
                },

                initialize: function(options) {
                    this.handlerOptions = OpenLayers.Util.extend(
                        {}, this.defaultHandlerOptions);
                    OpenLayers.Control.prototype.initialize.apply(
                        this, arguments); 
                    this.handler = new OpenLayers.Handler.Hover(
                        this,
                        {'pause': this.onPause, 'move': this.onMove},
                        this.handlerOptions);
                }, 

                onPause: function(evt) {
		  // var output = document.getElementById(this.key + 'Output');
		  // var msg = 'pause ' + evt.xy;
		  // output.value = output.value + msg + "\r\n";
		  // alert(msg);
		  itinerary.handleMapClick(evt.xy);
                },

                onMove: function(evt) {
                    // if this control sent an Ajax request (e.g. GetFeatureInfo) when
                    // the mouse pauses the onMove callback could be used to abort that
                    // request.
                }
            });

   this.hover = new OpenLayers.Control.Hover({
                        handlerOptions: {
                            'delay': 172
			      }
     });
   CS.map.addControl(this.hover);
   this.hover.activate();
   }
 },
 
 
 /**
  * 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);

     });
    },
 
 waypoints2features: function(waypoints)
 {
     // Convert all the waypoints into features.
     this.waypoints = new Array();
     var waypointsLength = waypoints.length;
     // Must use this way of iterating through javascript arrays, do not make the mistake of using for(w in ...)
     for(var w = 0; w < waypointsLength; w++) {
	 this.waypoints.push(this.hash2feature(waypoints[w], 'waypoint'));
     }

     // Bind the special start feature.
     this.start  = this.waypoints[0];//this.hash2feature(waypoints[0], 'start');
     if(this.start) {
	 this.start.attributes.type = 'start';
	 this.start.attributes.end  = 'start';
     }
     // Bind the special finish feature.
     var lastWaypoint = waypoints.length - 1;
     this.finish = this.waypoints[lastWaypoint];//this.hash2feature(waypoints[1], 'finish');
     if(this.finish) {
	 this.finish.attributes.type = 'finish';
	 this.finish.attributes.end  = 'finish';
     }
 },
 
    hash2feature: function(hash, end)
 {
     return hash ? this.makeFeature(hash.longitude, hash.latitude, end, hash.name, hash.island, hash.id) : false;
 },
 
 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});
 },

 /**
  * Also called from journeyPlannerPage::journeyRetrieveAndDisplay()
  */
 addPlannedRoute: function (plan, coordinates)
 {
   var feature = this.drawPlanCoordinates(plan, coordinates);
   this.plannedRoutes[plan] = feature;

   // Make sure the route is visible (actually all data on that layer)
   // This was Shaun's tip https://twitter.com/#!/smsm1/status/131172539518029824
   CS.map.zoomToExtent(this.routeLayer.getDataExtent());
 },


 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(CS.map.getProjectionObject(), CS.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);
   
   // The styleMap for the layer uses this field.
   routeFeature.attributes.routeColor = color;
   routeFeature.attributes.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 ()
 {
   // create a styleMap with a custom default symbolizer
   var styleMap = new OpenLayers.StyleMap({

       "default": new OpenLayers.Style({
	   
	   // Shadows are explained in ...
	   // http://london.cyclestreets.ibsen/openlayers/examples/marker-shadow.html
	 backgroundGraphic: CS.baseUrl + CS.themeLocation + 'wisp_shadow_51x43.png',
	     
	     graphicWidth:  51,
	     graphicHeight: 43,
	     
	     // 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:           11,
	     backgroundGraphicZIndex: 10,
	     
	     // Hotspot     
	     graphicXOffset: -10, // (* 7 1.414)
	     graphicYOffset: -41, // (* 29 1.414)
	     
	     // Offset the background by the same amount.
	     backgroundXOffset: -10,
	     backgroundYOffset: -41
	     }),
	 
	 "select": new OpenLayers.Style({

	   externalGraphic: CS.baseUrl + CS.themeLocation + 'blue_wisp_51x43.png',
	 backgroundGraphic: CS.baseUrl + CS.themeLocation + 'wisp_dark_shadow_51x43.png'

	       }),

	 "hover": new OpenLayers.Style({
	       }),
	 "hoverSelect": new OpenLayers.Style({
	       })
	 });

   // create a lookup table with different symbolizers for 0, 1 and 2
   var lookup = {
   start: {
     externalGraphic: CS.baseUrl + CS.themeLocation + 'green_wisp_51x43.png'
   },
   waypoint: {
     externalGraphic: CS.baseUrl + CS.themeLocation + 'amber_wisp_51x43.png'
   },
   finish: {
     externalGraphic: CS.baseUrl + CS.themeLocation + 'red_wisp_51x43.png'
   },
   temporary: {
     externalGraphic: CS.baseUrl + CS.themeLocation + 'gray_wisp_51x43.png'
   }
   };

   // Add a special style.
   if(this.hoverPlay) {
     // Slightly weird name to indicate this is a rather temporary hack - originally intended only for testing.
     lookup['zutOrangeDot'] = {
       //     externalGraphic: CS.baseUrl + CS.themeLocation + 'blue_wisp_36x30.png',
     backgroundGraphic: null,
     pointRadius: 6,
     fillColor: "#ffcc66",
     strokeColor: "#ff9933",
     strokeWidth: 2,
     graphicZIndex: 1
     };
   }
   
   // add rules from the above lookup table, with the keys 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, zIndexing: true}
	 });
   
   //   this.widgetLayer.addFeatures(features);
   CS.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 a rule from the above lookup table, with the keys 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: this.onRouteSelect */});

     //     this.selectControl.clickFeature = function (feature) {alert("Yah, ok" + feature);}

     CS.map.addControl(this.selectControl);
     this.selectControl.activate();
   }


   //   this.widgetLayer.addFeatures(features);
   CS.map.addLayer(this.routeLayer);


   },
   
   
	onRouteSelect: function (feature)
	{
		// #!# feature.id isn't right, but before it was just 'feature' so at least this gives a bit more info
		alert('You selected - ' + feature.id);
	},
   
   
 /**
  */
 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
   }
   };

   // Normally the styleMap rules property applies to the feature.attribute, but this can provide another context. Here we use the gml object, so the featureType can be referenced.
   var context = function(feature) {
       return feature.gml;
   };
   
   // Add a rule from the above lookup table, with the keys mapped to the "type" property of the features, for the "default" intent.
   styleMap.addUniqueValueRules("default", "featureType", lookup, context);
   
   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, key: this.apiKey},
	       format: this.formatGML})
	 }
     );
   
   // The examples/highlight-feature.html example suggests the use of separate controls for highlighting and selecting the features.
   this.highlightPathsControl = new OpenLayers.Control.SelectFeature(this.pathsLayer, {hover: true, highlightOnly: true});
   CS.map.addControl(this.highlightPathsControl);

   this.selectPathsControl = new OpenLayers.Control.SelectFeature(this.pathsLayer, {onSelect: itinerary.onPathSelect, onUnselect: itinerary.onPathUnSelect});
   CS.map.addControl(this.selectPathsControl);

   // The highlight and select controls are activated by the callback in here.
   this.pathsLayer.events.on({
       "visibilitychanged": this.manageLayers,
	 scope: this
	 });

   //   this.widgetLayer.addFeatures(features);
   CS.map.addLayer(this.pathsLayer);

   },

 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.highlightPathsControl.activate();
       this.selectPathsControl.activate();
       document.getElementById('jpInfo').innerHTML = this.pathsLayerIntroductoryHtml;
     } else {
       document.getElementById('jpInfo').innerHTML = '';
       document.getElementById('jpStart').className = 'focussed';
       this.showMarkerLayer();
       this.highlightPathsControl.deactivate();
       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 = CS.map.getCenter().clone().transform(CS.map.getProjectionObject(), CS.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>";
      }
      // This needs to be the same as meta->minCrowFlyMetres
      if(this.crow_fly_distance < this.min_crow_fly) {
	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;}
    
    CS.map.setCenter(CS.lonLatToMercator(new OpenLayers.LonLat(end.attributes.longitude, end.attributes.latitude)), CS.map.getZoom());
    
  }, 
 
 drawStartForm: function(state)
 {
    var z = CS.map.getZoom();
    var prompt = (z < this.minimumZoomForStreetSelection ? '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 class="chosen">' + 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);

    // Add custom link
    if(this.generateCustomLink) {
	var link = CS.baseUrl + '/link/?latlon=' + CS.r6(this.start.attributes.latitude) + ',' + CS.r6(this.start.attributes.longitude) + '&going=from&name=' + encodeURIComponent(this.start.attributes.name);
	document.getElementById('jpStartCue').innerHTML += '<br /><br /><a href="' + link + '" title="Generate a custom latitude,longitude link for journeys starting from this location"><img class="miniicon" src="' + CS.baseUrl + '/images/icons/lightning.png" alt="Lightning icon" /> Make custom link from here</a>';
    }

    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;
    // This is to keep the lint checker happy.
    default:
    break;
    }
    
  },
 
 
 drawFinishForm: function(state) {

    switch (state) {

    case 'inactive':
    document.getElementById('jpFinish').className = 'inactive';
    document.getElementById('jpFinishTitle').innerHTML = 'Choose your finish point';
    document.getElementById('jpFinishCue').innerHTML = '';
    document.getElementById('jpFinishStreetSearch').disabled = true;
    break;

    case 'focussed':
    document.getElementById('jpFinish').className = 'focussed';
    document.getElementById('jpFinishTitle').innerHTML = 'Choose your finish point';
    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 class="chosen">' + this.finish.attributes.name + '</strong> <a href="javascript:void itinerary.clearEnd(\'finish\');">change</a>';
    document.getElementById('jpFinishStreetSearch').disabled = true;

    // Add custom link
    if(this.generateCustomLink) {
	var link = CS.baseUrl + '/link/?latlon=' + CS.r6(this.finish.attributes.latitude) + ',' + CS.r6(this.finish.attributes.longitude) + '&going=to&name=' + encodeURIComponent(this.finish.attributes.name);
	document.getElementById('jpFinishCue').innerHTML += '<br /><br /><a href="' + link + '" title="Generate a custom latitude,longitude link for journeys to this location"><img class="miniicon" src="' + CS.baseUrl + '/images/icons/lightning.png" alt="Lightning icon" /> Make custom link to here</a>';
    }

    break;
    // This is to keep the lint checker happy.
    default:
    break;
    }
  },
 
 // *******************************************************************************************
 // *                                                                                         *
 // *                      Nearest point and dragging markers                                 *
 // *                                                                                         *
 // *                                                                                         *
 // *******************************************************************************************
 
 setEnd: function (end, feature) {
    
    this[end] = feature;
    
    this.mayPanZoom(feature);

    this.redrawPanel();

  },

    clearEnd: function (end, destroy, redraw) {

    if(this.impatient(false)) {return;}

    if(!this[end]) {return;}

    // Default destroy
    if(arguments.length < 2) {destroy = true;}
    
    if(destroy) {
      this[end].destroy();
    }
    
    this[end] = false;

    // Default redraw argument
    if(arguments.length < 3) {redraw = true;}
    if(redraw) {
        this.redrawPanel();
    }
  },
 
 /**
  * Bring the map to at least zoom  minimumZoomForStreetSelection, with the point in the middle.
  */
 mayPanZoom: function (feature)
 {
   // Zoom
   if(CS.map.getZoom() <= this.minimumZoomForStreetSelection) {
     // Don't zoom if both markers are present.
     if(!this.start || !this.finish) {
       CS.map.zoomTo(this.minimumZoomForStreetSelection);
     }
   }

   // Determine if the point needs to be brought into view.
   var bounds = CS.map.getExtent().transform(CS.map.getProjectionObject(), CS.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), 250);
      
   }
 },
 
 /**
  * Pan the map to the location given. This function was created so that it could be used in timeouts.
  */ 
 pan: function (to) {
    CS.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;

    // The order in which these controls are re-activated matters, because the last one activated gets the events first.
    this.drag.activate();
    this.click.activate();
    if(CS.iconlayerSelectControl) {CS.iconlayerSelectControl.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();

    if(CS.iconlayerSelectControl) {CS.iconlayerSelectControl.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 = CS.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) {

    // 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;
   }

   // #nearestPointTesting #hoverPlay
   // Open layers use the term 'feature' to describe what looks like a 'marker' in this case.
   if(this.hoverPlay) {
     feature.attributes.type = 'zutOrangeDot';
     feature.attributes.end = 'start';
     this.markerLayer.removeFeatures(this.markerLayer.features);
     this.hideTemporaryMarker();
     this.markerLayer.addFeatures(feature);
     return true;
     }

   this.addPoint(feature);
   this.markersMoved = true;
   return true;
 },
 
 
 handleMapClick: function (pixel) {

   // If both ends set, then treat a third click as a reselection of the finish point
   if(this.start && this.finish) {
	this.finish.destroy();
	this.finish = false;
   }
   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 = CS.map.getLonLatFromPixel(pixel);

   // Zoom if too far out (unless you've been dragging).
   if(CS.map.getZoom() < this.minimumZoomForStreetSelection && eventType == 'Click') {

     // Its worth commenting out these two lines for #hoverPlay
     if(!this.hoverPlay) {
       CS.map.setCenter(lonlat, this.minimumZoomForStreetSelection);
       return;
     }
   }
   
   
   this.moveTemporaryMarker(lonlat);
   
   if (CS.map.displayProjection) {
     lonlat.transform(CS.map.getProjectionObject(), CS.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)
 {
	// If there is a popup, destroy it #!# Hacky - this should ideally be in iconlayer.js but can't work out how to close a popup from within itself, after 2 days of frustration!
	if(CS.popup && CS.popup.feature){
		CS.map.removePopup(CS.popup.feature.popup);
		CS.popup.feature.popup.destroy();
		CS.popup.feature.popup = null;
		CS.popup.feature = null;
	}
	
     var url =  CS.baseUrl + '/api/nearestpoint.xml?key=' + this.apiKey + '&useDom=1&longitude=' + CS.r6(longitude) + '&latitude=' + CS.r6(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=' + CS.r6(end.attributes.longitude) + '&other_latitude=' + CS.r6(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 = this.speed;
    
    // 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 queryString = '?useDom=1';
   
   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;

   // Use the itinerarypoints api call.
   queryString += '&itinerarypoints=';
   queryString += encodeURI(this.start.attributes.longitude)  + ',' + encodeURI(this.start.attributes.latitude)  + ',' + encodeURI(this.start.attributes.name) + '|';
   queryString += encodeURI(this.finish.attributes.longitude) + ',' + encodeURI(this.finish.attributes.latitude) + ',' + encodeURI(this.finish.attributes.name);

   window.location = CS.baseUrl + '/journey/newItinerary.html' + queryString;
 },

 /**
  * 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);
   
   CS.map.zoomTo(zoom);
   CS.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 CS.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;
 },
 
 drawRoute: function (request, notedIssuesSummary)
 {
   // Read the features from the GML
   this.features = this.formatGML.read(request.responseText);

   // Pick out the route feature
   var routeFeature = this.getFeature('route');
   if(!routeFeature) {
     this.prompt(CS.icon_tag('exclamation') + ' No route. ' + notedIssuesSummary);
     return;
   }

   // Add a summary of the route
   this.addRouteSummary(routeFeature);
   
   // Choose a color for the route based on the plan.
   // The styleMap for the layer uses this field.
   routeFeature.attributes.routeColor = this.planColor(routeFeature.attributes.plan);
   routeFeature.attributes.type = 'route';

   // Remove any existing route.
   this.routeLayer.removeFeatures(this.routeLayer.features);

   // Add the feature
   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 'balanced':
   case 'manchest':
     color = "#d09800";
     break;
   case 'leisure':
       // brown
     color = "#964b00";
     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;}

   // Plan comes from clickRouting value.
   var plan = this.clickRouting;

   var queryString = '?useDom=1&key=' + this.apiKey + '&plan=' + plan;

   // Use the itinerarypoints api call.
   queryString += '&itinerarypoints=';
   queryString += encodeURI(CS.r6(this.start.attributes.longitude))  + ',' + encodeURI(CS.r6(this.start.attributes.latitude)) + '|';
   queryString += encodeURI(CS.r6(this.finish.attributes.longitude)) + ',' + encodeURI(CS.r6(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';}
 },


  // *******************************************************************************************
  // *                                                                                         *
  // *                     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;
  },

 // Used to display the route.
 showJourney: function (tabName, html, coords, js) {
    document.getElementById(tabName).innerHTML = html;
    this.addPlannedRoute(tabName, coords);
    eval(js);
    this.selectPlan(tabName);
  }
};

/**
 * Define a click control class, which requests the nearest point.
 * @see this same function in setmarker.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.Click(this, {'click': this.trigger}, this.handlerOptions);
    },
      
      trigger: function(e) {itinerary.handleMapClick(e.xy);}
    
  });

////Ends

