/*
 *  GMAP3 Plugin for JQuery 
 *  Version   : 4.1
 *  Date      : 2011-11-18
 *  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.empty = function (){
      for(var i = 0; i < st.length; i++){
        if (st[i]){
          return false
        }
      }
      return true;
    }
    this.add = function(v){
      st.push(v);
    }
    this.addNext = function ( v){
      var t=[], i, k = 0;
      for(i = 0; i < st.length; i++){
        if (!st[i]){
          continue;
        }
        if (k == 1) {
          t.push(v);
        }
        t.push(st[i]);
        k++;
      }
      if (k < 2) {
        t.push(v);
      }
      st = t;
    }
    this.get = function (){
      for(var i = 0; i < st.length; i++){
        if (st[i]) {
          return st[i];
        }
      }
      return false;
    }
    this.ack = function (){
      for(var i = 0; i < st.length; i++){                     
        if (st[i]) {
          delete st[i];
          break;
        }
      }
      if (this.empty()){
        st = [];
      }
    }
  }
  
  /***************************************************************************/
  /*                                STORE                                    */
  /***************************************************************************/
  function Store(){
    var store = {};
    
    /**
     * add a mixed to the store
     **/
    this.add = function(name, obj, todo){
      name = name.toLowerCase();
      if (!store[name]){
        store[name] = [];
      }
      store[name].push({obj:obj, tag:ival(todo, 'tag')});
      return name + '-' + (store[name].length-1);
    }
    
    /**
     * return a stored mixed
     **/
    this.get = function(name, last, tag){
      var i, idx, add;
      name = name.toLowerCase();
      if (!store[name] || !store[name].length){
        return null;
      }
      idx = last ? store[name].length : -1;
      add = last ? -1 : 1;
      for(i=0; i<store[name].length; i++){
        idx += add;
        if (store[name][idx]){
          if (tag !== undefined) {
            if ( (store[name][idx].tag === undefined) || ($.inArray(store[name][idx].tag, tag) < 0) ){
              continue;
            }
          }
          return store[name][idx].obj;
        }
      }
      return null;
    }
    
    /**
     * return all stored mixed
     **/
    this.all = function(name, tag){
      var i, result = [];
      name = name.toLowerCase();
      if (!store[name] || !store[name].length){
        return result;
      }
      for(i=0; i<store[name].length; i++){
        if (!store[name][i]){
          continue;
        }
        if ( (tag !== undefined) && ( (store[name][i].tag === undefined) || ($.inArray(store[name][i].tag, tag) < 0) ) ){
          continue;
        }
        result.push(store[name][i].obj);
      }
      return result;
    }
    
    /**
     * return all storation groups
     **/
    this.names = function(){
      var name, result = [];
      for(name in store){
        result.push(name);
      }
      return result;
    }
    
    /**
     * return an object from its reference
     **/
    this.refToObj = function(ref){
      ref = ref.split('-'); // name - idx
      if ((ref.length == 2) && store[ref[0]] && store[ref[0]][ref[1]]){
        return store[ref[0]][ref[1]].obj;
      }
      return null;
    }
    
    /**
     * remove one object from the store
     **/
    this.rm = function(name, tag, pop){
      var idx, i, tmp;
      name = name.toLowerCase();
      if (!store[name]) {
        return false;
      }
      if (tag !== undefined){
        if (pop){
          for(idx = store[name].length - 1; idx >= 0; idx--){
            if ( (store[name][idx] !== undefined) && (store[name][idx].tag !== undefined) && ($.inArray(store[name][idx].tag, tag) >= 0) ){
              break;
            }
          }
        } else {
          for(idx = 0; idx < store[name].length; idx++){
            if ( (store[name][idx] !== undefined) && (store[name][idx].tag !== undefined) && ($.inArray(store[name][idx].tag, tag) >= 0) ){
              break;
            }
          }
        }
      } else {
        idx = pop ? store[name].length - 1 : 0;
      }
      if ( !(idx in store[name]) ) {
        return false;
      }
      // Google maps element
      if (typeof(store[name][idx].obj.setMap) === 'function') {
        store[name][idx].obj.setMap(null);
      }
      // jQuery
      if (typeof(store[name][idx].obj.remove) === 'function') {
        store[name][idx].obj.remove();
      }
      // internal (cluster)
      if (typeof(store[name][idx].obj.free) === 'function') {
        store[name][idx].obj.free();
      }
      delete store[name][idx].obj;
      if (tag !== undefined){
        tmp = [];
        for(i=0; i<store[name].length; i++){
          if (i !== idx){
            tmp.push(store[name][i]);
          }
        }
        store[name] = tmp;
      } else {
        if (pop) {
          store[name].pop();
        } else {
          store[name].shift();
        }
      }
      return true;
    }
    
    /**
     * remove objects from the store
     **/
    this.clear = function(list, last, first, tag){
      var k, i, name;
      if (!list || !list.length){
        list = [];
        for(k in store){
          list.push(k);
        }
      } else {
        list = array(list);
      }
      for(i=0; i<list.length; i++){
        if (list[i]){
          name = list[i].toLowerCase();
          if (!store[name]){
            continue;
          }
          if (last){
            this.rm(name, tag, true);
          } else if (first){
            this.rm(name, tag, false);
          } else {
            // all
            while (this.rm(name, tag, false));
          }
        }
      }
    }
  }
  
  /***************************************************************************/
  /*                              CLUSTERER                                  */
  /***************************************************************************/

  function Clusterer(){
    var markers = [], events=[], stored=[], 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(data, obj, shadow){
      stored.push({data:data, obj:obj, shadow:shadow});
    }
    
    this.free = function(){
      for(var i = 0; i < events.length; i++){
        google.maps.event.removeListener(events[i]);
      }
      events=[];
      this.freeAll();
    }
    
    this.freeIndex = function(i){
      if (typeof(stored[i].obj.setMap) === 'function') {
        stored[i].obj.setMap(null);
      }
      if (typeof(stored[i].obj.remove) === 'function') {
        stored[i].obj.remove();
      }
      if (stored[i].shadow){ // only overlays has shadow
        if (typeof(stored[i].shadow.remove) === 'function') {
          stored[i].obj.remove();
        }
        if (typeof(stored[i].shadow.setMap) === 'function') {
          stored[i].shadow.setMap(null);
        }
        delete stored[i].shadow;
      }
      delete stored[i].obj;
      delete stored[i].data;
      delete stored[i];
    }
    
    this.freeAll = function(){
      var i;
      for(i = 0; i < stored.length; i++){
        if (stored[i]) {
          this.freeIndex(i);
        }
      }
      stored = [];
    }
    
    this.freeDiff = function(clusters){
      var i, j, same = {}, idx = [];
      for(i=0; i<clusters.length; i++){
        idx.push( clusters[i].idx.join('-') );
      }
      for(i = 0; i < stored.length; i++){
        if (!stored[i]) {
          continue;
        }
        j = $.inArray(stored[i].data.idx.join('-'), idx);
        if (j >= 0){
          same[j] = true;
        } else {
          this.freeIndex(i);
        }
      }
      return same;
    }
    
    this.add = function(latLng, marker){
      markers.push({latLng:latLng, marker:marker});
    }
    
    this.get = function(i){
      return markers[i];
    }
    
    this.clusters = function(map, radius, maxZoom, 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(),
          noClusters = maxZoom && (maxZoom <= map.getZoom()),
          chkContain = map.getZoom() > 2;
      
      cnt = 0;
      keys = {};
      for(i = 0; i < markers.length; i++){
        if (chkContain && !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 && !noClusters){
        for(k = 0; k < latest.length; k++){
          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;
        
        
        if (noClusters){
          saved = {lat:lat, lng:lng, idx:[i]};
        } else {
          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 = 0; k2 < saved.idx.length; k2++){
          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=0; i<markers.length; i++){
        bounds.extend(markers[i].latLng);
      }
      return bounds;
    }
  }

  /***************************************************************************/
  /*                           GMAP3 GLOBALS                                 */
  /***************************************************************************/
  
  var _default = {
      verbose:false,
      queryLimit:{
        attempt:5,
        delay:250, // setTimeout(..., delay + random);
        random:250
      },
      init:{
        mapTypeId : google.maps.MapTypeId.ROADMAP,
        center:[46.578498,2.457275],
        zoom: 2
      },
      classes:{
        Map               : google.maps.Map,
        Marker            : google.maps.Marker,
        InfoWindow        : google.maps.InfoWindow,
        Circle            : google.maps.Circle,
        Rectangle         : google.maps.Rectangle,
        OverlayView       : google.maps.OverlayView,
        StreetViewPanorama: google.maps.StreetViewPanorama,
        KmlLayer          : google.maps.KmlLayer,
        TrafficLayer      : google.maps.TrafficLayer,
        BicyclingLayer    : google.maps.BicyclingLayer,
        GroundOverlay     : google.maps.GroundOverlay,
        StyledMapType     : google.maps.StyledMapType
      }
    },
    _properties = ['events','onces','options','apply', 'callback', 'data', 'tag'],
    _noInit = ['init', 'geolatlng', 'getlatlng', 'getroute', 'getelevation', 'getdistance', 'addstyledmap', 'setdefault', 'destroy'],
    _directs = ['get'],
    geocoder = directionsService = elevationService = maxZoomService = distanceMatrixService = null;
    
  function setDefault(values){
    for(var k in values){
      if (typeof(_default[k]) === 'object'){
        _default[k] = $.extend({}, _default[k], values[k]);
      } else {
        _default[k] = values[k];
      }
    }
  }
  
  function autoInit(iname){
    if (!iname){
      return true;
    }
    for(var i = 0; i < _noInit.length; i++){
      if (_noInit[i] === iname) {
        return false;
      }
    }
    return true;
  }
  
    
  /**
   * return true if action has to be executed directly
   **/
  function isDirect (todo){
    var action = ival(todo, 'action');
    for(var i = 0; i < _directs.length; i++){
      if (_directs[i] === action) {
        return true;
      }
    }
    return false;
  }
        
  //-----------------------------------------------------------------------//
  // Objects tools
  //-----------------------------------------------------------------------//
  
  /**
   * return the real key by an insensitive seach
   **/
  function ikey (object, key){
    if (key.toLowerCase){
      key = key.toLowerCase();
      for(var k in object){
        if (k.toLowerCase && (k.toLowerCase() == key)) {
          return k;
        }
      }
    }
    return false;
  }
  
  /**
   * return the value of real key by an insensitive seach
   **/
  function ival (object, key, def){
    var k = ikey(object, key);
    return k ? object[k] : def;
  }
  
  /**
   * return true if at least one key is set in object
   * nb: keys in lowercase
   **/
  function hasKey (object, keys){
    var n, k;
    if (!object || !keys) {
      return false;
    }
    keys = array(keys);
    for(n in object){
      if (n.toLowerCase){
        n = n.toLowerCase();
        for(k in keys){
          if (n == keys[k]) {
            return true;
          }
        }
      }
    }
    return false;
  }
  
  /**
   * return a standard object
   * nb: include in lowercase
   **/
  function extractObject (todo, include, result/* = {} */){
    if (hasKey(todo, _properties) || hasKey(todo, include)){ // #1 classical object definition
      var i, k;
      // get defined properties values from todo
      for(i=0; i<_properties.length; i++){
        k = ikey(todo, _properties[i]);
        result[ _properties[i] ] = k ? todo[k] : {};
      }
      if (include && include.length){
        for(i=0; i<include.length; i++){
          if(k = ikey(todo, include[i])){
            result[ include[i] ] = todo[k];
          }
        }
      }
      return result;
    } else { // #2 simplified object (all excepted "action" are options properties)
      result.options= {};
      for(k in todo){
        if (k !== 'action'){
          result.options[k] = todo[k];
        }
      }
      return result;
    }
  }
  
  /**
   * identify object from object list or parameters list : [ objectName:{data} ] or [ otherObject:{}, ] or [ object properties ]
   * nb: include, exclude in lowercase
   **/
  function getObject(name, todo, include, exclude){
    var iname = ikey(todo, name),
        i, result = {}, keys=['map'];
    // include callback from high level
    result['callback'] = ival(todo, 'callback');
    include = array(include);
    exclude = array(exclude);
    if (iname) {
      return extractObject(todo[iname], include, result);
    }
    if (exclude && exclude.length){
      for(i=0; i<exclude.length; i++) {
        keys.push(exclude[i]);
      }
    }
    if (!hasKey(todo, keys)){
      result = extractObject(todo, include, result);
    }
    // initialize missing properties
    for(i=0; i<_properties.length; i++){
      if (_properties[i] in result){
        continue;
      }
      result[ _properties[i] ] = {};
    }
    return result;
  }
  
  //-----------------------------------------------------------------------//
  // Service tools
  //-----------------------------------------------------------------------//
    
  function getGeocoder(){
    if (!geocoder) {
      geocoder = new google.maps.Geocoder();
    }
    return geocoder;
  }
  
  function getDirectionsService(){
    if (!directionsService) {
      directionsService = new google.maps.DirectionsService();
    }
    return directionsService;
  }
  
  function getElevationService(){
    if (!elevationService) {
      elevationService = new google.maps.ElevationService();
    }
    return elevationService;
  }
  
  function getMaxZoomService(){
    if (!maxZoomService) {
      maxZoomService = new google.maps.MaxZoomService();
    }
    return maxZoomService;
  }
  
  function getDistanceMatrixService(){
    if (!distanceMatrixService) {
      distanceMatrixService = new google.maps.DistanceMatrixService();
    }
    return distanceMatrixService;
  }
    
  //-----------------------------------------------------------------------//
  // Unit tools
  //-----------------------------------------------------------------------//
  
  /**
   * return true if mixed is usable as number
   **/
  function numeric(mixed){
    return (typeof(mixed) === 'number' || typeof(mixed) === 'string') && mixed !== '' && !isNaN(mixed);
  }
  
    /**
   * convert data to array
   **/
  function array(mixed){
    var k, a = [];
    if (mixed !== undefined){
      if (typeof(mixed) === 'object'){
        if (typeof(mixed.length) === 'number') {
          a = mixed;
        } else {
          for(k in mixed) {
            a.push(mixed[k]);
          }
        }
      } else{ 
        a.push(mixed);
      }
    }
    return a;
  }
  
  /**
   * convert mixed [ lat, lng ] objet to google.maps.LatLng
   **/
  function toLatLng (mixed, emptyReturnMixed, noFlat){
    var empty = emptyReturnMixed ? mixed : null;
    if (!mixed || (typeof(mixed) === 'string')){
      return empty;
    }
    // defined latLng
    if (mixed.latLng) {
      return toLatLng(mixed.latLng);
    }
    // google.maps.LatLng object
    if (typeof(mixed.lat) === 'function') {
      return mixed;
    } 
    // {lat:X, lng:Y} object
    else if ( numeric(mixed.lat) ) {
      return new google.maps.LatLng(mixed.lat, mixed.lng);
    }
    // [X, Y] object 
    else if ( !noFlat && mixed.length){ // and "no flat" object allowed
      if ( !numeric(mixed[0]) || !numeric(mixed[1]) ) {
        return empty;
      }
      return new google.maps.LatLng(mixed[0], mixed[1]);
    }
    return empty;
  }
  
  /**
   * convert mixed [ sw, ne ] object by google.maps.LatLngBounds
   **/
  function toLatLngBounds(mixed, flatAllowed, emptyReturnMixed){
    var ne, sw, empty;
    if (!mixed) {
      return null;
    }
    empty = emptyReturnMixed ? mixed : null;
    if (typeof(mixed.getCenter) === 'function') {
      return mixed;
    }
    if (mixed.length){
      if (mixed.length == 2){
        ne = toLatLng(mixed[0]);
        sw = toLatLng(mixed[1]);
      } else if (mixed.length == 4){
        ne = toLatLng([mixed[0], mixed[1]]);
        sw = toLatLng([mixed[2], mixed[3]]);
      }
    } else {
      if ( ('ne' in mixed) && ('sw' in mixed) ){
        ne = toLatLng(mixed.ne);
        sw = toLatLng(mixed.sw);
      } else if ( ('n' in mixed) && ('e' in mixed) && ('s' in mixed) && ('w' in mixed) ){
        ne = toLatLng([mixed.n, mixed.e]);
        sw = toLatLng([mixed.s, mixed.w]);
      }
    }
    if (ne && sw){
      return new google.maps.LatLngBounds(sw, ne);
    }
    return empty;
  }

  /***************************************************************************/
  /*                                GMAP3                                    */
  /***************************************************************************/
  
  function Gmap3($this){
  
    var stack = new Stack(),
        store = new Store(),
        map = null,
        styles = {},
        running = false;
    
    //-----------------------------------------------------------------------//
    // Stack tools
    //-----------------------------------------------------------------------//
        
    /**
     * store actions to execute in a stack manager
     **/
    this._plan = function(list){
      for(var k = 0; k < list.length; k++) {
        stack.add(list[k]);
      }
      this._run();
    }
     
    /**
     * store one action to execute in a stack manager after the current
     **/
    this._planNext = function(todo){
      stack.addNext(todo);
    }
    
    /**
     * execute action directly
     **/
    this._direct = function(todo){
      var action = ival(todo, 'action');
      return this[action]($.extend({}, action in _default ? _default[action] : {}, todo.args ? todo.args : todo));
    }
    
    /**
     * called when action in finished, to acknoledge the current in stack and start next one
     **/
    this._end = function(){
      running = false;
      stack.ack();
      this._run();
    },
    /**
     * if not running, start next action in stack
     **/
    this._run = function(){
      if (running) {
        return;
      }
      var todo = stack.get();
      if (!todo) {
        return;
      }
      running = true;
      this._proceed(todo);
    }
    
    //-----------------------------------------------------------------------//
    // Call tools
    //-----------------------------------------------------------------------//
    
    /**
     * run the appropriated function
     **/
    this._proceed = function(todo){
      todo = todo || {};
      var action = ival(todo, 'action') || 'init',
          iaction = action.toLowerCase(),
          ok = true,
          target = ival(todo, 'target'), 
          args = ival(todo, 'args'),
          out;
      // check if init should be run automatically
      if ( !map && autoInit(iaction) ){
        this.init($.extend({}, _default.init, todo.args && todo.args.map ? todo.args.map : todo.map ? todo.map : {}), true);
      }
      
      // gmap3 function
      if (!target && !args && (iaction in this) && (typeof(this[iaction]) === 'function')){
        this[iaction]($.extend({}, iaction in _default ? _default[iaction] : {}, todo.args ? todo.args : todo)); // call fnc and extends defaults data
      } else {
        // "target" object function
        if (target && (typeof(target) === 'object')){
          if (ok = (typeof(target[action]) === 'function')){
            out = target[action].apply(target, todo.args ? todo.args : []);
          }
        // google.maps.Map direct function :  no result so not rewrited, directly wrapped using array "args" as parameters (ie. setOptions, addMapType, ...)
        } else if (map){
          if (ok = (typeof(map[action]) === 'function')){
            out = map[action].apply(map, todo.args ? todo.args : [] );
          }
        }
        if (!ok && _default.verbose) {
          alert("unknown action : " + action);
        }
        this._callback(out, todo);
        this._end();
      }
    }
    
    /**
     * returns the geographical coordinates from an address and call internal or given method
     **/
     this._resolveLatLng = function(todo, method, all, attempt){
      var address = ival(todo, 'address'),
          params,
          that = this,
          fnc = typeof(method) === 'function' ? method : that[method];
      if ( address ){
        if (!attempt){ // convert undefined to int
          attempt = 0;
        }
        if (typeof(address) === 'object'){
          params = address;
        } else {
          params = {'address': address};
        }
        getGeocoder().geocode(
          params, 
          function(results, status) {
          if (status === google.maps.GeocoderStatus.OK){
            fnc.apply(that, [todo, all ? results : results[0].geometry.location]);
          } else if ( (status === google.maps.GeocoderStatus.OVER_QUERY_LIMIT) && (attempt < _default.queryLimit.attempt) ){
            setTimeout(function(){
                that._resolveLatLng(todo, method, all, attempt+1);
              },
              _default.queryLimit.delay + Math.floor(Math.random() * _default.queryLimit.random)
            );
          } else {
            if (_default.verbose){
              alert('Geocode error : ' + status);
            }
            fnc.apply(that, [todo, false]);;
          }
        }
      );
      } else {
        fnc.apply(that, [todo, toLatLng(todo, false, true)]);
      }
    }
    
    /**
     * returns the geographical coordinates from an array of object using "address" and call internal method
     **/
    this._resolveAllLatLng = function(todo, property, method){
      var that = this,
          i = -1,
          solveNext = function(){
            do{
              i++;
            }while( (i < todo[property].length) && !('address' in todo[property][i]) );
            if (i < todo[property].length){
              (function(todo){
                that._resolveLatLng(
                  todo,
                  function(todo, latLng){
                    todo.latLng = latLng;
                    solveNext.apply(that, []); // solve next or execute exit method
                  }
                );
              })(todo[property][i]);
            } else {
              that[method](todo);
            }
          };
      solveNext();
    }
    
    /**
     * call a function of framework or google map object of the instance
     **/
    this._call = function(/* fncName [, ...] */){
      var i, fname = arguments[0], args = [];
      if ( !arguments.length || !map || (typeof(map[fname]) !== 'function') ){
        return;
      }
      for(i=1; i<arguments.length; i++){
        args.push(arguments[i]);
      }
      return map[fname].apply(map, args);
    }
    
    /**
     * init if not and manage map subcall (zoom, center)
     **/
    this._subcall = function(todo, latLng){
      var opts = {};
      if (!todo.map) return;
      if (!latLng) {
        latLng = ival(todo.map, 'latlng');
      }
      if (!map){
        if (latLng) {
          opts = {center:latLng};
        }
        this.init($.extend({}, todo.map, opts), true);
      } else { 
        if (todo.map.center && latLng){
          this._call("setCenter", latLng);
        }
        if (todo.map.zoom !== undefined){
          this._call("setZoom", todo.map.zoom);
        }
        if (todo.map.mapTypeId !== undefined){
          this._call("setMapTypeId", todo.map.mapTypeId);
        }
      }
    }
    
    /**
     * attach an event to a sender 
     **/
    this._attachEvent = function(sender, name, fnc, data, once){
      google.maps.event['addListener'+(once?'Once':'')](sender, name, function(event) {
        fnc.apply($this, [sender, event, data]);
      });
    }
    
    /**
     * attach events from a container to a sender 
     * todo[
     *  events => { eventName => function, }
     *  onces  => { eventName => function, }  
     *  data   => mixed data         
     * ]
     **/
    this._attachEvents = function(sender, todo){
      var name;
      if (!todo) {
        return
      }
      if (todo.events){
        for(name in todo.events){
          if (typeof(todo.events[name]) === 'function'){
            this._attachEvent(sender, name, todo.events[name], todo.data, false);
          }
        }
      }
      if (todo.onces){
        for(name in todo.onces){
          if (typeof(todo.onces[name]) === 'function'){
            this._attachEvent(sender, name, todo.onces[name], todo.data, true);
          }
        }
      }
    }
    
    /**
     * execute callback functions 
     **/
    this._callback = function(result, todo){
      if (typeof(todo.callback) === 'function') {
        todo.callback.apply($this, [result]);
      } else if (typeof(todo.callback) === 'object') {
        for(var i=0; i<todo.callback.length; i++){
          if (typeof(todo.callback[i]) === 'function') {
            todo.callback[k].apply($this, [result]);
          }
        }
      }
    }
    
    /**
     * execute ending functions 
     **/
    this._manageEnd = function(result, todo, internal){
      var i, apply;
      if (result && (typeof(result) === 'object')){
        // attach events
        this._attachEvents(result, todo);
        // execute "apply"
        if (todo.apply && todo.apply.length){
          for(i=0; i<todo.apply.length; i++){
            apply = todo.apply[i];
            // need an existing "action" function in the result object
            if(!apply.action || (typeof(result[apply.action]) !== 'function') ) { 
              continue;
            }
            if (apply.args) {
              result[apply.action].apply(result, apply.args);
            } else {
              result[apply.action]();
            }
          }
        }
      }
      if (!internal) {
        this._callback(result, todo);
        this._end();
      }
    }
    
    //-----------------------------------------------------------------------//
    // gmap3 functions
    //-----------------------------------------------------------------------//
    
    /**
     * destroy an existing instance
     **/
    this.destroy = function(todo){
      var k;
      store.clear();
      $this.empty();
      for(k in styles){
        delete styles[ k ];
      }
      styles = {};
      if (map){
        delete map;
      }
      this._callback(null, todo);
      this._end();
    }
    
    /**
     * Initialize google.maps.Map object
     **/
    this.init = function(todo, internal){
      var o, k, opts;
      if (map) { // already initialized
        return this._end();
      }
      
      o = getObject('map', todo);
      if ( (typeof(o.options.center) === 'boolean') && o.options.center) {
        return false; // wait for an address resolution
      }
      opts = $.extend({}, _default.init, o.options);
      if (!opts.center) {
        opts.center = [_default.init.center.lat, _default.init.center.lng];
      }
      opts.center = toLatLng(opts.center);
      map = new _default.classes.Map($this.get(0), opts);
      
      // add previous added styles
      for(k in styles) {
        map.mapTypes.set(k, styles[k]);
      }
      
      this._manageEnd(map, o, internal);
      return true;
    }
    
    /**
     * returns the geographical coordinates from an address
     **/
    this.getlatlng = function(todo){
      this._resolveLatLng(todo, '_getLatLng', true);
    },
    
    this._getLatLng = function(todo, results){
      this._manageEnd(results, todo);
    },
    
    
    /**
     * returns address from latlng        
     **/
    this.getaddress = function(todo, attempt){
      var latLng = toLatLng(todo, false, true),
          address = ival(todo, 'address'),
          params = latLng ?  {latLng:latLng} : ( address ? (typeof(address) === 'string' ? {address:address} : address) : null),
          callback = ival(todo, 'callback'),
          that = this;
      if (!attempt){ // convert undefined to int
        attempt = 0;
      }
      if (params && typeof(callback) === 'function') {
        getGeocoder().geocode(
          params, 
          function(results, status) {
            if ( (status === google.maps.GeocoderStatus.OVER_QUERY_LIMIT) && (attempt < _default.queryLimit.attempt) ){
              setTimeout(function(){
                  that.getaddress(todo, attempt+1);
                },
                _default.queryLimit.delay + Math.floor(Math.random() * _default.queryLimit.random)
              );
            } else {
              var out = status === google.maps.GeocoderStatus.OK ? results : false;
              callback.apply($this, [out, status]);
              if (!out && _default.verbose){
                alert('Geocode error : ' + status);
              }
              that._end();
            }
          } 
        );
      } else {
        this._end();
      }
    }
    
    /**
     * return a route
     **/
    this.getroute = function(todo){
      var callback = ival(todo, 'callback'),
          that = this;
      if ( (typeof(callback) === 'function') && todo.options ) {
        todo.options.origin = toLatLng(todo.options.origin, true);
        todo.options.destination = toLatLng(todo.options.destination, true);
        getDirectionsService().route(
          todo.options,
          function(results, status) {
            var out = status == google.maps.DirectionsStatus.OK ? results : false;
            callback.apply($this, [out, status]);
            that._end();
          }
        );
      } else {
        this._end();
      }
    }
    
    /**
     * return the elevation of a location
     **/
    this.getelevation = function(todo){
      var fnc, path, samples, i,
          locations = [],
          callback = ival(todo, 'callback'),
          latLng = ival(todo, 'latlng'),
          that = this;
          
      if (typeof(callback) === 'function'){
        fnc = function(results, status){
          var out = status === google.maps.ElevationStatus.OK ? results : false;
          callback.apply($this, [out, status]);
          that._end();
        };
        if (latLng){
          locations.push(toLatLng(latLng));
        } else {
          locations = ival(todo, 'locations') || [];
          if (locations){
            locations = array(locations);
            for(i=0; i<locations.length; i++){
              locations[i] = toLatLng(locations[i]);
            }
          }
        }
        if (locations.length){
          getElevationService().getElevationForLocations({locations:locations}, fnc);
        } else {
          path = ival(todo, 'path');
          samples = ival(todo, 'samples');
          if (path && samples){
            for(i=0; i<path.length; i++){
              locations.push(toLatLng(path[i]));
            }
            if (locations.length){
              getElevationService().getElevationAlongPath({path:locations, samples:samples}, fnc);
            }
          }
        }
      } else {
        this._end();
      }
    }
    
    /**
     * return the distance between an origin and a destination
     *      
     **/
    this.getdistance = function(todo){
      var i, 
          callback = ival(todo, 'callback'),
          that = this;
      if ( (typeof(callback) === 'function') && todo.options && todo.options.origins && todo.options.destinations ) {
        // origins and destinations are array containing one or more address strings and/or google.maps.LatLng objects
        todo.options.origins = array(todo.options.origins);
        for(i=0; i<todo.options.origins.length; i++){
          todo.options.origins[i] = toLatLng(todo.options.origins[i], true);
        }
        todo.options.destinations = array(todo.options.destinations);
        for(i=0; i<todo.options.destinations.length; i++){
          todo.options.destinations[i] = toLatLng(todo.options.destinations[i], true);
        }
        getDistanceMatrixService().getDistanceMatrix(
          todo.options,
          function(results, status) {
            var out = status == google.maps.DistanceMatrixStatus.OK ? results : false;
            callback.apply($this, [out, status]);
            that._end();
          }
        );
      } else {
        this._end();
      }
    }
    
    /**
     * Add a marker to a map after address resolution
     * if [infowindow] add an infowindow attached to the marker   
     **/
    this.addmarker = function(todo){
      this._resolveLatLng(todo, '_addMarker');
    }
    
    this._addMarker = function(todo, latLng, internal){
      var result, oi, to,
          o = getObject('marker', todo, 'to');
      if (!internal){
        if (!latLng) {
          this._manageEnd(false, o);
          return;
        }
        this._subcall(todo, latLng);
      } else if (!latLng){
        return;
      }
      if (o.to){
        to = store.refToObj(o.to);
        result = to && (typeof(to.add) === 'function');
        if (result){
          to.add(latLng, todo);
          if (typeof(to.redraw) === 'function'){
            to.redraw();
          }
        }
        if (!internal){
          this._manageEnd(result, o);
        }
      } else {
        o.options.position = latLng;
        o.options.map = map;
        result = new _default.classes.Marker(o.options);
        if (hasKey(todo, 'infowindow')){
          oi = getObject('infowindow', todo['infowindow'], 'open');
          // if "open" is not defined, add it in first position
          if ( (oi.open === undefined) || oi.open ){
            oi.apply = array(oi.apply);
            oi.apply.unshift({action:'open', args:[map, result]});
          }
          oi.action = 'addinfowindow';
          this._planNext(oi); 
        }
        if (!internal){
          store.add('marker', result, o);
          this._manageEnd(result, o);
        }
      }
      return result;
    }
    
    /**
     * add markers (without address resolution)
     **/
    this.addmarkers = function(todo){
      if (ival(todo, 'clusters')){
        this._resolveAllLatLng(todo, 'markers', '_addclusteredmarkers');
      } else {
        this._resolveAllLatLng(todo, 'markers', '_addmarkers');
      }
    }
    
    this._addmarkers = function(todo){
      var result, o, i, latLng, marker, options = {}, tmp, to, 
          markers = ival(todo, 'markers');
      this._subcall(todo);
      if (typeof(markers) !== 'object') {
        return this._end();
      }
      o = getObject('marker', todo, ['to', 'markers']);
      
      if (o.to){
        to = store.refToObj(o.to);
        result = to && (typeof(to.add) === 'function');
        if (result){
          for(i=0; i<markers.length; i++){
            if (latLng = toLatLng(markers[i])) {
              to.add(latLng, markers[i]);
            }
          }
          if (typeof(to.redraw) === 'function'){
            to.redraw();
          }
        }
        this._manageEnd(result, o);
      } else {
        $.extend(true, options, o.options);
        options.map = map;
        result = [];
        for(i=0; i<markers.length; i++){
          if (latLng = toLatLng(markers[i])){
            if (markers[i].options){
              tmp = {};
              $.extend(true, tmp, options, markers[i].options);
              o.options = tmp;
            } else {
              o.options = options;
            }
            o.options.position = latLng;
            marker = new _default.classes.Marker(o.options);
            result.push(marker);
            o.data = markers[i].data;
            o.tag = markers[i].tag;
            store.add('marker', marker, o);
            this._manageEnd(marker, o, true);
          }
        }
        o.options = options; // restore previous for futur use
        this._callback(result, todo);
        this._end();
      }
    }
    
    this._addclusteredmarkers = function(todo){
      var clusterer, i, latLng, storeId,
          that = this,
          radius = ival(todo, 'radius'),
          maxZoom = ival(todo, 'maxZoom'),
          markers = ival(todo, 'markers'),
          styles = ival(todo, 'clusters');
      
      if (!map.getBounds()){ // map not initialised => bounds not available
        // wait for map
        google.maps.event.addListenerOnce(
          map, 
          'bounds_changed', 
          function() {
            that._addclusteredmarkers(todo);
          }
        );
        return;
      }
      
      if (typeof(radius) === 'number'){
        clusterer = new Clusterer();
        for(i=0 ; i<markers.length; i++){
          latLng = toLatLng(markers[i]);
          clusterer.add(latLng, markers[i]);
        }
        storeId = this._initClusters(todo, clusterer, radius, maxZoom, styles);
      }
      
      this._callback(storeId, todo);
      this._end();
    }
    
    
    this._initClusters = function(todo, clusterer, radius, maxZoom, styles){
      var that = this;
      
      clusterer.setRedraw(function(force){
        var same, clusters = clusterer.clusters(map, radius, maxZoom, force);
        if (clusters){
          same = clusterer.freeDiff(clusters);
          that._displayClusters(todo, clusterer, clusters, same, 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 store.add('cluster', clusterer, todo);
    }
    
    this._displayClusters = function(todo, clusterer, clusters, same, styles){
      var k, i, ii, m, done, obj, shadow, cluster, options, tmp, w, h,
          atodo,
          ctodo = hasKey(todo, 'cluster') ? getObject('', ival(todo, 'cluster')) : {},
          mtodo = hasKey(todo, 'marker') ? getObject('', ival(todo, 'marker')) : {};
      for(i=0; i<clusters.length; i++){
        if (i in same){
          continue;
        }
        cluster = clusters[i];
        done = false;
        if (cluster.idx.length > 1){
          // look for the cluster design to use
          m = 0;
          for(k in styles){
            if ( (k > m) && (k <= cluster.idx.length) ){
              m = k;
            }
          }
          if (styles[m]){ // cluster defined for the current markers count
            w = ival(styles[m], 'width');
            h = ival(styles[m], 'height');
            
            // create a custom _addOverlay command
            atodo = {};
            $.extend(
              true, 
              atodo, 
              ctodo, 
              { options:{
                  pane: 'overlayLayer',
                  content:styles[m].content.replace('CLUSTER_COUNT', cluster.idx.length),
                  offset:{
                    x: -w/2,
                    y: -h/2
                  }
                }
              }
            );
            obj = this._addOverlay(atodo, toLatLng(cluster), true);
            atodo.options.pane = 'floatShadow';
            atodo.options.content = $('<div></div>');
            atodo.options.content.width(w);
            atodo.options.content.height(h);
            shadow = this._addOverlay(atodo, toLatLng(cluster), true);
            
            // store data to the clusterer
            ctodo.data = {
              latLng: toLatLng(cluster),
              markers:[]
            };
            for(ii=0; ii<cluster.idx.length; ii++){
              ctodo.data.markers.push(
                clusterer.get(cluster.idx[ii]).marker
              );
            }
            this._attachEvents(shadow, ctodo);
            clusterer.store(cluster, obj, shadow);
            done = true;
          }
        }
        if (!done){ // cluster not defined (< min count) or = 1 so display all markers of the current cluster
          // save the defaults options for the markers
          options = {};
          $.extend(true, options, mtodo.options);
          for(ii = 0; ii <cluster.idx.length; ii++){
            m = clusterer.get(cluster.idx[ii]);
            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(mtodo, mtodo.latLng, true);
            this._attachEvents(obj, mtodo);
            clusterer.store(cluster, obj);
          }
          mtodo.options = options; // restore previous for futur use
        }
      }
    }
    
    /**
     * add an infowindow after address resolution
     **/
    this.addinfowindow = function(todo){ 
      this._resolveLatLng(todo, '_addInfoWindow');
    }
    
    this._addInfoWindow = function(todo, latLng){
      var o, infowindow, args = [];
      this._subcall(todo, latLng);
      o = getObject('infowindow', todo, ['open', 'anchor']);
      if (latLng) {
        o.options.position = latLng;
      }
      infowindow = new _default.classes.InfoWindow(o.options);
      if ( (o.open === undefined) || o.open ){
        o.apply = array(o.apply);
        args.push(map);
        if (o.anchor){
          args.push(o.anchor);
        }
        o.apply.unshift({action:'open', args:args});
      }
      store.add('infowindow', infowindow, o);
      this._manageEnd(infowindow, o);
    }
    
    
    /**
     * add a polygone / polylin on a map
     **/
    this.addpolyline = function(todo){
      this._addPoly(todo, 'Polyline', 'path');
    }
    
    this.addpolygon = function(todo){
      this._addPoly(todo, 'Polygon', 'paths');
    }
    
    this._addPoly = function(todo, poly, path){
      var i, 
          obj, latLng, 
          o = getObject(poly.toLowerCase(), todo, path);
      if (o[path]){
        o.options[path] = [];
        for(i=0; i<o[path].length; i++){
          if (latLng = toLatLng(o[path][i])){
            o.options[path].push(latLng);
          }
        }
      }
      obj = new google.maps[poly](o.options);
      obj.setMap(map);
      store.add(poly.toLowerCase(), obj, o);
      this._manageEnd(obj, o);
    }
    
    /**
     * add a circle   
     **/
    this.addcircle = function(todo){
      this._resolveLatLng(todo, '_addCircle');
    }
    
    this._addCircle = function(todo, latLng){
      var c, o = getObject('circle', todo);
      if (!latLng) {
        latLng = toLatLng(o.options.center);
      }
      if (!latLng) {
        return this._manageEnd(false, o);
      }
      this._subcall(todo, latLng);
      o.options.center = latLng;
      o.options.map = map;
      c = new _default.classes.Circle(o.options);
      store.add('circle', c, o);
      this._manageEnd(c, o);
    }
    
    /**
     * add a rectangle   
     **/
    this.addrectangle = function(todo){
      this._resolveLatLng(todo, '_addRectangle');
    }
    
    this._addRectangle = function(todo, latLng ){
      var r, o = getObject('rectangle', todo);
      o.options.bounds = toLatLngBounds(o.options.bounds, true);
      if (!o.options.bounds) {
        return this._manageEnd(false, o);
      }
      this._subcall(todo, o.options.bounds.getCenter());
      o.options.map = map;
      r = new _default.classes.Rectangle(o.options);
      store.add('rectangle', r, o);
      this._manageEnd(r, o);
    }    
    
    /**
     * add an overlay to a map after address resolution
     **/
    this.addoverlay = function(todo){
      this._resolveLatLng(todo, '_addOverlay');
    }
    
    this._addOverlay = function(todo, latLng, internal){
      var ov,  
          o = getObject('overlay', todo),
          opts =  $.extend({
                    pane: 'floatPane',
                    content: '',
                    offset:{
                      x:0,y:0
                    }
                  },
                  o.options),
          $div = $('<div></div>'),
          listeners = [];
       
       $div
          .css('border', 'none')
          .css('borderWidth', '0px')
          .css('position', 'absolute');
        $div.append(opts.content);
      
      function f() {
       _default.classes.OverlayView.call(this);
        this.setMap(map);
      }            
      
      f.prototype = new _default.classes.OverlayView();
      
      f.prototype.onAdd = function() {
        var panes = this.getPanes();
        if (opts.pane in panes) {
          $(panes[opts.pane]).append($div);
        }
      }
      f.prototype.draw = function() {
        var overlayProjection = this.getProjection(),
            ps = overlayProjection.fromLatLngToDivPixel(latLng),
            that = this;
            
        $div
          .css('left', (ps.x+opts.offset.x) + 'px')
          .css('top' , (ps.y+opts.offset.y) + 'px');
        
        $.each( ("dblclick click mouseover mousemove mouseout mouseup mousedown").split(" "), function( i, name ) {
          listeners.push(
            google.maps.event.addDomListener($div[0], name, function(e) {
              google.maps.event.trigger(that, name);
            })
          );
        });
        listeners.push(
          google.maps.event.addDomListener($div[0], "contextmenu", function(e) {
            google.maps.event.trigger(that, "rightclick");
          })
        );
      }
      f.prototype.onRemove = function() {
        for (var i = 0; i < listeners.length; i++) {
          google.maps.event.removeListener(listeners[i]);
        }
        $div.remove();
      }
      f.prototype.hide = function() {
        $div.hide();
      }
      f.prototype.show = function() {
        $div.show();
      }
      f.prototype.toggle = function() {
        if ($div) {
          if ($div.is(':visible')){
            this.show();
          } else {
            this.hide();
          }
        }
      }
      f.prototype.toggleDOM = function() {
        if (this.getMap()) {
          this.setMap(null);
        } else {
          this.setMap(map);
        }
      }
      f.prototype.getDOMElement = function() {
        return $div[0];
      }
      ov = new f();
      if (!internal){
        store.add('overlay', ov, o);
        this._manageEnd(ov, o);
      }
      return ov;
    }
    
    /**
     * add a fix panel to a map
     **/
    this.addfixpanel = function(todo){
      var o = getObject('fixpanel', todo),
          x=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.width() - $c.width() - o.options.right;
        } else if (o.options.center){
          x = ($this.width() - $c.width()) / 2;
        }
        
        if (o.options.top !== undefined){
          y = o.options.top;
        } else if (o.options.bottom !== undefined){
          y = $this.height() - $c.height() - o.options.bottom;
        } else if (o.options.middle){
          y = ($this.height() - $c.height()) / 2
        }
      
        $div = $('<div></div>')
                .css('position', 'absolute')
                .css('top', y+'px')
                .css('left', x+'px')
                .css('z-index', '1000')
                .append($c);
        
        $this.first().prepend($div);
        this._attachEvents(map, o);
        store.add('fixpanel', $div, o);
        this._callback($div, o);
      }
      this._end();
    }
    
    /**
     * add a direction renderer to a map
     **/
    this.adddirectionsrenderer = function(todo, internal){
      var dr, o = getObject('directionrenderer', todo, 'panelId');
      o.options.map = map;
      dr = new google.maps.DirectionsRenderer(o.options);
      if (o.panelId) {
        dr.setPanel(document.getElementById(o.panelId));
      }
      store.add('directionrenderer', dr, o);
      this._manageEnd(dr, o, internal);
      return dr;
    }
    
    /**
     * set a direction panel to a dom element from its ID
     **/
    this.setdirectionspanel = function(todo){
      var dr = store.get('directionrenderer'),
          o = getObject('directionpanel', todo, 'id');
      if (dr && o.id) {
        dr.setPanel(document.getElementById(o.id));
      }
      this._manageEnd(dr, o);
    }
    
    /**
     * set directions on a map (create Direction Renderer if needed)
     **/
    this.setdirections = function(todo){
      var dr = store.get('directionrenderer'),
          o = getObject('directions', todo);
      if (todo) {
        o.options.directions = todo.directions ? todo.directions : (todo.options && todo.options.directions ? todo.options.directions : null);
      }
      if (o.options.directions) {
        if (!dr) {
          dr = this.adddirectionsrenderer(o, true);
        } else {
          dr.setDirections(o.options.directions);
        }
      }
      this._manageEnd(dr, o);
    }
    
    /**
     * set a streetview to a map
     **/
    this.setstreetview = function(todo){
      var panorama,
          o = getObject('streetview', todo, 'id');
      if (o.options.position){
        o.options.position = toLatLng(o.options.position);
      }
      panorama = new _default.classes.StreetViewPanorama(document.getElementById(o.id),o.options);
      if (panorama){
        map.setStreetView(panorama);
      }
      this._manageEnd(panorama, o);
    }
    
    /**
     * add a kml layer to a map
     **/
    this.addkmllayer = function(todo){
      var kml,
          o = getObject('kmllayer', todo, 'url');
      o.options.map = map;
      if (typeof(o.url) === 'string'){
        kml = new _default.classes.KmlLayer(o.url, o.options);
      }
      store.add('kmllayer', kml, o);
      this._manageEnd(kml, o);
    }
    
    /**
     * add a traffic layer to a map
     **/
    this.addtrafficlayer = function(todo){
      var o = getObject('trafficlayer', todo),
          tl = store.get('trafficlayer');
      if (!tl){
        tl = new _default.classes.TrafficLayer();
        tl.setMap(map);
        store.add('trafficlayer', tl, o);
      }
      this._manageEnd(tl, o);
    }
    
    /**
     * add a bicycling layer to a map
     **/
    this.addbicyclinglayer = function(todo){
      var o = getObject('bicyclinglayer', todo),
          bl = store.get('bicyclinglayer');
      if (!bl){
        bl = new _default.classes.BicyclingLayer();
        bl.setMap(map);
        store.add('bicyclinglayer', bl, o);
      }
      this._manageEnd(bl, o);
    }
    
    /**
     * add a ground overlay to a map
     **/
    this.addgroundoverlay = function(todo){
      var ov,
          o = getObject('groundoverlay', todo, ['bounds', 'url']);
      o.bounds = toLatLngBounds(o.bounds);
      if (o.bounds && (typeof(o.url) === 'string')){
        ov = new _default.classes.GroundOverlay(o.url, o.bounds);
        ov.setMap(map);
        store.add('groundoverlay', ov, o);
      }
      this._manageEnd(ov, o);
    }
    
    /**
     * geolocalise the user and return a LatLng
     **/
    this.geolatlng = function(todo){
      var callback = ival(todo, 'callback');
      if (typeof(callback) === 'function') {
        if(navigator.geolocation) {
          navigator.geolocation.getCurrentPosition(
            function(position) {
              var out = new google.maps.LatLng(position.coords.latitude,position.coords.longitude);
              callback.apply($this, [out]);
            }, 
            function() {
              var out = false;
              callback.apply($this, [out]);
            }
          );
        } else if (google.gears) {
          google.gears.factory.create('beta.geolocation').getCurrentPosition(
            function(position) {
              var out = new google.maps.LatLng(position.latitude,position.longitude);
              callback.apply($this, [out]);
            }, 
            function() {
              out = false;
              callback.apply($this, [out]);
            }
          );
        } else {
          callback.apply($this, [false]);
        }
      }
      this._end();
    }
    
    /**
     * add a style to a map
     **/
    this.addstyledmap = function(todo, internal){
      var o = getObject('styledmap', todo, ['id', 'style']);
      if  (o.style && o.id && !styles[o.id]) {
        styles[o.id] = new _default.classes.StyledMapType(o.style, o.options);
        if (map) {
          map.mapTypes.set(o.id, styles[o.id]);
        }
      }
      this._manageEnd(styles[o.id], o, internal);
    }
    
    /**
     * set a style to a map (add it if needed)
     **/
    this.setstyledmap = function(todo){
      var o = getObject('styledmap', todo, ['id', 'style']);
      if (o.id) {
        this.addstyledmap(o, true);
        if (styles[o.id]) {
          map.setMapTypeId(o.id);
          this._callback(styles[o.id], todo);
        }
      }
      this._manageEnd(styles[o.id], o);
    }
    
    /**
     * remove objects from a map
     **/
    this.clear = function(todo){
      var list = array(ival(todo, 'list') || ival(todo, 'name')),
          last = ival(todo, 'last', false),
          first = ival(todo, 'first', false),
          tag = ival(todo, 'tag');
      if (tag !== undefined){
        tag = array(tag);
      }
      store.clear(list, last, first, tag);
      this._end();
    }
    
    /**
     * return objects previously created
     **/
    this.get = function(todo){
      var name = ival(todo, 'name') || 'map',
          first= ival(todo, 'first'),
          all  = ival(todo, 'all'),
          tag = ival(todo, 'tag');
      name = name.toLowerCase();
      if (name === 'map'){
        return map;
      }
      if (tag !== undefined){
        tag = array(tag);
      }
      if (first){
        return store.get(name, false, tag);
      } else if (all){
        return store.all(name, tag);
      } else {
        return store.get(name, true, tag);
      }
    }
    
    /**
     * return the max zoom of a location
     **/
    this.getmaxzoom = function(todo){
      this._resolveLatLng(todo, '_getMaxZoom');
    }
    
    this._getMaxZoom = function(todo, latLng){
      var callback = ival(todo, 'callback'),
          that = this;
      if (callback && typeof(callback) === 'function') {
        getMaxZoomService().getMaxZoomAtLatLng(
          latLng, 
          function(result) {
            var zoom = result.status === google.maps.MaxZoomStatus.OK ? result.zoom : false;
            callback.apply($this, [zoom, result.status]);
            that._end();
          }
        );
      } else {
        this._end();
      }
    }
    
    /**
     * modify default values
     **/
    this.setdefault = function(todo){
      setDefault(todo);
      this._end();
    }
    
    /**
     * autofit a map using its overlays (markers, rectangles ...)
     **/
    this.autofit = function(todo, internal){
      var names, list, obj, i, j,
          empty = true, 
          bounds = new google.maps.LatLngBounds(),
          maxZoom = ival(todo, 'maxZoom', null);

      names = store.names();
      for(i=0; i<names.length; i++){
        list = store.all(names[i]);
        for(j=0; j<list.length; j++){
          obj = list[j];
          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 && (!map.getBounds() || !map.getBounds().equals(bounds))){
        if (maxZoom !== null){  
          // fitBouds Callback event => detect zoom level and check maxZoom
          google.maps.event.addListenerOnce(
            map, 
            'bounds_changed', 
            function() {
              if (this.getZoom() > maxZoom){
                this.setZoom(maxZoom);
              }
            }
          );
        }
        map.fitBounds(bounds);
      }
      if (!internal){
        this._manageEnd(empty ? false : bounds, todo, internal);
      }
    }
    
  };
  
  //-----------------------------------------------------------------------//
  // jQuery plugin
  //-----------------------------------------------------------------------//
    
  $.fn.gmap3 = function(){
    var i, args, list = [], empty = true, results = [];
    // store all arguments in a todo list 
    for(i=0; i<arguments.length; i++){
      args = arguments[i] || {};
      // resolve string todo - action without parameters can be simplified as string 
      if (typeof(args) === 'string'){
        args = {action:args};
      }
      list.push(args);
    }
    // resolve empty call - run init
    if (!list.length) {
      list.push({});
    }
    // loop on each jQuery object
    $.each(this, function() {
      var $this = $(this),
          gmap3 = $this.data('gmap3');
      empty = false;
      if (!gmap3){
        gmap3 = new Gmap3($this);
        $this.data('gmap3', gmap3);
      }
      // direct call : bypass jQuery method (not stackable, return mixed)
      if ( (list.length == 1) && (isDirect(list[0])) ){
        results.push(gmap3._direct(list[0]));
      } else {
        gmap3._plan(list);
      }
    });
    // return for direct call (only) 
    if (results.length){
      if (results.length === 1){ // 1 css selector
        return results[0];
      } else {
        return results;
      }
    }
    // manage setDefault call
    if (empty && (arguments.length == 2) && (typeof(arguments[0]) === 'string') && (arguments[0].toLowerCase() === 'setdefault')){
      setDefault(arguments[1]);
    }
    return this;
  }

}(jQuery));
