/** Defines the interface to the open layers 'map widget'.
 *
 * The main aim of this is to display a map with a marker at a longitude, latitude.
 *
 * The position of the marker can be set by clicking or dragging.
 * Changes to the marker position set longitude/latitude values in input boxes.
 *
 * Outline of the control flow
 * ===========================
 *
 * Based on the itinerary.js structure, but much simpler.
 *
 *
 *  Page Initialises
 *        |
 *        | CS object created
 *        | olmapwidget created.
 *        | Longitude/Latitude fields exist somewhere on the page
 *        v
 * olmapwidget.initialize();
 *        .
 *        . . . . . . . . . . . . . . .
 *        .                           .
 *        v                           v
 *    clickHandler                dragHandler
 * OpenLayers.Control.Click      OpenLayers.Control.DragFeature
 * handleMapClick()              handleMapDrag()
 *        |                           |
 *        +- - - - - - - - - - - - - -+
 *        v
 * handleMapEvent(Click|Drag)
 *        |
 *        |
 *        v
 *  callback(lon, lat)
 *
 */

var olmapwidget = {
  
  // This method makes this object easier to identify in firebug.
 toString: function () {
    var string = 'Olmapwidget()';
    return string;
  },

 // Click control for adding markers.
 click: null,
 
 // Drag control for moving the markers.
 drag: null,

 // The single marker that the user can set down or drag. 
 temporaryMarker: null,

 // A layer containing the markers
 markerLayer: null,

 // The location
 lonlat: null,

 // A boolean that controls whether to prompt for a bearing by rubber banding from the dropped marker.
 bearingRubberBanding: false,
 
 /** Sets up the Map to define the olmapwidget.
  * @param string Name defines the name of the layer in which the marker appears.
  * @param float longitude, latitude are the map centre, and the place where the marker initially appears if locationKnown
  * @param int zoom
  * @param bool changeable whether the position of the marker can be moved.
  * @param bool locationKnown whether the longitude,latitude should be used to initally place the marker.
  * @param function callback Takes args (longitude, latitude).
  * @param bool bearingRubberBanding Whether to prompt for a bearing by rubber banding from the dropped marker.
  */
 initialize: function (name, longitude, latitude, zoom, changeable, locationKnown, callback, zoomCallback, bearingRubberBanding)
 {

    this.changeable = changeable;
    this.callback = callback;
    this.zoomCallback = zoomCallback;
    this.bearingRubberBanding = bearingRubberBanding;

    var centre = CS.lonLatToMercator( new OpenLayers.LonLat(longitude, latitude));
    var map = CS.createMap('map', centre, zoom, (changeable ? true : false));

    this.setupMarkerLayer(name);


    // Add marker dragging (from drag-feature example)
    this.drag = new OpenLayers.Control.DragFeature(this.markerLayer, {
      onStart: 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 = 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;

	 
	  olmapwidget.draggedFeature = feature;
	  
	},

      onComplete: function(feature, pixel) {

	  // Cannot use 'this' in this closure.

	  // Add in the offset of the moust from the marker's hotspot.
	  pixel.x += feature.attributes.hotspotOffsetX;
	  pixel.y += feature.attributes.hotspotOffsetY;

	  olmapwidget.handleMapDrag(pixel);
	}
      });
    CS.map.addControl(this.drag);

    
    
    this.click = new OpenLayers.Control.Click();
    CS.map.addControl(this.click);
    

    // For displaying the bearing rubber band
    if(this.bearingRubberBanding) {
      var lineLayer = new OpenLayers.Layer.Vector("Line Layer");
      map.addLayer(lineLayer);
      // A customized control that rubber bands the bearing.
      this.line = new OpenLayers.Control.DrawBearing(lineLayer, OpenLayers.Handler.Line);
      CS.map.addControl(this.line);
    }


    // Only allow dragging if the position can be changed.
    if(changeable) {
      this.drag.activate();
      this.click.activate();    
    }


       CS.map.events.on({
            'zoomend': this.updateZoom,
            scope: this
        });


    if(locationKnown) {
      this.initialiseMarker(centre.lon, centre.lat);
    }
  },

 updateZoom: function(e) {
    this.zoomCallback(CS.map.getZoom());
  },
 /**
  * Setus up a style lookup table based on...
  * @link http://london.cyclestreets.ibsen/openlayers/examples/styles-unique.html
  */
 setupMarkerLayer: function (name)
 {

   // create a styleMap with a custom default symbolizer
   var styleMap = new OpenLayers.StyleMap({

       // 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:           100,
       backgroundGraphicZIndex: 110

	 });
   

   // create a lookup table with different symbolizers for 0, 1 and 2
   var lookup = {
   amber: {
     externalGraphic: CS.baseUrl + '/' + CS.themeLocation + 'amber_wisp_36x30.png',
     
     // Shadows are explained in ...
     // http://london.cyclestreets.ibsen/openlayers/examples/marker-shadow.html
     backgroundGraphic: CS.baseUrl + '/' + CS.themeLocation + 'wisp_shadow_36x30.png',
     
     graphicWidth:  51,
     graphicHeight: 42,
     
     // Hotspot     
     graphicXOffset:  -10,
     graphicYOffset: -41,
     
     // Makes sure the background graphic is placed correctly relative
     // to the external graphic.
     backgroundXOffset:  -10,
     backgroundYOffset: -41
   },
   
   x_marks_spot: {
     externalGraphic: CS.baseUrl + '/' + CS.themeLocation + 'x_marks_spot_39x30.png',
     
     // Shadows are explained in ...
     // http://london.cyclestreets.ibsen/openlayers/examples/marker-shadow.html
     backgroundGraphic: null,
     
     graphicWidth:  39,
     graphicHeight: 30,
     
     // Hotspot     
     graphicXOffset: -20,
     graphicYOffset: -12
   }
   };
   
   // 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(name, {
     styleMap: styleMap,
	 rendererOptions: {yOrdering: false}
	 });
   
   CS.map.addLayer(this.markerLayer);

   },

 initialiseMarker: function (lon, lat)
 {
   // A temporary marker that gets moved around.
   this.temporaryMarker = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(lon, lat),
  {type: (this.changeable ? 'amber' : 'x_marks_spot')});
   
   this.markerLayer.addFeatures(this.temporaryMarker);
 },
 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);
 },
 
 
 handleMapClick: function (pixel) {this.handleMapEvent(pixel, 'Click');},
 
 handleMapDrag:  function (pixel) {this.handleMapEvent(pixel, 'Drag');},
 
 handleMapEvent: function (pixel, eventType)
 {
   this.lonlat = CS.map.getLonLatFromPixel(pixel);

   
   // If a marker is not already there create one at that location.
   if(!this.temporaryMarker) {
     this.initialiseMarker(this.lonlat.lon, this.lonlat.lat);
   } else {
     // Clicks need to move the already existing marker.
     if(eventType == 'Click') {
       this.moveTemporaryMarker(this.lonlat);
     }
   }
   
   if (CS.map.displayProjection) {
     this.lonlat.transform(CS.map.getProjectionObject(), CS.map.displayProjection );
   }

   // Switch for new draw bearing feature.
   if(!this.bearingRubberBanding) {
     
     this.callback(this.lonlat.lon, this.lonlat.lat, null);

   } else {
     
     // Activate the rubber banding by turning off marker movment and setting up the line handler.
     this.drag.deactivate();
     this.click.deactivate();
     this.line.activate();
     // Simulate the start of hte line at the location where the marker was dropped.
     this.line.handler.mousedown({xy: pixel});
     this.line.handler.mouseup({xy: pixel});
   }
 },

 // Called by the rubber banding when a bearing has been set.
 finishBearing: function (geometry)
 {
   geometry.transform(CS.map.getProjectionObject(), CS.map.displayProjection );

   var startPoint  = geometry.components[0];
   var finishPoint = geometry.components[1];

   var bearing = LatLon2bearing(startPoint.y, startPoint.x, finishPoint.y, finishPoint.x);

   var roundedBearing = 45 * Math.floor((bearing + 22.5) / 45);

   if(roundedBearing == 360) {roundedBearing = 0;}

   // The bearing has to be in a rather annoying format, strings as one of 0, 045, 090, 135...315.
   switch(roundedBearing) {
   case   0:
   case 360: roundedBearing = '0'; break;
   case  45: roundedBearing = '045'; break;
   case  90: roundedBearing = '090'; break;
   default: roundedBearing = roundedBearing.toString();
   }

   this.callback(this.lonlat.lon, this.lonlat.lat, roundedBearing);

   //   alert("rb = " + roundedBearing + "\n\nBearing = " + bearing + "\n\n" + geometry);

   // Reactivate the main marker.
   this.line.deactivate();
   this.drag.activate();
   this.click.activate();
 }

};


// http://www.movable-type.co.uk/scripts/latlong.js
/*
 * calculate (initial) bearing between two points
 *   see http://williams.best.vwh.net/avform.htm#Crs
 */
LatLon2bearing = function(lat1, lon1, lat2, lon2) {
  lat1 = lat1.toRad(); lat2 = lat2.toRad();
  var dLon = (lon2-lon1).toRad();

  var y = Math.sin(dLon) * Math.cos(lat2);
  var x = Math.cos(lat1)*Math.sin(lat2) -
          Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
  return Math.atan2(y, x).toBrng();
};

// extend Number object with methods for converting degrees/radians

Number.prototype.toRad = function() {  // convert degrees to radians
  return this * Math.PI / 180;
};

Number.prototype.toDeg = function() {  // convert radians to degrees (signed)
  return this * 180 / Math.PI;
};

Number.prototype.toBrng = function() {  // convert radians to degrees (as bearing: 0...360)
  return (this.toDeg()+360) % 360;
};




/**
 * Define a click control class, which requests the nearest point.
 * @see this same function in itinerary.js
 */

OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {                
  defaultHandlerOptions: {
      'single': true,
	'double': false,
	'pixelTolerance': 0,
	'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) {olmapwidget.handleMapClick(e.xy);}
  });


// To get the desired behaviour of a rubber-band appearing after the marker has been dropped, two subclasses of the drawing tools are used.
// DrawBearing subclasses DrawFeature to provide its own finalisation function. drawFeature().
// Line subclasses the Path handler so that a mouse down finalises drawing.


/**
 * Based on:
 * OpenLayers.Control.DrawFeature
 */
OpenLayers.Control.DrawBearing = OpenLayers.Class(OpenLayers.Control.DrawFeature, {

    initialize: function(layer, handler, options) {
        OpenLayers.Control.DrawFeature.prototype.initialize.apply(this, arguments);
    },

      // This is the call back when the bearing is set by rubber-banding.
      drawFeature: function(geometry) {
      // Could potentially draw a stub of the bearing.
      //alert(geometry);
      olmapwidget.finishBearing(geometry);
    },
      
    CLASS_NAME: "OpenLayers.Control.DrawBearing"
});



/**
 * Handler -based on OpenLayers.Handler.Path
 */
OpenLayers.Handler.Line = OpenLayers.Class(OpenLayers.Handler.Path, {
    

    initialize: function(control, callbacks, options) {
        OpenLayers.Handler.Path.prototype.initialize.apply(this, arguments);
    },


      // Mouse down and double click are redefined to stop drawing.
      // finalize with no arguments calls the 'done' callback.
    mousedown: function(evt) {

      if(this.drawing) {
	this.removePoint();
	this.finalize();
	return false;
      }

      // Call the parent to handle
      return OpenLayers.Handler.Path.prototype.mousedown.apply(this, arguments);

    },

    dblclick: function(evt) {
      this.removePoint();
      this.finalize();
      return false;
    },

    CLASS_NAME: "OpenLayers.Handler.Line"
});


//// Ends
