/*
 *  GMAP3 Plugin for JQuery 
 *  Version   : 2.1
 *  Date      : February, 12 2011
 *  Licence   : GPL v3 : http://www.gnu.org/licenses/gpl.html  
 *  Author    : DEMONTE Jean-Baptiste
 *  Contact   : jbdemonte@gmail.com
 *  Web site  : http://night-coder.com
 *  
 *  Thanks for mailing me for bug, feedback, integration ...
 *  
 *  2.1 - 2011-02-12
 *    fixed: default values {} for optional properties removed (cause pb in getElementById)
 *    fixed: jQuery used instead of $ (some were missing)
 *    fixed: auto-init problem in ie  
 *    added: get, clear, addmarkers
 *    deprecated: removedirectionsrenderer, removebicyclinglayer, removetrafficlayer : use clear
 *
 *  2.0 - 2011-01-07
 *    updated: jQuery used instead of $ 
 *    updated: callback function return jquery object instead of it's id
 *    updated: events return jquery object instead of it's id
 *    updated: infoWindow simplier in :addMarker
 *    updated: latLng standardised and can now be [lat:number, lng:number]
 *    updated: callback can be an array of function
 *    updated: setbicyclinglayer renamed addBicyclingLayer
 *    updated: setGroundOverlay renamed addGroundOverlay
 *    updated: setkmllayer renamed addkmllayer
 *    updated: setTrafficLayer renamed addTrafficLayer
 *    added: manage callback in :init
 *    added: onces : addListenerOnce managed
 *    added: :addOverlay :addFixPanel :addCircle :addRectangle :getElevation :removeBicyclingLayer :removeTrafficLayer
 *    fixed: :init run by _subcall doesn't acknoledge stack  
 *    fixed: :addDirectionRenderer twice no longer remove newt action
 *    fixed: :addPolygon and :addPolyline now return the elements
 *    fixed: :addMarker run apply (deprecated methods removed)
 *    fixed: :removeDirectionsRenderer remove now the directions 
 *    fixed: some bugs when wrong parameters (locks)
 *    fixed: private function manage auto init like public one ( _getLatLng => :getLatLng )  
 *   
 *  1.2 - 2010-12-03
 *    fixed : map modification in frm functions (addmarker...) now works
 *    fixed : asynchronous actions (ie: address resolution) were bypassed by synchronous
 *            ie: addMarker[string address], enableScrollWheelZoom => before address is resolved, enableScrollWheelZoom starts
 *            => added a stack manager which push all actions and start next one once previous is finished.
 *            thanks to james for bug report 
 *    added : addStyledMap, setStyledMap      
 *         
 *  1.1 - 2010-11-10
 *    fixed : implicit init doesn't use map parameters
 *    added : getRoute, addDirectionsRenderer, setDirectionsPanel, setDirections
 *  
 *  1.0 - 2010-11-01      
 */
  
jQuery.gmap3 = {
  /*============================*/
  /*          PRIVATE           */
  /*============================*/
  
  _ids:{},
  
  /****************************************************************************/
  /*                                  STACK
  /****************************************************************************/
  _running:{
  },
  _stack:{
    _a:{},
    init: function(id){
      if (!this._a[id]) this._a[id] = [];
    },
    add: function(id, a){
      this.init(id);
      this._a[id].push(a);
    },
    addNext: function(id, a){
      var t = [], i=0, k;
      this.init(id);
      for(k in this._a[id]){
        if (i == 1) t.push(a);
        t.push(this._a[id][k]);
        i++;
      }
      if (i < 2) t.push(a);
      this._a[id] = t;
    },
    get: function(id){
      var k;
      if (this._a[id]) 
        for(k in this._a[id]){
          if (this._a[id][k]) return this._a[id][k];
        }
      return false;
    },
    ack: function(id){
      var k;
      if (this._a[id]) 
        for(k in this._a[id]){                     
          if (this._a[id][k]) {
            delete this._a[id][k];
            break;
          }
        }
        if (this.empty(id)) delete this._a[id];
    },
    empty: function(id){
      var k;
      if (!this._a[id]) return true;    
      for(k in this._a[id]){
        if (this._a[id][k]) return false
      }
      return true;
    }
    
  },
  /**
   * @desc create default structure if not existing
   **/
  _init: function($this, id){
    if (!this._ids[ id ]) {
      this._ids[ id ] = {
        $this:$this,
        styles: {},
        stored:{},
        map:null
      };
    }
  },
  /**
   * @desc store actions to do in a stack manager
   **/
  _plan: function($this, id, list){
    var k;
    this._init($this, id);
    for(k in list) this._stack.add(id, list[k] );
    this._run(id);
  },
  /**
   * @desc return true if action has to be executed directly
   **/
  _isDirect: function(id, data){
    var action = this._ival(data, 'action');
    return action == ':get';
  },
  /**
   * @desc execute action directly
   **/
  _direct: function(id, data){
    var action = this._ival(data, 'action').substr(1);
    return this[action](id, jQuery.extend({}, this._default[action], data['args'] ? data['args'] : data));
  }, 
  /**
   * @desc store one action to do in a stack manager after the first
   **/
  _planNext: function(id, a){
    var $this = this._jObject(id);
    this._init($this, id);
    this._stack.addNext(id, a);
  },
  /**
   * @desc called when action in finished, to acknoledge the current in stack and start next one
   **/
  _end: function(id){
    delete this._running[id];
    this._stack.ack(id);
    this._run(id);
  },
  /**
   * @desc if not running, start next action in stack
   **/
  _run: function(id){
    if (this._running[id]) return;
    var a = this._stack.get(id);
    if (!a) return;
    this._running[id] = true;
    this._proceed(id, a);
  },
  /****************************************************************************/
  
  _properties:['events','onces','options','apply', 'callback'],
  
  _default:{
    verbose:true,
    init:{
      mapTypeId : google.maps.MapTypeId.ROADMAP,
      center:[46.593623,0.342922],
      zoom: 10
    }
  },
  
  _geocoder: null,
  _getGeocoder: function(){
    if (!this._geocoder) this._geocoder = new google.maps.Geocoder();
    return this._geocoder;
  },
  
  _directionsService: null,
  _getDirectionsService: function(){
    if (!this._directionsService) this._directionsService = new google.maps.DirectionsService();
    return this._directionsService;
  },
  
  _elevationService: null,
  _getElevationService: function(){
    if (!this._elevationService) this._elevationService = new google.maps.ElevationService();
    return this._elevationService;
  },
  
  _getMap: function( id ){
    return this._ids[ id ].map;
  },
  
  _setMap: function (id, map){
    this._ids[ id ].map = map;
  },
  
  _jObject: function( id ){
    return this._ids[ id ].$this;
  },
  
  _addStyle: function(id, styleId, style){
    this._ids[ id ].styles[ styleId ] = style;
  },
  
  _getStyles: function(id){
    return this._ids[ id ].styles;
  },
  
  _getStyle: function(id, styleId){
    return this._ids[ id ].styles[ styleId ];
  },
  
  _styleExist: function(id, styleId){
    return this._ids[ id ] && this._ids[ id ].styles[ styleId ];
  },
  
  _getDirectionRenderer: function(id){
    return this._getFirstStored(id, 'directionrenderer');
  },
  
  _exist: function(id){
    return this._ids[ id ].map ? true : false;
  },
  
  /**
   * @desc return last non-null object
   **/
  _getFirstStored: function(id, name){
    var idx = 0;
    if (this._ids[ id ].stored[name] && this._ids[ id ].stored[name].length ){
      while(idx < this._ids[ id ].stored[name].length){
        if (this._ids[ id ].stored[name][idx]){
          return this._ids[ id ].stored[name][idx];
        }
        idx++;
      }
    }
    return null;
  },
  
  /**
   * @desc return last non-null object
   **/
  _getLastStored: function(id, name){ 
    var idx;
    if (this._ids[ id ].stored[name] && this._ids[ id ].stored[name].length ){
      idx = this._ids[ id ].stored[name].length - 1;
      do{
        if (this._ids[ id ].stored[name][idx]){
          return this._ids[ id ].stored[name][idx];
        }
        idx--;
      }while(idx >= 0);
    }
    return null;
  },
  
  /**
   * @desc add an object in the stored structure
   **/
  _store: function(id, name, data){
    name = name.toLowerCase();
    if (!this._ids[ id ].stored[name])
        this._ids[ id ].stored[name] = new Array();
    this._ids[ id ].stored[name].push(data);
  },
  
  /**
   * @desc remove an object from the stored structure
   **/
  _unstore: function(id, name, pop){
    var idx, t = this._ids[ id ].stored[name];
    if (!t) return false;
    idx = pop ? t.length - 1 : 0;
    if (typeof(t[idx]) == 'undefined') return false;
    if (typeof(t[idx]['setMap']) == 'function') t[idx].setMap(null);  // Google Map element
    if (typeof(t[idx]['remove']) == 'function') t[idx].remove();        // JQuery
    delete t[idx];
    if (pop) {
      t.pop();
    } else {
      t.shift();
    }
    return true;
  },
  
  /**
   * @desc manage remove objects
   **/
  _clear: function(id, list, last, first){
    var k, n, i;
    if (!list || !list.length){
      list = [];
      for(k in this._ids[ id ].stored) 
        list.push(k);
    }
    for(k in list){
      n = list[k].toLowerCase();
      if (!this._ids[ id ].stored[n]) continue;
      if (last){
        this._unstore(id, n, true);
      } else if (first){
        this._unstore(id, n, false);
      } else {
        while (this._unstore(id, n, false));
      }
    }
  },
  
  /**
   * @desc return true if "init" action must be run
   **/
  _autoInit: function(name){
    var k,
        fl = name.substr(0,1),
        names = [
          'init', 
          'geolatlng', 
          'getlatlng', 
          'getroute',
          'getelevation', 
          'addstyledmap', 
          'destroy'
        ];
    if ( !name || ( (fl != ':') && (fl != '_') ) ) return true;
    name = name.substr(1); // remove ':' & '_' : public & private
    for(k in names){
      if (names[k] == name) return false;
    }
    return true;
  },
  /**
   * @desc call functions associated
   * @param
   *  id      : string
   *  action  : string : function wanted
   *     
   *  options : {}
   *     
   *    O1    : {}
   *    O2    : {}
   *    ...
   *    On    : {}
   *      => On : option : {}
   *          action : string : function name
   *          ... (depending of functions called)
   *             
   *  args    : [] : parameters for directs call to map
   *  target? : object : replace map to call function 
   **/
  _proceed: function(id, data){
    data = data || {};
    var action = this._ival(data, 'action') || ':init',
        iaction = action.toLowerCase(),
        fl = action.substr(0,1),
        ok = true,
        target, map;
    if (fl == '_') return; // private function
    if ( !this._exist(id) && this._autoInit(iaction) ){
      this.init(id, jQuery.extend({}, this._default['init'], data['args'] && data['args']['map'] ? data['args']['map'] : data['map'] ? data['map'] : {}), true);
    }
    if (fl == ':'){
      // framework functions
      action = iaction.substr(1);
      if (typeof(this[action]) == 'function'){ 
        this[action](id, jQuery.extend({}, this._default[action], data['args'] ? data['args'] : data)); // call fnc and extends defaults data
      } else ok = false;
    } else {
      // target of a direct call
      target = this._ival(data, 'target');
      if (target){
        if (typeof(target[action]) == 'function'){
          data['out'] = target[action].apply(target, data['args'] ? data['args'] : []);
        } else ok = false;
      // gm direct function :  no result so not rewrited, directly wrapped using array "args" as parameters (ie. setOptions, addMapType, ...)
      } else {
        map = this._getMap(id);
        if (typeof(map[action]) == 'function'){
          data['out'] = map[action].apply(map, data['args'] ? data['args'] : [] );
        } else ok = false;
      }
      if (!ok && this._default['verbose']) alert("unknown action : " + action);
      this._callback(id, data['out'], data);
      this._end(id);
    }
  },
  
  /**
   * @desc call a function of framework or google map object of the instance
   * @param
   *  id      : string : instance
   *  fncName : string : function name
   *  ... (depending on function called)
   **/
  _call: function(/* id, fncName [, ...] */){
    if (arguments.length < 2) return;
    if (!this._exist(arguments[0])) return ;
    var i, id = arguments[0],
        fname = arguments[1],
        map = this._getMap(id),
        args = [];
    if (typeof(map[ fname ]) != 'function') return;
    for(i=2; i<arguments.length; i++){
      args.push(arguments[i]);
    }
    return map[ fname ].apply( map, args );
  },
  /**
   * @desc convert data to array
   **/
  _array: function(mixed){
    var k, a = [];
    if (typeof(mixed) =='object')
      for(k in mixed) a.push(mixed[k]);
    else if (typeof(mixed) != 'undefined')
      a.push(mixed);
    return a;
  },
  
  /**
   * @desc init if not and manage map subcall (zoom, center)
   **/
  _subcall: function(id, data, latLng){
    var opts = {};
    if (!data['map']) return;
    if (!latLng) {
      latLng = this._ival(data['map'], 'latlng');
    }
    if (!this._exist(id)){
      if (latLng) {
        opts = {center:latLng};
      }
      this.init(id, jQuery.extend({}, data['map'], opts), true);
    } else { 
      if (data['map']['center'] && latLng) this._call(id, "setCenter", latLng);
      if (typeof(data['map']['zoom']) != 'undefined') this._call(id, "setZoom", data['map']['zoom']);
      if (typeof(data['map']['mapTypeId'])!= 'undefined') this._call(id, "setMapTypeId", data['map']['mapTypeId']);
    }
  },
  
  /**
   * @desc attach an event to a sender (once) 
   **/
  _attachEvent: function(id, sender, name, f, once){
    var $o = this._jObject(id);
    google.maps.event['addListener'+(once?'Once':'')](sender, name, function(event) {
      f($o, sender, event);
    });
  },
  
  /**
   * @desc attach events from a container to a sender 
   * ctnr[
   *  events => { eventName => function, }
   *  onces  => { eventName => function, }      
   * ]
   **/
  _attachEvents : function(id, sender, ctnr){
    var name;
    if (!ctnr) return
    if (ctnr['events']){
      for(name in ctnr['events']){
        if (typeof(ctnr['events'][name]) == 'function'){
          this._attachEvent(id, sender, name, ctnr['events'][name], false);
        }
      }
    }
    if (ctnr['onces']){
      for(name in ctnr['onces']){
        if (typeof(ctnr['onces'][name]) == 'function'){
          this._attachEvent(id, sender, name, ctnr['onces'][name], true);
        }
      }
    }
  },
  
  /**
   * @desc execute callback functions 
   **/
  _callback: function(mixed, result, ctnr){
    var k, $j;
    ctnr['out'] = result;
    if (typeof(ctnr['callback']) == 'function') {
      $j = typeof(mixed) == 'string' ? this._jObject(mixed) : mixed;
      ctnr['callback']($j, result);
    } else if (typeof(ctnr['callback']) == 'object') {
      for(k in ctnr['callback']){
        if (!$j) $j = typeof(mixed) == 'string' ? this._jObject(mixed) : mixed;
        if (typeof(ctnr['callback'][k]) == 'function') ctnr['callback'][k]($j, result);
      }
    }
  },
  
  /**
   * @desc execute end functions 
   **/
  _manageEnd: function(id, sender, data, internal){
    var k, c;
    if (typeof(sender) == 'object'){
      this._attachEvents(id, sender, data);
      for(k in data['apply']){
        c = data['apply'][k];
        if(!c['action']) continue;
        if (typeof(sender[c['action']]) != 'function') continue;
        if (c['args']) {
          sender[c['action']].apply(sender, c['args']);
        } else {
          sender[c['action']]();
        }
      }
    }
    if (!internal) {
      this._callback(id, sender, data);
      this._end(id);
    }
  },
  
  /**
   *  @desc convert mixed [ lat, lng ] objet by google.maps.LatLng
   **/
  _latLng: function(mixed, emptyReturnMixed, noFlat){
    var k, empty, latLng={}, i=0;
    if (!mixed) return null;
    if (mixed['latLng']) {
      return this._latLng(mixed['latLng']);
    }
    empty = emptyReturnMixed ? mixed : null;
    if (typeof(mixed['lat']) == 'function') {
      return mixed;
    } else if (typeof(mixed['lat']) == 'number') {
      return new google.maps.LatLng(mixed['lat'], mixed['lng']);
    } else if ( !noFlat ){
      for(k in mixed){
        if (typeof(mixed[k]) != 'number') return empty;
        latLng[i==0?'lat':'lng'] = mixed[k];
        if (i) break;
        i++;
      }
      if (i) return new google.maps.LatLng(latLng['lat'], latLng['lng']);
    }
    return empty;
  },
  
  _count: function(mixed){
    var k, c = 0;
    for(k in mixed) c++;
    return c;
  },
  
  /**
   * @desc convert mixed [ sw, ne ] object by google.maps.LatLngBounds
   **/
  _latLngBounds: function(mixed, flatAllowed, emptyReturnMixed){
    var empty, cnt, ne, sw, k, t, ok, nesw, i;
    if (!mixed) return null;
    empty = emptyReturnMixed ? mixed : null;
    if (typeof(mixed['getCenter']) == 'function') return mixed;
    cnt = this._count(mixed);
    if (cnt == 2){
      if (mixed['ne'] && mixed['sw']){
        ne = this._latLng(mixed['ne']);
        sw = this._latLng(mixed['sw']);
      } else {
        for(k in mixed){
          if (!ne) {
            ne = this._latLng(mixed[k]);
          } else {
            sw = this._latLng(mixed[k]);
          }
        }
      }
      if (sw && ne) return new google.maps.LatLngBounds(sw, ne);
      return empty;
    } else if (cnt == 4){
      t = ['n', 'e', 's', 'w'];
      ok=true;
      for(i in t) ok &= typeof(mixed[t[i]]) == 'number';
      if (ok) return new google.maps.LatLngBounds(this._latLng([mixed['s'], mixed['w']]), this._latLng([mixed['n'], mixed['e']]));
      if (flatAllowed){
        i=0;
        nesw={};
        for(k in mixed){
          if (typeof(mixed[k]) != 'number') return empty;
          nesw[t[i]] = mixed[k];
          i++;
        }
        return new google.maps.LatLngBounds(this._latLng([nesw['s'], nesw['w']]), this._latLng([nesw['n'], nesw['e']]));
      }
    }
    return empty;
  },
  
  /**
   * @desc search an (insensitive) key
   **/
  _ikey: function(object, key){
    key = key.toLowerCase();
    for(var k in object){
      if (k.toLowerCase() == key) return k;
    }
    return false;
  },
  
  /**
   * @desc search an (insensitive) key
   **/
  _ival: function(object, key){
    var k = this._ikey(object, key);
    if ( k ) return object[k];
    return null;
  },
  
  /**
   * @desc return true if at least one key is set in object
   * nb: keys in lowercase
   **/
  _hasKey: function(object, keys){
    var n, k;
    if (!object || !keys) return false;
    for(n in object){
      n = n.toLowerCase();
      for(k in keys){
        if (n == keys[k]) return true;
      }
    }
    return false;
  },
  
  /**
   * @desc return a standard object
   * nb: include in lowercase
   **/
  _extractObject: function(data, include){
    if (this._hasKey(data, this._properties) || this._hasKey(data, include)){
      var k, p, ip, r={};
      for(k in this._properties){
        p=this._properties[k];
        ip = this._ikey(data, p);
        r[p] = ip ? data[ip] : {};
      }
      for(k in include){
        p=include [k];
        ip = this._ikey(data, p);
        if (ip) r[p] = data[ip];
      }
      return r;
    } else {
      r = { options : {} };
      for(k in data){
        if (k == 'action') continue;
        r.options[k] = data[k];
      }
      return r;
    }
  },
  
  /**
   * @desc identify object from object list or parameters list : [ objectName:{data} ] or [ otherObject:{}, ] or [ object properties ]
   * nb: include, exclude in lowercase
   **/
  _object: function(name, data, include, exclude){
    var k = this._ikey(data, name),
        p, r = {}, keys=['map'];
    if (k) return this._extractObject(data[k], include);
    for(k in exclude) keys.push(exclude[k]);
    if (!this._hasKey(data, keys)) r = this._extractObject(data, include);
    for(k in this._properties){
      p=this._properties[k];
      if (!r[p]) r[p] = {};
    }
    return r;
  },
  
  /**
   * @desc Returns the geographical coordinates from an address and call internal method
   **/
  _resolveLatLng: function(id, data, method, all){
    var address = this._ival(data, 'address'),
        that = this, cb;
    if ( address ){
        cb = function(results, status) {
        if (status == google.maps.GeocoderStatus.OK){
          that[method](id, data, all ? results : results[0].geometry.location);
        } else {
          that[method](id, data, false);
        }
      };
      this._getGeocoder().geocode( { 'address': address }, cb );
    } else {
      this[method](id, data, this._latLng(data, false, true));
    }
  },
  
  /*============================*/
  /*          PUBLIC            */
  /*============================*/
  
  /**
   * @desc Destroy an existing instance
   **/
  destroy: function(id, data){
    var k, $j;
    if (this._ids[ id ]){
      this._clear(id);
      this._ids[ id ].$this.empty();
      if (this._ids[ id ].bl) delete this._ids[ id ].bl;
      for(k in this._ids[ id ].styles){
        delete this._ids[ id ].styles[ k ];
      }
      delete this._ids[ id ].map;
      $j = this._jObject(id);
      delete this._ids[ id ];
      this._callback($j, null, data);
    }
    this._end(id);
  },
  
  /**
   * @desc Initialize google map object an attach it to the dom element (using id)
   **/
  init: function(id, data, internal){
    var o, opts, map, styles, k; 
    if ( (id == '') || (this._exist(id)) ) return this._end(id);
    o = this._object('map', data);
    if ( (typeof(o['options']['center']) == 'boolean') && o['options']['center']) return false; // wait for an address resolution
    opts = jQuery.extend({}, this._default['init'], o['options']);
    if (!opts['center']) opts['center'] = [this._default.init['center']['lat'], this._default.init['center']['lng']];
    opts['center'] = this._latLng(opts['center']);
    
    this._setMap(id, new google.maps.Map(document.getElementById(id), opts));
    map = this._getMap(id);
    
    // add previous added styles
    styles = this._getStyles( id );
    for(k in styles) map.mapTypes.set(k, styles[k]);
    
    this._manageEnd(id, map, o, internal);
    return true;
  },
  
  /**
   * @desc Returns the geographical coordinates from an address
   **/
  getlatlng: function(id, data){
    this._resolveLatLng(id, data, '_getLatLng', true);
  },
  _getLatLng: function(id, data, results){
    this._manageEnd(id, results, data);
  },
  
  /**
   * @desc Return a route
   **/
  getroute: function(id, data){
    var $this, callback;
    if ( (typeof(data['callback']) == 'function') && data['options'] ) {
      data['options']['origin']      = this._latLng(data['options']['origin'], true);
      data['options']['destination'] = this._latLng(data['options']['destination'], true);
      $this = this._jObject(id);
      callback = function(results, status) {
        data['out'] = status == google.maps.DirectionsStatus.OK ? results : false;
        data['callback']($this, data['out']);
      };
      this._getDirectionsService().route( data['options'], callback );
    }
    this._end(id);
  },
  
  getelevation: function(id, data){
    var $this, callback, latLng, ls, k, path, samples,
        locations = [],
        cb = this._ival(data, 'callback');
    if (cb && typeof(cb) == 'function') {
      $this = this._jObject(id);
      callback = function(results, status) {
        data['out'] = status == google.maps.ElevationStatus.OK ? results : false;
        data['callback']($this, data['out']);
      };
      latLng = this._ival(data, 'latlng')
      if ( latLng ) 
        locations.push( this._latLng(latLng) )
      else {
        ls = this._ival(data, 'locations')
        if ( ls ){
          for(k in ls){
            locations.push( this._latLng(ls[k]) );
          }
        }
      }
      if ( locations.length ){
        this._getElevationService().getElevationForLocations( {locations:locations}, callback );
      } else {
        path = this._ival(data, 'path');
        samples = this._ival(data, 'samples');
        if (path && samples){
          for(k in path){
            locations.push( this._latLng(path[k]) );
          }
          if ( locations.length ){
            this._getElevationService().getElevationAlongPath( {path:locations, samples:samples}, callback );
          }
        }
      }
    }
    this._end(id);
  },
  
  /**
   * @desc Add a marker to a map after address resolution
   * if [infowindow] add an infowindow attached to the marker   
   **/
  addmarker: function(id, data){
    this._resolveLatLng(id, data, '_addMarker');
  },
  _addMarker: function(id, data, latLng ){
    var o, marker, oi,
        n = 'marker', niw = 'infowindow';
    if (!latLng) {
      return this._end(id);
    }
    this._subcall(id, data, latLng);
    
    o = this._object(n, data);
    o['options']['position'] = latLng;
    o['options']['map'] = this._getMap(id);
    marker = new google.maps.Marker(o['options']);
    if ( data[niw] ){
      oi = this._object(niw, data[niw], ['open']);
      if ( (typeof(oi['open']) == 'undefined') || oi['open'] ) {
        oi['apply'] = this._array(oi['apply']);
        oi['apply'].unshift({action:'open', args:[this._getMap(id), marker]});
      }
      oi['action'] = ':add'+niw;
      this._planNext(id, oi); 
    }
    this._store(id, n, marker);
    this._manageEnd(id, marker, o);
  },
  
  /**
   * @desc Add markers to a map without address resolution : need latLng : [ "latLng", "latLng", ...]
   **/
  addmarkers: function(id, data){
    var o, k, latLng, marker, markers = [], 
        n = 'marker',
        lstLatLng = this._ival(data, 'latlng');
    this._subcall(id, data);
    if (typeof(lstLatLng) != 'object') {
      return this._end(id);
    }
    o = this._object(n, data);
    o['options']['map'] = this._getMap(id);
    for(k in lstLatLng){
      latLng = this._latLng(lstLatLng[k]);
      if (!latLng) continue;
      o['options']['position'] = latLng;
      marker = new google.maps.Marker(o['options']);
      markers.push(marker);
      this._store(id, n, marker);
      this._manageEnd(id, marker, o, true);
    }
    this._callback(id, markers, data);
    this._end(id);
  },
  
  /**
   * @desc Add an infowindow after address resolution
   **/
  addinfowindow: function(id, data){ 
    this._resolveLatLng(id, data, '_addInfoWindow');
  },
  _addInfoWindow: function(id, data, latLng){
    var o, infowindow, args = [],
        n = 'infowindow';
    this._subcall(id, data, latLng);
    o = this._object(n, data, ['open', 'anchor']);
    if (latLng) {
      o['options']['position'] = latLng;
    }
    infowindow = new google.maps.InfoWindow(o['options']);
    if ( (typeof(o['open']) == 'undefined') || o['open'] ){
      o['apply'] = this._array(o['apply']);
      args.push(this._getMap(id));
      if (o['anchor']){
        args.push(o['anchor']);
      }
      o['apply'].unshift({action:'open', args:args});
    }
    this._store(id, n, infowindow);
    this._manageEnd(id, infowindow, o);
  },
  
  /**
   * @desc add a polygone / polylin on a map
   **/
  addpolyline: function(id, data){
    this._addPoly(id, data, 'Polyline', 'path');
  },
  addpolygon: function(id, data){
    this._addPoly(id, data, 'Polygon', 'paths');
  },
  _addPoly: function(id, data, poly, path){
    var k, i, obj, o = this._object(poly.toLowerCase(), data, [path]);
    if (o[path]){
      o['options'][path] = [];
      i = 0; 
      for(k in o[path]){
        o['options'][path][i++] = this._latLng(o[path][k]);
      }
    }
    obj = new google.maps[poly](o['options']);
    obj.setMap(this._getMap(id));
    this._store(id, poly.toLowerCase(), obj);
    this._manageEnd(id, obj, o);
  },
  
  /**
   * @desc add a circle   
   **/
  addcircle: function(id, data){
    this._resolveLatLng(id, data, '_addCircle');
  },
  _addCircle: function(id, data, latLng ){
    var c, n = 'circle',
        o = this._object(n, data);
    if (!latLng) latLng = this._latLng(o['options']['center']);
    if (!latLng) return this._end(id);
    this._subcall(id, data, latLng);
    o['options']['center'] = latLng;
    o['options']['map'] = this._getMap(id);
    c = new google.maps.Circle(o['options']);
    this._store(id, n, c);
    this._manageEnd(id, c, o);
  },
  
  /**
   * @desc add a rectangle   
   **/
  addrectangle: function(id, data){
    this._resolveLatLng(id, data, '_addRectangle');
  },
  _addRectangle: function(id, data, latLng ){
    var r, n = 'rectangle',
        o = this._object(n, data);
    o['options']['bounds'] = this._latLngBounds(o['options']['bounds'], true);
    if (!o['options']['bounds']) return this._end(id);
    this._subcall(id, data, o['options']['bounds'].getCenter());
    o['options']['map'] = this._getMap(id);
    r = new google.maps.Rectangle(o['options']);
    this._store(id, n, r);
    this._manageEnd(id, r, o);
  },
  
  /**
   * @desc add an overlay to a map after address resolution
   **/
  addoverlay: function(id, data){
    this._resolveLatLng(id, data, '_addOverlay');
  },
  _addOverlay: function(id, data, latLng){
    var ov,  
        o = this._object('overlay', data),
        opts =  jQuery.extend({
                  pane: 'floatPane',
                  content: '',
                  offset:{
                    x:0,y:0
                  }
                },
                o['options']);
  
    f.prototype = new google.maps.OverlayView();
    
    function f(opts, latLng, map) {
      this.opts_ = opts;
      this.$div_ = null;
      this.latLng_ = latLng;
      this.map_ = map;
      this.setMap(map);
    }
    f.prototype.onAdd = function() {
      var panes,
          $div = jQuery('<div></div>');
      $div
        .css('border', 'none')
        .css('borderWidth', '0px')
        .css('position', 'absolute');
      $div.append(jQuery(this.opts_['content']));
      this.$div_ = $div;
      panes = this.getPanes();
      if (panes[this.opts_['pane']]) jQuery(panes[this.opts_['pane']]).append(this.$div_);
    }
    f.prototype.draw = function() {
      if (!this.$div_) return;
      var ps, overlayProjection = this.getProjection();
      ps = overlayProjection.fromLatLngToDivPixel(this.latLng_);
      this.$div_
        .css('left', (ps.x+this.opts_['offset']['x']) + 'px')
        .css('top' , (ps.y+this.opts_['offset']['y']) + 'px');
    }
    f.prototype.onRemove = function() {
      this.$div_.remove();
      this.$div_ = null;
    }
    f.prototype.hide = function() {
      if (this.$div_) this.$div_.hide();
    }
    f.prototype.show = function() {
      if (this.$div_) this.$div_.show();
    }
    f.prototype.toggle = function() {
      if (this.$div_) {
        if (this.$div_.is(':visible')){
          this.show();
        } else {
          this.hide();
        }
      }
    }
    f.prototype.toggleDOM = function() {
      if (!this.$div_) return;
      if (this.getMap()) {
        this.setMap(null);
      } else {
        this.setMap(this.map_);
      }
    }
    ov = new f(opts, latLng, this._getMap(id));
    this._store(id, 'overlay', ov);
    this._manageEnd(id, ov, o);
  },
  
  /**
   * @desc add fixed panel to a map
   **/
  addfixpanel: function(id, data){
    var n = 'fixpanel',
        o = this._object(n, data),
        x=0, y=0, $c, $div;
    if (o['options']['content']){
      $c = jQuery(o['options']['content']);
      
      if (typeof(o['options']['left']) != 'undefined'){
        x = o['options']['left'];
      } else if (typeof(o['options']['right']) != 'undefined'){
        x = this._jObject(id).width() - $c.width() - o['options']['right'];
      } else if (o['options']['center']){
        x = (this._jObject(id).width() - $c.width()) / 2;
      }
      
      if (typeof(o['options']['top']) != 'undefined'){
        y = o['options']['top'];
      } else if (typeof(o['options']['bottom']) != 'undefined'){
        y = this._jObject(id).height() - $c.height() - o['options']['bottom'];
      } else if (o['options']['middle']){
        y = (this._jObject(id).height() - $c.height()) / 2
      }
    
      $div = jQuery('<div></div>')
              .css('position', 'absolute')
              .css('top', y+'px')
              .css('left', x+'px')
              .css('z-index', '1000')
              .append(o['options']['content']);
      
      this._jObject(id).first().prepend($div);
      this._attachEvents(id, this._getMap(id), o);
      this._store(id, n, $div);
      this._callback(id, $div, o);
    }
    this._end(id);
  },
  
  /**
   * @desc Remove a direction renderer
   * deprecated   
   **/
  removedirectionsrenderer: function(id, data, internal){
    var o = this._object('directionrenderer', data);
    this._clear(id, 'directionrenderer');
    this._manageEnd(id, true, o, internal);
  },
  
  /**
   * @desc Add a direction renderer to a map
   **/
  adddirectionsrenderer: function(id, data, internal){
    var n = 'directionrenderer',
        dr, o = this._object(n, data, ['panelId']);
    this._clear(id, n);
    o['options']['map'] = this._getMap(id);
    dr = new google.maps.DirectionsRenderer(o['options']);
    if (o['panelId']) {
      dr.setPanel(document.getElementById(o['panelId']));
    }
    this._store(id, n, dr);
    this._manageEnd(id, dr, o, internal);
  },
  
  /**
   * @desc Set direction panel to a dom element from it ID
   **/
  setdirectionspanel: function(id, data){
    var dr, o = this._object('directionpanel', data, ['id']);
    if (o['id']) {
      dr = this._getDirectionRenderer(id);
      dr.setPanel(document.getElementById(o['id']));
    }
    this._manageEnd(id, dr, o);
  },
  
  /**
   * @desc Set directions on a map (create Direction Renderer if needed)
   **/
  setdirections: function(id, data){
    var dr, o = this._object('directions', data);
    if (data) o['options']['directions'] = data['directions'] ? data['directions'] : (data['options'] && data['options']['directions'] ? data['options']['directions'] : null);
    if (o['options']['directions']) {
      dr = this._getDirectionRenderer(id);
      if (!dr) {
        this.adddirectionsrenderer(id, o, true);
        dr = this._getDirectionRenderer(id);
      } else {
        dr.setDirections(o['options']['directions']);
      }
    }
    this._manageEnd(id, dr, o);
  },
  
  /**
   * @desc set a streetview to a map
   **/
  setstreetview: function(id, data){
    var o = this._object('streetview', data, ['id']),
        panorama = new google.maps.StreetViewPanorama(document.getElementById(o['id']),o['options']);
    this._getMap(id).setStreetView(panorama);
    this._manageEnd(id, panorama, o);
  },
  
  /**
   * @desc add a kml layer to a map
   **/
  addkmllayer: function(id, data){
    var kml, o = this._object('kmllayer', data, ['url']);
    o['options']['map'] = this._getMap(id);
    kml = new google.maps.KmlLayer(o['url'], o['options']);
    this._manageEnd(id, kml, data);
  },
  
  /**
   * @desc add a traffic layer to a map
   **/
  addtrafficlayer: function(id, data){
    var n = 'trafficlayer', 
        o = this._object(n),
        tl = this._getFirstStored(id, n);
    if (!tl){
      tl = new google.maps.TrafficLayer();
      tl.setMap(this._getMap(id));
      this._store(id, n, tl);
    }
    this._manageEnd(id, tl, o);
  },
  
  /**
   * @desc remove a traffic layer from a map
   * deprecated   
   **/
  removetrafficlayer: function(id, data){
    var n = 'trafficlayer',
        o = this._object(n),
        tl = this._getFirstStored(id, n),
        r = tl ? true : false;
    if (tl) this._clear(id, n);
    this._manageEnd(id, r, o);
  },
  
  /**
   * @desc set a bicycling layer to a map
   **/
  addbicyclinglayer: function(id, data){
    var n = 'bicyclinglayer',
        o = this._object(n),
        bl = this._getFirstStored(id, n);
    if (!bl){
      bl = new google.maps.BicyclingLayer();
      bl.setMap(this._getMap(id));
      this._store(id, n, bl);
    }
    this._manageEnd(id, bl, o);
  },
  
  /**
   * @desc remove a bicycling layer from a map
   * deprecated   
   **/
  removebicyclinglayer: function(id, data){
    var n = 'bicyclinglayer',
        o = this._object(n),
        bl = this._getFirstStored(id, n),
        r = bl ? true : false;
    if (bl) this._clear(id, n);
    this._manageEnd(id, r, o);
  },
  
  
  /**
   * @desc add a ground overlay to a map
   **/
  addgroundoverlay: function(id, data){
    var n = 'groundoverlay',
        o = this._object(n, data, ['bounds', 'url']),
        ov;
    o['bounds'] = this._latLngBounds(o['bounds']);
    if (o['bounds'] && o['url']){
      ov = new google.maps.GroundOverlay(o['url'], o['bounds']);
      ov.setMap(this._getMap(id));
      this._store(id, n, ov);
    }
    this._manageEnd(id, ov, o);
  },
  
  /**
   * @desc Geolocalise the user and return a LatLng
   **/
  geolatlng: function(id, data){
    if (typeof(data['callback']) == 'function') {
      var geo, $this = this._jObject(id);
      if(navigator.geolocation) {
        browserSupportFlag = true;
        navigator.geolocation.getCurrentPosition(function(position) {
          data['out'] = new google.maps.LatLng(position.coords.latitude,position.coords.longitude);
          data['callback']($this, data['out']);
        }, function() {
          data['out'] = false;
          data['callback']($this, data['out']);
        });
      } else if (google.gears) {
        browserSupportFlag = true;
        geo = google.gears.factory.create('beta.geolocation');
        geo.getCurrentPosition(function(position) {
          data['out'] = new google.maps.LatLng(position.latitude,position.longitude);
          data['callback']($this, data['out']);
        }, function() {
          data['out'] = false;
          data['callback']($this, data['out']);
        });
      } else {
          data['out'] = false;
          data['callback']($this, data['out']);
      }
    }
    this._end(id);
  },
  
  /**
   * @desc Add a style to a map
   **/
  addstyledmap: function(id, data, internal){
    var o = this._object('styledmap', data, ['id', 'style']),
        style;
    if  (o['style'] && o['id'] && !this._styleExist(id, o['id'])) {
      style = new google.maps.StyledMapType(o['style'], o['options']);
      this._addStyle(id, o['id'], style);
      if (this._getMap(id)) this._getMap(id).mapTypes.set(o['id'], style);
    }
    this._manageEnd(id, style, o, internal);
  },
  
  /**
   * @desc Set a style to a map (add it if needed)
   **/
  setstyledmap: function(id, data){
    var o = this._object('styledmap', data, ['id', 'style']),
        style;
    if (o['id']) {
      this.addstyledmap(id, o, true);
      style = this._getStyle(id, o['id']);
      if (style) {
        this._getMap(id).setMapTypeId(o['id']);
        this._callback(id, style, data);
      }
    }
    this._manageEnd(id, style, o);
  },
  
  /**
   * @desc Remove objects from a map
   **/
  clear: function(id, data){
    var list = this._array(data['list']),
        last = data['last'] ? true : false,
        first = data['first'] ? true : false;
    this._clear(id, list, last, first);
    this._end(id);
  },
  
  /**
   * @desc Return Google object(s) wanted
   **/
  get: function(id, data){
    var name = this._ival(data, 'name') || 'map',
        first= this._ival(data, 'first'),
        all  = this._ival(data, 'all'),
        r, k;
    name = name.toLowerCase();
    if (name == 'map'){
      return this._getMap(id);
    }
    if (first){
      return this._getFirstStored(id, name);
    } else if (all){
      r = new Array();
      if (this._ids[ id ].stored[name]){
        for(k in this._ids[ id ].stored[name]){
          if (this._ids[ id ].stored[name][k]){
            r.push(this._ids[ id ].stored[name][k]);
          }
        }
      }
      return r;
    } else {
      return this._getLastStored(id, name);
    }
  },
  
  /**
   * @desc modify default values
   **/
  setDefault: function(d){
    for(var k in d){
      this._default[k] = jQuery.extend({}, this._default[k], d[k]);
    }
  }
};


jQuery.fn.extend({
  gmap3: function(){
    var id, i, 
        todo = [],
        $this = jQuery(this);
    if ($this.length > 0){
      id = $this.attr('id');
      for(i=0; i<arguments.length; i++){
        todo.push(arguments[i] || {});
      }         
      if (!todo.length) todo.push({});
      if ( (todo.length == 1) && (jQuery.gmap3._isDirect(id, todo[0])) ){
        return jQuery.gmap3._direct(id, todo[0]);
      } else {
        jQuery.gmap3._plan($this, id, todo);
      }
    }
    return jQuery(this);
  }	
});
