/*
 *  GMAP3 Plugin for JQuery 
 *  Version   : 
 *  Date      : 
 *  Licence   : GPL v3 : http://www.gnu.org/licenses/gpl.html  
 *  Author    : DEMONTE Jean-Baptiste
 *  Contact   : jbdemonte@gmail.com
 *  Web site  : http://gmap3.net
 *   
 *  Copyright (c) 2010-2011 Jean-Baptiste DEMONTE
 *  All rights reserved.
 *   
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above 
 *     copyright notice, this list of conditions and the following 
 *     disclaimer in the documentation and/or other materials provided 
 *     with the distribution.
 *   - Neither the name of the author nor the names of its contributors 
 *     may be used to endorse or promote products derived from this 
 *     software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 */
 
(function ($) {
  
  /***************************************************************************/
  /*                                STACK                                    */
  /***************************************************************************/
  function Stack (){
    var st={};
    this.init = function (id){
      if (!st[id]){
        st[id] = [];
      }
    }
    this.empty = function (id){
      var k;
      if (!st[id]) {
        return true;
      }    
      for(k in st[id]){
        if (st[id][k]){
          return false
        }
      }
      return true;
    }
    this.add = function (id, v){
      this.init(id);
      st[id].push(v);
    }
    this.addNext = function (id, v){
      var t=[], i=0, k;
      this.init(id);
      for(k in st[id]){
        if (i == 1) {
          t.push(v);
        }
        t.push(st[id][k]);
        i++;
      }
      if (i < 2) {
        t.push(v);
      }
      st[id] = t;
    }
    this.get = function (id){
      var k;
      if (st[id]){
        for(k in st[id]){
          if (st[id][k]) return st[id][k];
        }
      }
      return false;
    }
    this.ack = function (id){
      var k;
      if (st[id]) {
        for(k in st[id]){                     
          if (st[id][k]) {
            delete st[id][k];
            break;
          }
        }
        if (this.empty(id)){
          delete st[id];
        }
      }
    }
  }
  /***************************************************************************/
  /*                              CLUSTERER                                  */
  /***************************************************************************/

  function Clusterer(){
    var markers = [], events=[], dom=[], latest=[], redrawing = false, redraw;
    
    this.events = function(){
      for(var i=0; i<arguments.length; i++){
        events.push(arguments[i]);
      }
    }
    
    this.startRedraw = function(){
      if (!redrawing){
        redrawing = true;
        return true;
      }
      return false;
    }
    
    this.endRedraw = function(){
      redrawing = false;
    }
    
    this.redraw = function(){
      var i, args = [], that = this; 
      for(i=0; i<arguments.length; i++){
        args.push(arguments[i]);
      }
      if (this.startRedraw){
        redraw.apply(that, args);
        this.endRedraw();
      } else {
        setTimeout(function(){
            that.redraw.apply(that, args);
          },
          50
        );
      }
    };
    
    this.setRedraw = function(fnc){
      redraw  = fnc;
    }
    
    this.store = function(obj){
      dom.push(obj);
    }
    
    this.free = function(){
      for(var k in events){
        google.maps.event.removeListener(events[k]);
      }
      events=[];
      this.freeDom();
    }
    
    this.freeDom = function(){
      var i, j;
      for(i in dom){
        if (typeof(dom[i].setMap) === 'function') dom[i].setMap(null);
        if (typeof(dom[i].remove) === 'function') dom[i].remove();
        delete dom[i];
      }
      dom = [];
    }
    
    this.add = function(latLng, marker){
      markers.push({latLng:latLng, marker:marker});
    }
    
    this.get = function(i){
      return markers[i];
    }
    
    this.clusters = function(map, radius, force){
      var proj = map.getProjection(),
          nwP = proj.fromLatLngToPoint(
            new google.maps.LatLng(
                map.getBounds().getNorthEast().lat(),
                map.getBounds().getSouthWest().lng()
            )
          ),
          i, j, j2, p, x, y, k, k2, z = map.getZoom(),
          pos = {}, 
          saved = {},
          unik = {},
          clusters=[],
          cluster,
          chk,
          lat, lng, keys, cnt,
          bounds = map.getBounds();
      
      cnt = 0;
      keys = {};
      for(i in markers){
        if (!bounds.contains(markers[i].latLng)){
          continue;
        }
        p = proj.fromLatLngToPoint(markers[i].latLng);
        pos[i] = [
          Math.floor((p.x - nwP.x) * Math.pow(2, z)),
          Math.floor((p.y - nwP.y) * Math.pow(2, z))
        ];
        keys[i] = true;
        cnt++;
      }
      // check if visible markers have changed 
      if (!force){
        for(k in latest){
          if( k in keys ){
            cnt--;
          } else {
            break;
          }
        }
        if (!cnt){
          return false; // no change
        }
      }
      
      // save current keys to check later if an update has been done 
      latest = keys;
      
      keys = [];
      for(i in pos){
        x = pos[i][0];
        y = pos[i][1];
        if ( !(x in saved) ){
          saved[x] = {};
        }
        if (!( y in saved[x]) ) {
          saved[x][y] = i;
          unik[i] = {};
          keys.push(i);
        }
        unik[ saved[x][y] ][i] = true;
      }
      radius = Math.pow(radius, 2);
      delete(saved);
      
      k = 0;
      while(1){
        while((k <keys.length) && !(keys[k] in unik)){
          k++;
        }
        if (k == keys.length){
          break;
        }
        i = keys[k];
        lat = pos[i][0];
        lng = pos[i][1];
        saved = null;
        
        do{
          cluster = {lat:0, lng:0, idx:[]};
          for(k2 = k; k2<keys.length; k2++){
            if (!(keys[k2] in unik)){
              continue;
            }
            j = keys[k2];
            if ( Math.pow(lat - pos[j][0], 2) + Math.pow(lng-pos[j][1], 2) <= radius ){
              for(j2 in unik[j]){
                cluster.lat += markers[j2].latLng.lat();
                cluster.lng += markers[j2].latLng.lng();
                cluster.idx.push(j2);
              }
            }
          }
          cluster.lat /= cluster.idx.length;
          cluster.lng /= cluster.idx.length;
          if (!saved){
            chk = cluster.idx.length > 1;
            saved = cluster;
          } else {
            chk = cluster.idx.length > saved.idx.length;
            if (chk){
              saved = cluster;
            }
          }
          if (chk){
            p = proj.fromLatLngToPoint( new google.maps.LatLng(saved.lat, saved.lng) );
            lat = Math.floor((p.x - nwP.x) * Math.pow(2, z));
            lng = Math.floor((p.y - nwP.y) * Math.pow(2, z));
          }
         } while(chk);
         
         for(k2 in saved.idx){
          if (saved.idx[k2] in unik){
            delete(unik[saved.idx[k2]]);
          }
         }
        clusters.push(saved);
      }
      return clusters;
    }
    
    this.getBounds = function(){
      var i, bounds = new google.maps.LatLngBounds();
      for(i in markers){
        bounds.extend(markers[i].latLng);
      }
      return bounds;
    }
  }

  /***************************************************************************/
  /*                                GMAP3                                    */
  /***************************************************************************/
  
  var gmap3 = {
    _ids:{},
    _properties:['events','onces','options','apply', 'callback', 'data', 'tag'],
    
    _default:{
      verbose:false,
      unit: 'mi',
      init:{
        mapTypeId : google.maps.MapTypeId.ROADMAP,
        center:[46.578498,2.457275],
        zoom: 2
      }
    },
    _running:{
    },
    _stack: new Stack(),
    /**
     * @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, todo){
      var action = this._ival(todo, 'action'),
          directs = {
            distance    :true,
            earthradius :true,
            get         :true
          };
      return action in directs;
    },
    /**
     * @desc execute action directly
     **/
    _direct: function(id, todo){
      var action = this._ival(todo, 'action');
      if (action.substr(0,1) == ':'){
        action = action.substr(1);
      }
      return this[action](id, $.extend({}, this._default[action], todo.args ? todo.args : todo));
    }, 
    /**
     * @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);
    },
    
    _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;
    },
    
    _maxZoomService:null,
    _getMaxZoomService: function(){
      if (!this._maxZoomService) this._maxZoomService = new google.maps.MaxZoomService();
      return this._maxZoomService;
    },
    
    _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._getStored(id, 'directionrenderer');
    },
    
    _exist: function(id){
      return this._ids[id].map ? true : false;
    },
    
    /**
     * @desc return last non-null object
     **/
    _getStored: function(id, name, last, tag){
      if (!this._ids[id].stored[name] || !this._ids[id].stored[name].length){
        return null;
      }
      var t = this._ids[id].stored[name],
          i,
          idx = last ? t.length : -1,
          add = last ? -1 : 1;
      for(i=0; i<t.length; i++){
        idx += add;
        if (t[idx]){
          if (tag !== undefined) {
            if ( (t[idx].tag === undefined) || ($.inArray(t[idx].tag, tag) < 0) ){
              continue;
            }
          }
          return t[idx].obj;
        }
      }
      return null;
    },
    
    /**
     * @desc return an object from its reference
     **/
    _getStoredId: function(id, ref){
      ref = ref.split('-');
      if ((ref.length == 2) && this._ids[id].stored[ref[0]] && this._ids[id].stored[ref[0]][ref[1]]){
        return this._ids[id].stored[ref[0]][ref[1]].obj;
      }
      return null;
    },
    
    /**
     * @desc add an object in the stored structure
     **/
    _store: function(id, name, obj, todo){
      name = name.toLowerCase();
      if (!this._ids[id].stored[name])
          this._ids[id].stored[name] = new Array();
      this._ids[id].stored[name].push({obj:obj, tag:this._ival(todo, 'tag')});
      return name + '-' + (this._ids[id].stored[name].length-1);
    },
    
    /**
     * @desc remove an object from the stored structure
     **/
    _unstore: function(id, name, tag, pop){
      var idx, t = this._ids[id].stored[name];
      if (!t) return false;
      if (tag !== undefined){
        if (pop){
          for(idx = t.length - 1; idx >= 0; idx--){
            if ( (t[idx] !== undefined) && (t[idx].tag !== undefined) && ($.inArray(t[idx].tag, tag) >= 0) ){
              break;
            }
          }
        } else {
          for(idx = 0; idx < t.length; idx++){
            if ( (t[idx] !== undefined) && (t[idx].tag !== undefined) && ($.inArray(t[idx].tag, tag) >= 0) ){
              break;
            }
          }
        }
      } else {
        idx = pop ? t.length - 1 : 0;
      }
      if ( !(idx in t) ) {
        return false;
      }
      // Google Map element
      if (typeof(t[idx].obj.setMap) === 'function') {
        t[idx].obj.setMap(null);
      }
      // JQuery
      if (typeof(t[idx].obj.remove) === 'function') {
        t[idx].obj.remove();
      }
      // internal (cluster)
      if (typeof(t[idx].obj.free) === 'function') {
        t[idx].obj.free();
      }
      delete t[idx].obj;
      if (tag !== undefined){
        this._ids[id].stored[name] = this._rmFromArray(t,idx);
      } else {
        if (pop) {
          t.pop();
        } else {
          t.shift();
        }
      }
      return true;
    },
    
    /**
     * @desc manage remove objects
     **/
    _clear: function(id, list, last, first, tag){
      var k, n, i;
      if (!list || !list.length){
        list = [];
        for(k in this._ids[id].stored) 
          list.push(k);
      } else {
        list = this._array(list);
      }
      for(k in list){
        if (list[k].toLowerCase){ // some libraries add properties in Array
          n = list[k].toLowerCase();
          if (!this._ids[id].stored[n]) continue;
          if (last){
            this._unstore(id, n, tag, true);
          } else if (first){
            this._unstore(id, n, tag, false);
          } else {
            while (this._unstore(id, n, tag, 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',
            'setdefault', 
            'destroy'
          ];
      if ( !name ) return true;
      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, todo){
      todo = todo || {};
      var action = this._ival(todo, 'action') || 'init',
          iaction = action.toLowerCase(),
          ok = true,
          target = this._ival(todo, 'target'), 
          args = this._ival(todo, 'args'),
          map, out;
      if ( !this._exist(id) && this._autoInit(iaction) ){
        this.init(id, $.extend({}, this._default.init, todo.args && todo.args.map ? todo.args.map : todo.map ? todo.map : {}), true);
      }
      if (!target && !args && (typeof(this[iaction]) === 'function')){
        // framework functions
        this[iaction](id, $.extend({}, this._default[iaction], todo.args ? todo.args : todo)); // call fnc and extends defaults data
      } else {
        if (target && (typeof(target) === 'object')){
          if (typeof(target[action]) === 'function'){
            out = target[action].apply(target, todo.args ? todo.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'){
            out = map[action].apply(map, todo.args ? todo.args : [] );
          } else ok = false;
        }
        if (!ok && this._default.verbose) alert("unknown action : " + action);
        this._callback(id, out, todo);
        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 (mixed !== undefined){
        if (typeof(mixed) === 'object'){
          for(k in mixed) a.push(mixed[k]);
        } else{ 
          a.push(mixed);
        }
      }
      return a;
    },
    
    /**
     * @desc create a new Array without some entries
     **/
     _rmFromArray: function(a, key){
      var k, r = new Array();
      for(k in a){
        if (k != key){
          r.push(a[k]);
        }
      }
      return r;
     },
    
    /**
     * @desc init if not and manage map subcall (zoom, center)
     **/
    _subcall: function(id, todo, latLng){
      var opts = {};
      if (!todo.map) return;
      if (!latLng) {
        latLng = this._ival(todo.map, 'latlng');
      }
      if (!this._exist(id)){
        if (latLng) {
          opts = {center:latLng};
        }
        this.init(id, $.extend({}, todo.map, opts), true);
      } else { 
        if (todo.map.center && latLng) this._call(id, "setCenter", latLng);
        if (todo.map.zoom !== undefined) this._call(id, "setZoom", todo.map.zoom);
        if (todo.map.mapTypeId !== undefined) this._call(id, "setMapTypeId", todo.map.mapTypeId);
      }
    },
    
    /**
     * @desc attach an event to a sender (once) 
     **/
    _attachEvent: function(id, sender, name, f, data, once){
      var that=this, $o = this._jObject(id);
      google.maps.event['addListener'+(once?'Once':'')](sender, name, function(event) {
        f.apply($o, [sender, event, data]);
      });
    },
    
    /**
     * @desc attach events from a container to a sender 
     * todo[
     *  events => { eventName => function, }
     *  onces  => { eventName => function, }  
     *  data   => mixed data         
     * ]
     **/
    _attachEvents : function(id, sender, todo){
      var name;
      if (!todo) return
      if (todo.events){
        for(name in todo.events){
          if (typeof(todo.events[name]) === 'function'){
            this._attachEvent(id, sender, name, todo.events[name], todo.data, false);
          }
        }
      }
      if (todo.onces){
        for(name in todo.onces){
          if (typeof(todo.onces[name]) === 'function'){
            this._attachEvent(id, sender, name, todo.onces[name], todo.data, true);
          }
        }
      }
    },
    
    /**
     * @desc execute callback functions 
     **/
    _callback: function(mixed, result, todo){
      var k, $j;
      if (typeof(todo.callback) === 'function') {
        $j = typeof(mixed) === 'number' ? this._jObject(mixed) : mixed;
        todo.callback.apply($j, [result]);
      } else if (typeof(todo.callback) === 'object') {
        for(k in todo.callback){
          if (!$j) $j = typeof(mixed) === 'number' ? this._jObject(mixed) : mixed;
          if (typeof(todo.callback[k]) === 'function') todo.callback[k].apply($j, [result]);
        }
      }
    },
    
    /**
     * @desc execute end functions 
     **/
    _manageEnd: function(id, sender, todo, internal){
      var k, c;
      if (sender && (typeof(sender) === 'object')){
        this._attachEvents(id, sender, todo);
        for(k in todo.apply){
          c = todo.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, todo);
        this._end(id);
      }
    },
    
    /**
     * @desc return true if mixed is usable as number
     **/
    _isNumeric: function (mixed){
      return (typeof(mixed) === 'number' || typeof(mixed) === 'string') && mixed !== '' && !isNaN(mixed);
    },
    
    /**
     *  @desc convert mixed [ lat, lng ] objet by google.maps.LatLng
     **/
    _latLng: function(mixed, emptyReturnMixed, noFlat){
      var k, latLng={}, i=0,
          empty = emptyReturnMixed ? mixed : null;
      if (!mixed || (typeof(mixed) === 'string')){
        return empty;
      }
      if (mixed.latLng) {
        return this._latLng(mixed.latLng);
      }
      if (typeof(mixed.lat) === 'function') {
        return mixed;
      } else if ( this._isNumeric(mixed.lat) ) {
        return new google.maps.LatLng(mixed.lat, mixed.lng);
      } else if ( !noFlat ){
        for(k in mixed){
          if ( !this._isNumeric(mixed[k]) ) return empty;
          latLng[i?'lng':'lat'] = 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 &= this._isNumeric(mixed[t[i]]);
        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 (!this._isNumeric(mixed[k])) 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){
      if (key.toLowerCase){
        key = key.toLowerCase();
        for(var k in object){
          if (k.toLowerCase && (k.toLowerCase() == key)) return k;
        }
      }
      return false;
    },
    
    /**
     * @desc search an (insensitive) key
     **/
    _ival: function(object, key, def){
      var k = this._ikey(object, key);
      if ( k ) return object[k];
      return def;
    },
    
    /**
     * @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){
        if (n.toLowerCase){
          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(todo, include, r){
      if (this._hasKey(todo, this._properties) || this._hasKey(todo, include)){
        var k, p, ip, r={};
        for(k in this._properties){
          p=this._properties[k];
          ip = this._ikey(todo, p);
          r[p] = ip ? todo[ip] : {};
        }
        for(k in include){
          p=include [k];
          ip = this._ikey(todo, p);
          if (ip) r[p] = todo[ip];
        }
        return r;
      } else {
        r.options= {};
        for(k in todo){
          if (k == 'action') continue;
          r.options[k] = todo[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, todo, include, exclude){
      var k = this._ikey(todo, name),
          p, r = {}, keys=['map'], 
          cb='callback';
      r[cb] = this._ival(todo, cb);
      if (k) return this._extractObject(todo[k], include, r);
      for(k in exclude) keys.push(exclude[k]);
      if (!this._hasKey(todo, keys)) r = this._extractObject(todo, include, r);
      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, todo, method, all){
      var address = this._ival(todo, 'address'),
          region, params,
          that = this, cb;
      if ( address ){
          cb = function(results, status) {
          if (status == google.maps.GeocoderStatus.OK){
            that[method](id, todo, all ? results : results[0].geometry.location);
          } else {
            if (that._default.verbose){
              alert('Geocode error : ' + status);
            }
            that[method](id, todo, false);
          }
        };
        if (typeof(address) === 'object'){
          params = address;
        } else {
          params = { 'address': address };
          region = this._ival(todo, 'region');
          if (region){
            params.region = region;
          }
        }
        this._getGeocoder().geocode( params, cb );
      } else {
        this[method](id, todo, this._latLng(todo, false, true));
      }
    },
    
    /*============================*/
    /*          PUBLIC            */
    /*============================*/
    
    /**
     * @desc Destroy an existing instance
     **/
    destroy: function(id, todo){
      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, todo);
      }
      this._end(id);
    },
    
    /**
     * @desc Initialize google map object an attach it to the dom element (using id)
     **/
    init: function(id, todo, internal){
      var o, opts, map, styles, k, $this;
      if ( (id == '') || (this._exist(id)) ) return this._end(id);
      o = this._object('map', todo);
      if ( (typeof(o.options.center) === 'boolean') && o.options.center) {
        return false; // wait for an address resolution
      }
      opts = $.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 = this._jObject(id);
      this._setMap(id, new google.maps.Map($this.get(0), 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, todo){
      this._resolveLatLng(id, todo, '_getLatLng', true);
    },
    _getLatLng: function(id, todo, results){
      this._manageEnd(id, results, todo);
    },
    
    /**
     * @desc Return address from latlng        
     **/
    getaddress: function(id, todo){
      var callback,
          $this = this._jObject(id),
          latLng = this._latLng(todo, false, true),
          address = this._ival(todo, 'address'),
          params = latLng ?  {latLng:latLng} : ( address ? (typeof(address) === 'string' ? {address:address} : address) : null),
          cb = this._ival(todo, 'callback');
      if (params && cb && typeof(cb) === 'function') {
        callback = function(results, status) {
          var out = status == google.maps.GeocoderStatus.OK ? results : false;
          cb.apply($this, [out, status]);
        };
        this._getGeocoder().geocode(params, callback);
      }
      this._end(id);
    },
    
    /**
     * @desc Return a route
     **/
    getroute: function(id, todo){
      var callback,
          $this = this._jObject(id),
          cb = this._ival(todo, 'callback');
      if ( (typeof(cb) === 'function') && todo.options ) {
        todo.options.origin = this._latLng(todo.options.origin, true);
        todo.options.destination = this._latLng(todo.options.destination, true);
        callback = function(results, status) {
          var out = status == google.maps.DirectionsStatus.OK ? results : false;
          cb.apply($this, [out, status]);
        };
        this._getDirectionsService().route( todo.options, callback );
      }
      this._end(id);
    },
    /**
     *  @desc return elevation
     **/
    getelevation: function(id, todo){
      var callback, latLng, ls, k, path, samples,
          locations = [],
          $this = this._jObject(id),
          cb = this._ival(todo, 'callback'),
          latLng = this._ival(todo, 'latlng');
      if (cb && typeof(cb) === 'function') {
        callback = function(results, status) {
          var out = status == google.maps.ElevationStatus.OK ? results : false;
          cb.apply($this, [out, status]);
        };
        if (latLng){
          locations.push( this._latLng(latLng) );
        } else {
          ls = this._ival(todo, '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(todo, 'path');
          samples = this._ival(todo, '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, todo){
      this._resolveLatLng(id, todo, '_addMarker');
    },
    
    _addMarker: function(id, todo, latLng, internal){
      var obj, oi, to,
          n = 'marker', niw = 'infowindow',
          o = this._object(n, todo, ['to']);
      if (!internal){
        if (!latLng) {
          this._manageEnd(id, false, o);
          return;
        }
        this._subcall(id, todo, latLng);
      } else if (!latLng){
        return;
      }
      if (o.to){
        to = this._getStoredId(id, o.to);
        obj = to && (typeof(to.add) === 'function');
        if (obj){
          to.add(latLng, todo);
          if (typeof(to.redraw) === 'function'){
            to.redraw();
          }
        }
        if (!internal){
          this._manageEnd(id, obj, o);
        }
      } else {
        o.options.position = latLng;
        o.options.map = this._getMap(id);
        obj = new google.maps.Marker(o.options);
        if ( todo[niw] ){
          oi = this._object(niw, todo[niw], ['open']);
          if ( (oi['open'] === undefined) || oi['open'] ) {
            oi.apply = this._array(oi.apply);
            oi.apply.unshift({action:'open', args:[this._getMap(id), obj]});
          }
          oi.action = 'add'+niw;
          this._planNext(id, oi); 
        }
        if (!internal){
          this._store(id, n, obj, o);
          this._manageEnd(id, obj, o);
        }
      }
      return obj;
    },
    
    /**
     * @desc Add markers (without address resolution)
     **/
    addmarkers: function(id, todo){
      if (this._ival(todo, 'clusters')){
        this._addclusteredmarkers(id, todo);
      } else {
        this._addmarkers(id, todo);
      }
    },
    _addmarkers: function(id, todo){
      var o, k, latLng, marker, markers = [], options = {}, tmp,
          n = 'marker',
          markers = this._ival(todo, 'markers');
      this._subcall(id, todo);
      if ( !markers || (typeof(markers) !== 'object') ) {
        return this._end(id);
      }
      o = this._object(n, todo, ['to', 'markers']);
      
      if (o.to){
        to = this._getStoredId(id, o.to);
        mk = to && (typeof(to.add) === 'function');
        if (mk){
          for(k in markers){
            latLng = this._latLng(markers[k]);
            if (!latLng) continue;
            to.add(latLng, markers[k]);
          }
          if (typeof(to.redraw) === 'function'){
            to.redraw();
          }
        }
        this._manageEnd(id, mk, o);
      } else {
        $.extend(true, options, o.options);
        options.map = this._getMap(id);
        for(k in markers){
          latLng = this._latLng(markers[k]);
          if (!latLng) continue;
          if (markers[k].options){
            tmp = {};
            $.extend(true, tmp, options, markers[k].options);
            o.options = tmp;
          } else {
            o.options = options;
          }
          o.options.position = latLng;
          marker = new google.maps.Marker(o.options);
          markers.push(marker);
          o.data = markers[k].data;
          o.tag = markers[k].tag;
          this._store(id, n, marker, o);
          this._manageEnd(id, marker, o, true);
        }
        o.options = options; // restore previous for futur use
        this._callback(id, markers, todo);
        this._end(id);
      }
    },
    
    getscale: function(id, todo, internal){
      var map = this._getMap(id),
          zoom = map.getZoom(),
          scales = [
            77.864462034120315,
            45.42785688110077,
            16.220730575856892,
            6.879509682822463,
            3.5034960477802986,
            1.8034610362879133,
            0.9127181102723314,
            0.4598746767146186,
            0.23053567913908648,
            0.11545247438886701,
            0.05775371939320953,
            0.02881647975962874,
            0.014414070716531697,
            0.007207618499622224,
            0.003603886381819732,
            0.0018015948787526637,
            0.0009008246767800296,
            0.0004504160086085826,
            0.00022520761796505934,
            0.00011260535432642145,
            0.00005630113180858676
          ];
      scale = scales[zoom];
      if (!internal){
        this._callback(id, scale, todo);
        this._end(id);
      }
      return scale;
    },
    _addclusteredmarkers:function(id, todo){
      var clusterer, k, latLng, clusters, storeId,
          that = this,
          radius = this._ival(todo, 'radius'),
          markers = this._ival(todo, 'markers'),
          styles = this._ival(todo, 'clusters');
          
      if (! this._getMap(id).getBounds() ){ // map not initialised => bounds not available
        // wait for map
        google.maps.event.addListenerOnce(
          this._getMap(id), 
          'bounds_changed', 
          function() {
            that._addclusteredmarkers(id, todo);
          }
        );
        return;
      }
      
      if (typeof(radius) === 'number'){
        clusterer = new Clusterer();
        for (k in markers){
          latLng = this._latLng(markers[k]);
          clusterer.add(latLng, markers[k]);
        }
        storeId = this._initClusters(id, todo, clusterer, radius, styles);
      }
      
      this._callback(id, storeId, todo);
      this._end(id);
    },
    
    _initClusters: function(id, todo, clusterer, radius, styles){
      var that=this, 
          map = this._getMap(id);
          
      clusterer.setRedraw(function(force){
        var ret = clusterer.clusters(map, radius, force);
        if (ret){
          clusters = ret;
          clusterer.freeDom();
          that._displayClusters(id, todo, clusterer, clusters, styles);
        }
      });
      
      clusterer.events(
        google.maps.event.addListener(
          map, 
          'zoom_changed',
          function() {
            clusterer.redraw(true);
          }
        ),
        google.maps.event.addListener(
          map, 
          'bounds_changed',
          function() {
            clusterer.redraw();
          }
        )
      );
      
      clusterer.redraw();
      return this._store(id, 'cluster', clusterer, todo);
    },
    
    _displayClusters: function(id, todo, clusterer, clusters, styles){
      var c, k, i, m, done, obj, cl, options = {}, tmp,
      ctodo = this._ival(todo, 'cluster') || {},
      mtodo = this._ival(todo, 'marker') || todo;
      for(c in clusters){
        cl = clusters[c];
        done = false;
        if (cl.idx.length > 1){
          m = 0;
          for(k in styles){
            if ( (k > m) && (k <= cl.idx.length) ){
              m = k;
            }
          }
          if (styles[m]){
            ctodo = {
              options:{
                content:styles[m].content.replace('CLUSTER_COUNT', cl.idx.length),
                offset:{
                  x: -this._ival(styles[m], 'width')/2,
                  y: -this._ival(styles[m], 'height')/2
                }
              },
              data:{
                latLng: this._latLng(cl)
              }
            };
            obj = this._addOverlay(id, ctodo, this._latLng(cl), true);
            this._attachEvents(id, obj, ctodo);
            clusterer.store(obj);
            done = true;
          }
        }
        if (!done){
          cl.dom = [];
          $.extend(true, options, mtodo.options);
          for(i in cl.idx){
            m = clusterer.get(cl.idx[i]);
            mtodo.latLng = m.latLng;
            mtodo.data = m.marker.data;
            mtodo.tag = m.marker.tag;
            if (m.marker.options){
              tmp = {};
              $.extend(true, tmp, options, m.marker.options);
              mtodo.options = tmp;
            } else {
              mtodo.options = options;
            }
            obj = this._addMarker(id, mtodo, mtodo.latLng, true);
            this._attachEvents(id, obj, mtodo);
            clusterer.store(obj);
          }
          mtodo.options = options; // restore previous for futur use
        }
      }
    },
    
    /**
     * @desc Add an infowindow after address resolution
     **/
    addinfowindow: function(id, todo){ 
      this._resolveLatLng(id, todo, '_addInfoWindow');
    },
    _addInfoWindow: function(id, todo, latLng){
      var o, infowindow, args = [],
          n = 'infowindow';
      this._subcall(id, todo, latLng);
      o = this._object(n, todo, ['open', 'anchor']);
      if (latLng) {
        o.options.position = latLng;
      }
      infowindow = new google.maps.InfoWindow(o.options);
      if ( (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, o);
      this._manageEnd(id, infowindow, o);
    },
    
    /**
     * @desc add a polygone / polylin on a map
     **/
    addpolyline: function(id, todo){
      this._addPoly(id, todo, 'Polyline', 'path');
    },
    addpolygon: function(id, todo){
      this._addPoly(id, todo, 'Polygon', 'paths');
    },
    _addPoly: function(id, todo, poly, path){
      var k, i, obj, o = this._object(poly.toLowerCase(), todo, [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, o);
      this._manageEnd(id, obj, o);
    },
    
    /**
     * @desc add a circle   
     **/
    addcircle: function(id, todo){
      this._resolveLatLng(id, todo, '_addCircle');
    },
    _addCircle: function(id, todo, latLng ){
      var c, n = 'circle',
          o = this._object(n, todo);
      if (!latLng) latLng = this._latLng(o.options.center);
      if (!latLng) return this._manageEnd(id, false, o);
      this._subcall(id, todo, latLng);
      o.options.center = latLng;
      o.options.map = this._getMap(id);
      c = new google.maps.Circle(o.options);
      this._store(id, n, c, o);
      this._manageEnd(id, c, o);
    },
    
    /**
     * @desc add a rectangle   
     **/
    addrectangle: function(id, todo){
      this._resolveLatLng(id, todo, '_addRectangle');
    },
    _addRectangle: function(id, todo, latLng ){
      var r, n = 'rectangle',
          o = this._object(n, todo);
      o.options.bounds = this._latLngBounds(o.options.bounds, true);
      if (!o.options.bounds) return this._manageEnd(id, false, o);
      this._subcall(id, todo, o.options.bounds.getCenter());
      o.options.map = this._getMap(id);
      r = new google.maps.Rectangle(o.options);
      this._store(id, n, r, o);
      this._manageEnd(id, r, o);
    },
    
    /**
     * @desc add an overlay to a map after address resolution
     **/
    addoverlay: function(id, todo){
      this._resolveLatLng(id, todo, '_addOverlay');
    },
    _addOverlay: function(id, todo, latLng, internal){
      var ov,  
          o = this._object('overlay', todo),
          opts =  $.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 = $('<div></div>');
        $div
          .css('border', 'none')
          .css('borderWidth', '0px')
          .css('position', 'absolute');
        $div.append($(this.opts_.content));
        this.$div_ = $div;
        panes = this.getPanes();
        if (panes[this.opts_.pane]) $(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));
      if (!internal){
        this._store(id, 'overlay', ov, o);
        this._manageEnd(id, ov, o);
      }
      return ov;
    },
    
    /**
     * @desc add fixed panel to a map
     **/
    addfixpanel: function(id, todo){
      var n = 'fixpanel',
          o = this._object(n, todo),
          x=0, y=0, $c, $div;
      if (o.options.content){
        $c = $(o.options.content);
        
        if (o.options.left !== undefined){
          x = o.options.left;
        } else if (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 (o.options.top !== undefined){
          y = o.options.top;
        } else if (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 = $('<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, o);
        this._callback(id, $div, o);
      }
      this._end(id);
    },
    
    /**
     * @desc Add a direction renderer to a map
     **/
    adddirectionsrenderer: function(id, todo, internal){
      var n = 'directionrenderer',
          dr, o = this._object(n, todo, ['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, o);
      this._manageEnd(id, dr, o, internal);
    },
    
    /**
     * @desc Set direction panel to a dom element from it ID
     **/
    setdirectionspanel: function(id, todo){
      var dr, o = this._object('directionpanel', todo, ['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, todo){
      var dr, o = this._object('directions', todo);
      if (todo) o.options.directions = todo.directions ? todo.directions : (todo.options && todo.options.directions ? todo.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, todo){
      var o = this._object('streetview', todo, ['id']),
          panorama;
      if (o.options.position){
        o.options.position = this._latLng(o.options.position);
      }
      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, todo){
      var kml, o = this._object('kmllayer', todo, ['url']);
      o.options.map = this._getMap(id);
      kml = new google.maps.KmlLayer(o.url, o.options);
      this._manageEnd(id, kml, todo);
    },
    
    /**
     * @desc add a traffic layer to a map
     **/
    addtrafficlayer: function(id, todo){
      var n = 'trafficlayer', 
          o = this._object(n, todo),
          tl = this._getStored(id, n);
      if (!tl){
        tl = new google.maps.TrafficLayer();
        tl.setMap(this._getMap(id));
        this._store(id, n, tl, o);
      }
      this._manageEnd(id, tl, o);
    },
    
    /**
     * @desc set a bicycling layer to a map
     **/
    addbicyclinglayer: function(id, todo){
      var n = 'bicyclinglayer',
          o = this._object(n, todo),
          bl = this._getStored(id, n);
      if (!bl){
        bl = new google.maps.BicyclingLayer();
        bl.setMap(this._getMap(id));
        this._store(id, n, bl, o);
      }
      this._manageEnd(id, bl, o);
    },
    
    
    /**
     * @desc add a ground overlay to a map
     **/
    addgroundoverlay: function(id, todo){
      var n = 'groundoverlay',
          o = this._object(n, todo, ['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, o);
      }
      this._manageEnd(id, ov, o);
    },
    
    /**
     * @desc Geolocalise the user and return a LatLng
     **/
    geolatlng: function(id, todo){
      var geo,
          cb = this._ival(todo, 'callback'),
          $this = this._jObject(id);
      if (typeof(cb) === 'function') {
        if(navigator.geolocation) {
          browserSupportFlag = true;
          navigator.geolocation.getCurrentPosition(function(position) {
            var out = new google.maps.LatLng(position.coords.latitude,position.coords.longitude);
            cb.apply($this, [out]);
          }, function() {
            var out = false;
            cb.apply($this, [out]);
          });
        } else if (google.gears) {
          browserSupportFlag = true;
          geo = google.gears.factory.create('beta.geolocation');
          geo.getCurrentPosition(function(position) {
            var out = new google.maps.LatLng(position.latitude,position.longitude);
            cb.apply($this, [out]);
          }, function() {
            out = false;
            cb.apply($this, [out]);
          });
        } else {
            out = false;
            cb.apply($this, [out]);
        }
      }
      this._end(id);
    },
    
    /**
     * @desc Add a style to a map
     **/
    addstyledmap: function(id, todo, internal){
      var o = this._object('styledmap', todo, ['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, todo){
      var o = this._object('styledmap', todo, ['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, todo);
        }
      }
      this._manageEnd(id, style, o);
    },
    
    /**
     * @desc Remove objects from a map
     **/
    clear: function(id, todo){
      var list = this._array(this._ival(todo, 'list') || this._ival(todo, 'name')),
          last = this._ival(todo, 'last', false),
          first = this._ival(todo, 'first', false),
          tag = this._ival(todo, 'tag');
      if (tag !== undefined){
        tag = this._array(tag);
      }
      this._clear(id, list, last, first, tag);
      this._end(id);
    },
    
    /**
     * @desc Return Google object(s) wanted
     **/
    get: function(id, todo){
      var name = this._ival(todo, 'name') || 'map',
          first= this._ival(todo, 'first'),
          all  = this._ival(todo, 'all'),
          tag = this._ival(todo, 'tag'),
          r, idx, t;
      name = name.toLowerCase();
      if (tag !== undefined){
        tag = this._array(tag);
      }
      if (name == 'map'){
        return this._getMap(id);
      }
      if (first){
        return this._getStored(id, name, false, tag);
      } else if (all){
        r = new Array();
        t = this._ids[id].stored[name];
        if (t){
          for(idx in t){
            if (!t[idx]){
              continue;
            }
            if (tag !== undefined) {
              if ( (t[idx].tag === undefined) || ($.inArray(t[idx].tag, tag) < 0) ){
                continue;
              }
            }
            r.push(t[idx].obj);
          }
        }
        return r;
      } else {
        return this._getStored(id, name, true, tag);
      }
    },
    
    /**
     * @desc return the radius of the earth depending on the unit
     **/
    earthradius: function(unit){
      unit = unit ? unit : this._default.unit;
      return (typeof(unit) === 'string' && (unit.toLowerCase() === 'km')) ? 6371 : 3959;
    },
    
    /**
     * @desc the distance between 2 latLng depending on the unit
     **/
    distance: function(id, todo){
      var unit = this._ival(todo, 'unit'),
          a = this._latLng(this._ival(todo, 'a')),
          b = this._latLng(this._ival(todo, 'b')),
          e,f,g,h, dist;
      if (a && b){
        e=(Math.PI*a.lat()/180);
        f=(Math.PI*a.lng()/180);
        g=(Math.PI*b.lat()/180);
        h=(Math.PI*b.lng()/180);
        dist = this.earthradius(unit)*Math.acos(Math.min(Math.cos(e)*Math.cos(g)*Math.cos(f)*Math.cos(h)+Math.cos(e)*Math.sin(f)*Math.cos(g)*Math.sin(h)+Math.sin(e)*Math.sin(g),1)); 
      }
      return dist;
    },
    
    /**
     * @desc return the max zoom of a latlng
     **/
    getmaxzoom: function(id, todo){
      this._resolveLatLng(id, todo, '_getMaxZoom');
    },
    _getMaxZoom: function(id, todo, latLng){
      var callback,
          $this = this._jObject(id), 
          cb = this._ival(todo, 'callback');
      if (cb && typeof(cb) === 'function') {
        callback = function(result) {
          var zoom = result.status == google.maps.MaxZoomStatus.OK ? result.zoom : false;
          cb.apply($this, [zoom, result.status]);
        };
        this._getMaxZoomService().getMaxZoomAtLatLng(latLng, callback);
      }
      this._end(id);
    },
  
    /**
     * @desc modify default values
     **/
    setdefault: function(id, todo, internal){
      for(var k in todo){
        if (typeof(this._default[k]) === 'object'){
          this._default[k] = jQuery.extend({}, this._default[k], todo[k]);
        } else {
          this._default[k] = todo[k];
        }
      }
      if (!internal){
        this._end(id);
      }
    },
    /**
     * @desc : autofit a map using its overlays (markers, rectangles ...)
     **/
    autofit: function(id, todo, internal){
      var n, k, stored, obj, empty = true, bounds = new google.maps.LatLngBounds();
      if (id in this._ids){
        for(n in this._ids[id].stored){
          stored = this._ids[id].stored[n];
          for(k in stored){
            obj = stored[k].obj;
            if (obj.getPosition){
              bounds.extend(obj.getPosition());
              empty = false;
            } else if (obj.getBounds){
              bounds.extend(obj.getBounds().getNorthEast());
              bounds.extend(obj.getBounds().getSouthWest());
              empty = false;
            } else if (obj.getPaths){
              obj.getPaths().forEach(function(path){
                path.forEach(function(latLng){
                  bounds.extend(latLng);
                  empty = false;
                });
              });
            } else if (obj.getPath){
              obj.getPath().forEach(function(latLng){
                bounds.extend(latLng);
                empty = false;
              });
            } else if (obj.getCenter){
              bounds.extend(obj.getCenter());
              empty = false;
            }
          }
        }
        if (!empty){
          this._getMap(id).fitBounds(bounds);
        }
      }
      if (!internal){
        this._manageEnd(id, empty ? false : bounds, todo, internal);
      }
    }
    
  };
  
  var globalId = 0;
  
  $.fn.gmap3 = function(){
    var a,i,
        todo = [],
        results = [],
        empty = true;
    for(i=0; i<arguments.length; i++){
      a = arguments[i] || {};
      if (typeof(a) === 'string'){
        a = {action:a};
      }
      if (a.action && (a.action.substr(0, 1) == ':')){
        a.action = a.action.substr(1);
      }
      todo.push(a);
    }
    if (!todo.length) todo.push({});
    $.each(this, function() {
      var id;
        $this = $(this),
        id = $this.data('id');
      empty = false;
      if (!id){
        id = ++globalId;
        $this.data('id', id);
      }
      if (todo.length == 1){
        if (gmap3._isDirect(id, todo[0])){
          results.push(gmap3._direct(id, todo[0]));
        } else {
          res = gmap3._plan($this, id, todo);
          if (res){ // value returned in [] by _proceed => return current
            for(i in res){
              results.push(res[i]);
            }
          }
        }
      } else {
        gmap3._plan($this, id, todo);
      }
    });
    if (results.length){
      if (results.length === 1){ // 1 css selector
        return results[0];
      } else {
        return results;
      }
    }
    if (empty && (arguments.length == 2) && (typeof(arguments[0]) === 'string') && (arguments[0].toLowerCase() === 'setdefault')){
      gmap3.setdefault(0, arguments[1], true);
    }
    return this;
  }

}(jQuery));
