// this file uses the YUI Doc standard for documentation
// more information and a parser can be found at:
// http://developer.yahoo.com/yui/yuidoc/

// create namespace if it doesn't exist yet
if (typeof window.WLCP == "undefined") window.WLCP = new Object();

/**
 * The Map module provides a binding for the GoogleMaps to show geotagged assets
 * and to tag them
 * @module Map
 */

/**
 * Pool of all GIcons, that can be use by the Map class
 *
 * @class IconPool
 * @namespace WLCP
 */
WLCP.IconPool = function() {

  /////////////////////////////////////////////////////////////////////////
  //
  // Private Properties
  //
  /////////////////////////////////////////////////////////////////////////

  /**
   * Collection of icons
   *
   * @property _icons
   * @private
   */
  var _icons = new Array();

  return {
    /////////////////////////////////////////////////////////////////////////
    //
    // Static Methods
    //
    /////////////////////////////////////////////////////////////////////////

    /**
     * Add an icon to the pool
     *
     * @method add
     * @static
     * @param ident {String} Identifier of the icon
     * @param params {Array} Parameters to setup the icon
     * @return {GIcon} The icon
     */
    add: function(ident, params) {
      var icon = new GIcon(G_DEFAULT_ICON);

      if (params.image)
        icon.image = params.image;
      if (params.shadow)
        icon.shadow = params.shadow;
      if (params.iconSize)
        icon.iconSize = new GSize(params.iconSize[0], params.iconSize[1]);
      if (params.shadowSize)
        icon.shadowSize = new GSize(params.shadowSize[0], params.shadowSize[1]);
      if (params.iconAnchor)
        icon.iconAnchor = new GPoint(params.iconAnchor[0], params.iconAnchor[1]);
      if (params.infoWindowAnchor)
        icon.infoWindowAnchor = new GPoint(params.infoWindowAnchor[0], params.infoWindowAnchor[1]);

      _icons.push({ident: ident, icon:icon});
      return icon;
    },

    /**
     * @method get
     * @static
     * @param ident {String} Identifier of the icon
     * @return {GIcon} The icon
     */
    get: function(ident) {
      if (!ident) return null;
      for (var i=0; i < _icons.size(); i++) {
        if (_icons[i].ident == ident) return _icons[i].icon;
      }
      return null;
    }
  }
}();

/**
 * Represents a map and provides methods to add markers and to configure the
 * representation
 *
 * @class Map
 * @namespace WLCP
 * @constructor
 * @param domId {String} Dom ID of the map container
 * @param options {Object} Associative Array with configuration options
 */
WLCP.Map = function(domId, options) {

  /////////////////////////////////////////////////////////////////////////
  //
  // Private properties
  //
  /////////////////////////////////////////////////////////////////////////

  /**
   * The Dom ID of the map container
   *
   * @property _domId
   * @type String
   * @private
   */
  this._domId = domId;

  /**
   * Associative array of configuration options
   *
   * @property _options
   * @type Object
   * @private
   */
  this._options = options || {};

  /**
   * Pool of GMarkers
   *
   * @property _markers
   * @type Gmarker[]
   * @private
   */
  this._markers = [];

  /**
   * Pool of draggable markers, indexed by ident
   *
   * @property _draggableMarkers
   * @type Object
   * @private
   */
  this._draggableMarkers = {};

  /**
   * Flag, true if map is already renderd, false otherwise
   *
   * @property _rendered
   * @type boolean
   * @private
   */
  this._rendered = false;

  /**
   * Refernce to the GMap
   *
   * @property _map
   * @type GMap2
   * @private
   */
  this._map = null;

  /**
   * Refernce to the Geocoder
   *
   * @property _geocoder
   * @type GClientGeocoder
   * @private
   */
  this._geocoder = new GClientGeocoder();

  /**
   * Reference to the Clusterer
   *
   * @property _clusterer
   * @type Clusterer
   * @private
   */
  this._clusterer = null;
}


/////////////////////////////////////////////////////////////////////////
//
// Private static constants
//
/////////////////////////////////////////////////////////////////////////

/**
 * Default values for all configuration options
 *
 * @property DEFAULT_OPTIONS
 * @type Object
 * @static
 * @final
 */
WLCP.Map.DEFAULT_OPTIONS = {
  maxZoomLevel:                   0,
  maxZoomLevelDefaultPosition:    0,
  defaultLat:                     0,
  defaultLng:                     0,
  maxVisibleMarkers:              100,
  minMarkersPerCluster:           1,
  maxLinesPerInfoBox:             3,
  errorNotCompatible:             "Google Maps doesn't work in your brower.",
  successSendDraggablePosition:   "Position sent.",
  errorSendDraggablePosition:     "Position could not been sent.",
  errorRetrievePositionFromAsset: "Position could not been retrieved."
};


/////////////////////////////////////////////////////////////////////////
//
// Public methods
//
/////////////////////////////////////////////////////////////////////////

WLCP.Map.prototype = {
  /**
   * Add marker from associative array
   *
   * @method addMarker
   * @param markerInfo {Object} Description of the marker to add
   * @param options {Object} Configuration passed to the GMarker constructor
   * @return {GMarker} GMarker Object
   */
  addMarker: function(markerInfo, options) {
    options = options || {};
    var icon = WLCP.IconPool.get(options.icon);

    var point = new GLatLng(parseFloat(markerInfo.latitude),
                            parseFloat(markerInfo.longitude));
    var marker = new GMarker(point, {icon: icon});

    // add info window
    if (markerInfo.info) {
      GEvent.addListener(marker, "click", function() {
          marker.openInfoWindowHtml(markerInfo.info);
      });
    }

    // push marker in pool
    this._markers.push(marker);

    // add marker to map, if map is already rendered
    if (this._rendered) {
      this._clusterer.AddMarker(marker, "test");
    }
    return marker;
  },

  /**
   * Add markers from an Array, using addMarker.
   *
   * @method addMarkers
   * @param markers {Object[]} Array of marker descriptions
   * @param options {Object} Configuration passed to the GMarker constructor
   */
  addMarkers: function(markers, options) {
    for (var i=0; i < markers.size(); i++) {
      this.addMarker(markers[i], options);
    }
    return true;
  },

  /**
   * Add a draggable marker
   *
   * @method addDraggableMarker
   * @param location {GLatLng|String} Position or Address of the location
   * @param ident {String} identifier to get position of this marker later
   * @param options {Object} Configuration passed to the GMarker constructor
   */
  addDraggableMarker: function(location, ident, options) {
    options = options || {};
    var icon = WLCP.IconPool.get(options.icon);

    // define call backback for geocoding
    var self = this; // save this in self for callback
    var callback = function(point) {
      var hasDefaultPosition = false;
      // default if position is empty
      if (!point.lat() || !point.lng()) {
        point = new GLatLng(self._getOption('defaultLat'),
                            self._getOption('defaultLng'));
        hasDefaultPosition = true;
      }
      var marker = new GMarker(point, {draggable: true, icon: icon});
      marker.hasDefaultPosition = hasDefaultPosition;
      marker.setByUser = false;

      // set setByUser flag to true if dragged
      GEvent.addListener(marker, "dragend", function() {
        marker.setByUser = true;
      });

      self._draggableMarkers[ident] = marker;
      self._markers.push(marker);
    }

    // use geocoding, if location is a string
    if (typeof location == "string") {
      this._geocoder.getLatLng(location, callback);
    }
    // otherwise use the position given
    else {
      var point = new GLatLng(parseFloat(location.latitude),
                              parseFloat(location.longitude));

      callback(point);
    }
  },

  /**
   * Send position of draggable marker via AJAX to defined url
   *
   * @param sendDraggablePosition
   * @param ident {String} Identifier to get position of this marker later
   * @param url {String} URL to which the reqeust is sent
   * @param options {Object} Prototype AJAX options.
   *                         Parameters will be overridden with position.
   *                         If onSuccess/onFailure callbacks are defined
   *                         the default callbacks (alert) will only be executed if 
   *                         the custom callbacks return true
   */
  sendDraggablePosition: function(ident, url, options) {
    options = options || {};
    var marker = this._draggableMarkers[ident];
    var point = marker.getLatLng();

    var self = this;

    //callbacks
    this._wrapCallback(options, 'onSuccess', function() {
      alert(self._getOption('successSendDraggablePosition'));
    });
    this._wrapCallback(options, 'onFailure', function() {
        alert(self._getOption('errorSendDraggablePosition'));
    });

    options.parameters = {lat:         point.lat(),
                          lng:         point.lng(),
                          set_by_user: marker.setByUser};
    new Ajax.Request(url, options); // NEEDS PROTOTYPE
  },

  /**
   * Retrieve position from the attached asset per ajax and reset the map view
   * to show all markers
   *
   * @param ident {String} Identifier to get position of this marker later
   * @param url {String} URL to which the reqeust is sent
   * @param options {Object} Prototype AJAX options.
   *                         If onSuccess/onFailure callbacks are defined
   *                         the default callbacks (alert, center map) will only be
   *                         executed if the custom callbacks return true
   */
  retrievePositionFromAsset: function(ident, url, options) {
    options = options || {};
    var marker = this._draggableMarkers[ident];

    var self = this;

    // callbacks
    this._wrapCallback(options, 'onSuccess', function(transport) {
      var result = transport.responseJSON;
      var point = new GLatLng(result.latitude, result.longitude);
      marker.setLatLng(point);
      marker.hasDefaultPosition = false;
      marker.setByUser = false;
      self.showAllMarkers();
    });

    this._wrapCallback(options, 'onFailure', function(transport) {
      alert(self._getOption('errorRetrievePositionFromAsset'));
    });

    new Ajax.Request(url, options); // NEEDS PROTOTYPE
  },

  /**
   * Renders the map in the map container
   *
   * @method render
   * @return {boolean} true on success, false otherwise
   */
  render: function() {
    if (!GBrowserIsCompatible()) {
      alert(this._getOption('errorNotCompatible'));
      return false;
    }
    var container = document.getElementById(this._domId);
    if (typeof container == "undefined" || container == null) {
      return false;
    }

    // set up map and clusterer
    this._map = new GMap2(container);
    this._clusterer = new Clusterer(this._map);
    this._applyConfig();

    // add markers
    for (var i=0; i < this._markers.size(); i++) {
      this._clusterer.AddMarker(this._markers[i], "test");
    }

    // focus map
    this.showAllMarkers();

    this._rendered = true;
    return true;
  },

  /**
   * Finds a zoom level and a center for the map at which all markers can be seen
   * and applies zoom level and center to the map
   *
   * @method showAllMarkers
   */
  showAllMarkers: function() {
    // set up default center and high zoom
    var center = new GLatLng(this._getOption('defaultLat'),
                             this._getOption('defaultLng'));
    var zoom   = 15;

    // try to get center and zoom level from the markers
    if (this._markers.size() > 0) {
      var bounds = new GLatLngBounds();
      for (var i=0; i < this._markers.size(); i++) {
        bounds.extend(this._markers[i].getLatLng());
      }

      center = bounds.getCenter();
      zoom   = this._map.getBoundsZoomLevel(bounds);
    }

    // correct zoom level with maximum zoom
    zoom = (this._maxZoomLevel() < zoom) ? this._maxZoomLevel() : zoom;

    this._map.setCenter(center, zoom);
  },


  /////////////////////////////////////////////////////////////////////////
  //
  // Protected methods
  //
  /////////////////////////////////////////////////////////////////////////

  /**
   * Returns configuration value falling back on default configuration
   *
   * @method _getOption
   * @protected
   * @param key {String} Name of the configuration parameter
   * @return {MIXED} Value of the configuration parameter
   */
  _getOption: function(key) {
    return (this._options[key] || WLCP.Map.DEFAULT_OPTIONS[key]);
  },

  /**
   * Returns the maximum zoom level which is _options.maxZoomLevelDefaultPosition
   * if there are markers set to default position and
   * _options.maxZoomLevel otherwise
   *
   * @method _maxZoomLevel
   * @protected
   * @return {Number} The zoom level
   */
  _maxZoomLevel: function() {
    for (var i=0; i < this._markers.size(); i++) {
      if (this._markers[i].hasDefaultPosition)
        return this._getOption('maxZoomLevelDefaultPosition');
    }

    if (this._markers.size() == 0)
      return this._getOption('maxZoomLevelDefaultPosition');

    return this._getOption('maxZoomLevel');
  },
                  
  /**
   * Applies configuration to map and clusterer
   *
   * @method _applyConfig
   * @protected
   */
  _applyConfig: function() {
    // maps controls
    var controls = this._options.mapControls || [];
    for (var i=0; i < controls.size(); i++) {
      var Control = eval(controls[i]);
      this._map.addControl(new Control());
    }

    // clusterer settings
    this._clusterer.SetMaxVisibleMarkers(this._getOption('maxVisibleMarkers'));
    this._clusterer.SetMinMarkersPerCluster(this._getOption('minMarkersPerCluster'));
    this._clusterer.SetMaxLinesPerInfoBox(this._getOption('maxLinesPerInfoBox'));
  },

  /**
   * Wrapps the callback given by options[event] and the default callback
   * in a function and substitutes options[event] with that.
   * the default callback will only be called if the custom call returns
   * true. if the custom callback is empty only the default callback is 
   * returned
   *
   * @method _wrapCallback
   * @private
   * @param {Object} options The AJAX options object
   * @param {String} event The name of the event (the key of the options hash)
   * @param {Function} defaultCallback the default callback
   * @return {Object} the options Object with substituded callback
   *
   */
  _wrapCallback: function(options, event, defaultCallback) {
    var callback = options[event] || function() {return true};

    options[event] = function(transport) {
      if (callback(transport)) {
        defaultCallback(transport);
      }
    }
  }
}


/////////////////////////////////////////////////////////////////////////
//
// Static private Properties
//
/////////////////////////////////////////////////////////////////////////

/**
 * Pool of map objects
 *
 * @property _maps
 * @static
 * @private
 */
WLCP.Map._maps = new Array();


/////////////////////////////////////////////////////////////////////////
//
// Static Methods
//
/////////////////////////////////////////////////////////////////////////

/**
 * Creates new map objects, attache it to the pool
 * and registers the render method for exeuction at window load
 *
 * @method create
 * @static
 * @param domId {String} The Dom ID of the map container
 * @param options {Object} Associative array of configuration options
 */
WLCP.Map.create = function(domId, options) {
  // set up map object
  var map = new WLCP.Map(domId, options);

  // push map object in the pool
  WLCP.Map._maps.push({domId: domId, 'map': map});

  // render map on load
  if (document.loaded) { // NEEDS PROTOTYPE >1.6
    map.render();
  }
  else {
    Event.observe(window, 'load', // NEEDS PROTOTYPE
      function() { map.render(); }
    );
  }
  return map;
}

/**
 * Get map object from the object pool
 *
 * @method get
 * @static
 * @param domId {String} the Dom ID of the map container
 */
WLCP.Map.get = function(domId) {
  var map;
  for (key in WLCP.Map._maps) {
    map = WLCP.Map._maps[key];
    if (map.domId == domId) return map.map;
  }
  return null;
}
