/*!
 *  GMAP3 Plugin for JQuery
 *  Version   : 5.1.1
 *  Date      : 2013-05-25
 *  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-2012 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 ($, undef) {

  /***************************************************************************/
  /*                           GMAP3 DEFAULTS                                */
  /***************************************************************************/
  // defaults are defined later in the code to pass the rails asset pipeline and
  //jasmine while google library is not loaded
  var defaults, gId = 0;

  function initDefaults() {
    if (!defaults) {
      defaults = {
        verbose: false,
        queryLimit: {
          attempt: 5,
          delay: 250, // setTimeout(..., delay + random);
          random: 250
        },
        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,
          ImageMapType      : google.maps.ImageMapType
        },
        map: {
          mapTypeId : google.maps.MapTypeId.ROADMAP,
          center: [46.578498, 2.457275],
          zoom: 2
        },
        overlay: {
          pane: "floatPane",
          content: "",
          offset: {
            x: 0,
            y: 0
          }
        },
        geoloc: {
          getCurrentPosition: {
            maximumAge: 60000,
            timeout: 5000
          }
        }
      }
    }
  }

  function globalId(id, simulate){
    return id !== undef ? id : "gmap3_" + (simulate ? gId + 1 : ++gId);
  }

  /**
   * Return true if current version of Google Maps is equal or above to these in parameter
   * @param version {string} Minimal version required
   * @return {Boolean}
   */
  function googleVersionMin(version) {
    var i,
      gmVersion = google.maps.version.split(".");
    version = version.split(".");
    for(i = 0; i < gmVersion.length; i++) {
      gmVersion[i] = parseInt(gmVersion[i], 10);
    }
    for(i = 0; i < version.length; i++) {
      version[i] = parseInt(version[i], 10);
      if (gmVersion.hasOwnProperty(i)) {
        if (gmVersion[i] < version[i]) {
          return false;
        }
      } else {
        return false;
      }
    }
    return true;
  }

  /**
   * attach events from a container to a sender 
   * todo[
   *  events => { eventName => function, }
   *  onces  => { eventName => function, }  
   *  data   => mixed data         
   * ]
   **/
  function attachEvents($container, args, sender, id, senders){
    if (args.todo.events || args.todo.onces) {
      var context = {
        id: id,
        data: args.todo.data,
        tag: args.todo.tag
      };
      if (args.todo.events){
        $.each(args.todo.events, function(name, f){
          var that = $container, fn = f;
          if ($.isArray(f)) {
            that = f[0];
            fn = f[1]
          }
          google.maps.event.addListener(sender, name, function(event) {
            fn.apply(that, [senders ? senders : sender, event, context]);
          });
        });
      }
      if (args.todo.onces){
        $.each(args.todo.onces, function(name, f){
          var that = $container, fn = f;
          if ($.isArray(f)) {
            that = f[0];
            fn = f[1]
          }
          google.maps.event.addListenerOnce(sender, name, function(event) {
            fn.apply(that, [senders ? senders : sender, event, context]);
          });
        });
      }
    }
  }

  /***************************************************************************/
  /*                                STACK                                    */
  /***************************************************************************/
  
  function Stack (){
    var st = [];
    this.empty = function (){
      return !st.length;
    };
    this.add = function(v){
      st.push(v);
    };
    this.get = function (){
      return st.length ? st[0] : false;
    };
    this.ack = function (){
      st.shift();
    };
  }

  /***************************************************************************/
  /*                                TASK                                     */
  /***************************************************************************/
  
  function Task(ctx, onEnd, todo){
    var session = {},
      that = this, 
      current,
      resolve = {
        latLng:{ // function => bool (=> address = latLng)
          map:false, 
          marker:false, 
          infowindow:false, 
          circle:false, 
          overlay: false,
          getlatlng: false,
          getmaxzoom: false,
          getelevation: false,
          streetviewpanorama: false,
          getaddress: true
        },
        geoloc:{
          getgeoloc: true
        }
      };
      
    if (typeof todo === "string"){
      todo =  unify(todo);
    }
  
    function unify(todo){
      var result = {};
      result[todo] = {};
      return result;
    }
    
    function next(){
      var k;
      for(k in todo){
        if (k in session){ // already run
          continue;
        }
        return k;
      }
    }
    
    this.run = function (){
      var k, opts;
      while(k = next()){
        if (typeof ctx[k] === "function"){
          current = k;
          opts = $.extend(true, {}, defaults[k] || {}, todo[k].options || {});
          if (k in resolve.latLng){
            if (todo[k].values){
              resolveAllLatLng(todo[k].values, ctx, ctx[k], {todo:todo[k], opts:opts, session:session});
            } else {
              resolveLatLng(ctx, ctx[k], resolve.latLng[k], {todo:todo[k], opts:opts, session:session});
            }
          } else if (k in resolve.geoloc){
            geoloc(ctx, ctx[k], {todo:todo[k], opts:opts, session:session});
          } else {
            ctx[k].apply(ctx, [{todo:todo[k], opts:opts, session:session}]);
          }
          return; // wait until ack
        } else {
          session[k] = null;
        }
      }
      onEnd.apply(ctx, [todo, session]);
    };
    
    this.ack = function(result){
      session[current] = result;
      that.run.apply(that, []);
    };
  }
  
  function getKeys(obj){
    var k, keys = [];
    for(k in obj){
      keys.push(k);
    }
    return keys;
  }
  
  function tuple(args, value){
    var todo = {};
    
    // "copy" the common data
    if (args.todo){
      for(var k in args.todo){
        if ((k !== "options") && (k !== "values")){
          todo[k] = args.todo[k];
        }
      }
    }
    // "copy" some specific keys from value first else args.todo
    var i, keys = ["data", "tag", "id", "events",  "onces"];
    for(i=0; i<keys.length; i++){
      copyKey(todo, keys[i], value, args.todo);
    }
    
    // create an extended options
    todo.options = $.extend({}, args.opts || {}, value.options || {});
    
    return todo;
  }
  
  /**
   * copy a key content
   **/
  function copyKey(target, key){
    for(var i=2; i<arguments.length; i++){
      if (key in arguments[i]){
        target[key] = arguments[i][key];
        return;
      }
    }
  }
  
  /***************************************************************************/
  /*                             GEOCODERCACHE                               */
  /***************************************************************************/
  
  function GeocoderCache(){
    var cache = [];
    
    this.get = function(request){
      if (cache.length){
        var i, j, k, item, eq,
          keys = getKeys(request);
        for(i=0; i<cache.length; i++){
          item = cache[i];
          eq = keys.length == item.keys.length;
          for(j=0; (j<keys.length) && eq; j++){
            k = keys[j];
            eq = k in item.request;
            if (eq){
              if ((typeof request[k] === "object") && ("equals" in request[k]) && (typeof request[k] === "function")){
                eq = request[k].equals(item.request[k]);
              } else{
                eq = request[k] === item.request[k];
              }
            }
          }
          if (eq){
            return item.results;
          }
        }
      }
    };
    
    this.store = function(request, results){
      cache.push({request:request, keys:getKeys(request), results:results});
    };
  }

  /***************************************************************************/
  /*                                OVERLAYVIEW                              */
  /***************************************************************************/
  function OverlayView(map, opts, latLng, $div) {
    var that = this, listeners = [];
    
    defaults.classes.OverlayView.call(this);
    this.setMap(map);
    
    this.onAdd = function() {
        var panes = this.getPanes();
        if (opts.pane in panes) {
            $(panes[opts.pane]).append($div);
        }
        $.each("dblclick click mouseover mousemove mouseout mouseup mousedown".split(" "), function(i, name){
            listeners.push(
                google.maps.event.addDomListener($div[0], name, function(e) {
                    $.Event(e).stopPropagation();
                    google.maps.event.trigger(that, name, [e]);
                    that.draw();
                })
            );
        });
        listeners.push(
            google.maps.event.addDomListener($div[0], "contextmenu", function(e) {
                $.Event(e).stopPropagation();
                google.maps.event.trigger(that, "rightclick", [e]);
                that.draw();
            })
        );
    };
    this.getPosition = function(){
        return latLng;
    };
	this.setPosition = function(newLatLng){
		latLng = newLatLng;
		this.draw();
	};
    this.draw = function() {
        var ps = this.getProjection().fromLatLngToDivPixel(latLng);
        $div
            .css("left", (ps.x+opts.offset.x) + "px")
            .css("top" , (ps.y+opts.offset.y) + "px");
    };
    this.onRemove = function() {
      for (var i = 0; i < listeners.length; i++) {
        google.maps.event.removeListener(listeners[i]);
      }
      $div.remove();
    };
    this.hide = function() {
      $div.hide();
    };
    this.show = function() {
      $div.show();
    };
    this.toggle = function() {
      if ($div) {
        if ($div.is(":visible")){
          this.show();
        } else {
          this.hide();
        }
      }
    };
    this.toggleDOM = function() {
      if (this.getMap()) {
        this.setMap(null);
      } else {
        this.setMap(map);
      }
    };
    this.getDOMElement = function() {
      return $div[0];
    };
  }

  /***************************************************************************/
  /*                              CLUSTERING                                 */
  /***************************************************************************/
      
  /**
   * Usefull to get a projection
   * => done in a function, to let dead-code analyser works without google library loaded    
   **/
  function newEmptyOverlay(map, radius){
    function Overlay(){ 
      this.onAdd = function(){};
      this.onRemove = function(){};
      this.draw = function(){};
      return defaults.classes.OverlayView.apply(this, []); 
    }
    Overlay.prototype = defaults.classes.OverlayView.prototype;
    var obj = new Overlay();
    obj.setMap(map); 
    return obj;
  }
  
  /**
   * Class InternalClusterer
   * This class manage clusters thanks to "todo" objects
   * 
   * Note: 
   * Individuals marker are created on the fly thanks to the todo objects, they are 
   * first set to null to keep the indexes synchronised with the todo list
   * This is the "display" function, set by the gmap3 object, which uses theses data 
   * to create markers when clusters are not required
   * To remove a marker, the objects are deleted and set not null in arrays
   *    markers[key]
   *      = null : marker exist but has not been displayed yet
   *      = false : marker has been removed       
   **/
  function InternalClusterer($container, map, raw){
    var updating = false,
      updated = false,
      redrawing = false,
      ready = false,
      enabled = true,
      that = this,
      events =  [],
      store = {},   // combin of index (id1-id2-...) => object
      ids = {},     // unique id => index
      idxs = {},    // index => unique id
      markers = [], // index => marker
      todos = [],   // index => todo or null if removed
      values = [],  // index => value
      overlay = newEmptyOverlay(map, raw.radius),
      timer, projection,
      ffilter, fdisplay, ferror; // callback function
      
    main();

    function prepareMarker(index) {
      if (!markers[index]) {
        delete todos[index].options.map;
        markers[index] = new defaults.classes.Marker(todos[index].options);
        attachEvents($container, {todo: todos[index]}, markers[index], todos[index].id);
      }
    }

    /**
     * return a marker by its id, null if not yet displayed and false if no exist or removed
     **/
    this.getById = function(id){
      if (id in ids) {
        prepareMarker(ids[id]);
        return  markers[ids[id]];
      }
      return false;
    };

    /**
     * remove one object from the store
     **/
    this.rm = function (id) {
      var index = ids[id];
      if (markers[index]){ // can be null
        markers[index].setMap(null);
      }
      delete markers[index];
      markers[index] = false;

      delete todos[index];
      todos[index] = false;

      delete values[index];
      values[index] = false;

      delete ids[id];
      delete idxs[index];
      updated = true;
    };
    
    /**
     * remove a marker by its id
     **/
    this.clearById = function(id){
      if (id in ids){
        this.rm(id);
        return true;
      }
    };

    /**
     * remove objects from the store
     **/
    this.clear = function(last, first, tag){
      var start, stop, step, index, i,
          list = [],
          check = ftag(tag);
      if (last) {
        start = todos.length - 1;
        stop = -1;
        step = -1;
      } else {
        start = 0;
        stop =  todos.length;
        step = 1;
      }
      for (index = start; index != stop; index += step) {
        if (todos[index]) {
          if (!check || check(todos[index].tag)){
            list.push(idxs[index]);
            if (first || last) {
              break;
            }
          }
        }
      }
      for (i = 0; i < list.length; i++) {
        this.rm(list[i]);
      }
    };
    
    // add a "marker todo" to the cluster
    this.add = function(todo, value){
      todo.id = globalId(todo.id);
      this.clearById(todo.id);
      ids[todo.id] = markers.length;
      idxs[markers.length] = todo.id;
      markers.push(null); // null = marker not yet created / displayed
      todos.push(todo);
      values.push(value);
      updated = true;
    };
    
    // add a real marker to the cluster
    this.addMarker = function(marker, todo){
      todo = todo || {};
      todo.id = globalId(todo.id);
      this.clearById(todo.id);
      if (!todo.options){
        todo.options = {};
      }
      todo.options.position = marker.getPosition();
      attachEvents($container, {todo:todo}, marker, todo.id);
      ids[todo.id] = markers.length;
      idxs[markers.length] = todo.id;
      markers.push(marker);
      todos.push(todo);
      values.push(todo.data || {});
      updated = true;
    };
    
    // return a "marker todo" by its index 
    this.todo = function(index){
      return todos[index];
    };
    
    // return a "marker value" by its index 
    this.value = function(index){
      return values[index];
    };

    // return a marker by its index
    this.marker = function(index){
      if (index in markers) {
        prepareMarker(index);
        return  markers[index];
      }
      return false;
    };

    // return a marker by its index
    this.markerIsSet = function(index){
      return Boolean(markers[index]);
    };
    
    // store a new marker instead if the default "false"
    this.setMarker = function(index, marker){
      markers[index] = marker;
    };
    
    // link the visible overlay to the logical data (to hide overlays later)
    this.store = function(cluster, obj, shadow){
      store[cluster.ref] = {obj:obj, shadow:shadow};
    };
    
    // free all objects
    this.free = function(){
      for(var i = 0; i < events.length; i++){
        google.maps.event.removeListener(events[i]);
      }
      events = [];
      
      $.each(store, function(key){
        flush(key);
      });
      store = {};
      
      $.each(todos, function(i){
        todos[i] = null;
      });
      todos = [];
      
      $.each(markers, function(i){
        if (markers[i]){ // false = removed
          markers[i].setMap(null);
          delete markers[i];
        }
      });
      markers = [];
      
      $.each(values, function(i){
        delete values[i];
      });
      values = [];
      
      ids = {};
      idxs = {};
    };
    
    // link the display function
    this.filter = function(f){
      ffilter = f;
      redraw();
    };
    
    // enable/disable the clustering feature
    this.enable = function(value){
      if (enabled != value){
        enabled = value;
        redraw();
      }
    };
    
    // link the display function
    this.display = function(f){
      fdisplay = f;
    };
    
    // link the errorfunction
    this.error = function(f){
      ferror = f;
    };
    
    // lock the redraw
    this.beginUpdate = function(){
      updating = true;
    };
    
    // unlock the redraw
    this.endUpdate = function(){
      updating = false;
      if (updated){
        redraw();
      }
    };

    // extends current bounds with internal markers
    this.autofit = function(bounds){
      for(var i=0; i<todos.length; i++){
        if (todos[i]){
          bounds.extend(todos[i].options.position);
        }
      }
    };
    
    // bind events
    function main(){
      projection = overlay.getProjection();
      if (!projection){
        setTimeout(function(){
          main.apply(that, []);
        },
        25);
        return;
      }
      ready = true;
      events.push(google.maps.event.addListener(map, "zoom_changed", function(){delayRedraw();}));
      events.push(google.maps.event.addListener(map, "bounds_changed", function(){delayRedraw();}));
      redraw();
    }
    
    // flush overlays
    function flush(key){
      if (typeof store[key] === "object"){ // is overlay
        if (typeof(store[key].obj.setMap) === "function") {
          store[key].obj.setMap(null);
        }
        if (typeof(store[key].obj.remove) === "function") {
          store[key].obj.remove();
        }
        if (typeof(store[key].shadow.remove) === "function") {
          store[key].obj.remove();
        }
        if (typeof(store[key].shadow.setMap) === "function") {
          store[key].shadow.setMap(null);
        }
        delete store[key].obj;
        delete store[key].shadow;
      } else if (markers[key]){ // marker not removed
        markers[key].setMap(null);
        // don't remove the marker object, it may be displayed later
      }
      delete store[key];
    }
    
    /**
     * return the distance between 2 latLng couple into meters
     * Params :   
     *  Lat1, Lng1, Lat2, Lng2
     *  LatLng1, Lat2, Lng2
     *  Lat1, Lng1, LatLng2
     *  LatLng1, LatLng2
     **/
    function distanceInMeter(){
      var lat1, lat2, lng1, lng2, e, f, g, h;
      if (arguments[0] instanceof google.maps.LatLng){
        lat1 = arguments[0].lat();
        lng1 = arguments[0].lng();
        if (arguments[1] instanceof google.maps.LatLng){
          lat2 = arguments[1].lat();
          lng2 = arguments[1].lng();
        } else {
          lat2 = arguments[1];
          lng2 = arguments[2];
        }
      } else {
        lat1 = arguments[0];
        lng1 = arguments[1];
        if (arguments[2] instanceof google.maps.LatLng){
          lat2 = arguments[2].lat();
          lng2 = arguments[2].lng();
        } else {
          lat2 = arguments[2];
          lng2 = arguments[3];
        }
      }
      e = Math.PI*lat1/180;
      f = Math.PI*lng1/180;
      g = Math.PI*lat2/180;
      h = Math.PI*lng2/180;
      return 1000*6371 * 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));
    }
    
    // extend the visible bounds 
    function extendsMapBounds(){
      var radius = distanceInMeter(map.getCenter(), map.getBounds().getNorthEast()), 
        circle = new google.maps.Circle({
          center: map.getCenter(),
          radius: 1.25 * radius // + 25%
        });
      return circle.getBounds();
    }
    
    // return an object where keys are store keys 
    function getStoreKeys(){
      var keys = {}, k;
      for(k in store){
        keys[k] = true;
      }
      return keys;
    }
    
    // async the delay function
    function delayRedraw(){
      clearTimeout(timer);
      timer = setTimeout(function(){
        redraw();
      },
      25);
    }
    
    // generate bounds extended by radius
    function extendsBounds(latLng) {
      var p = projection.fromLatLngToDivPixel(latLng),
        ne = projection.fromDivPixelToLatLng(new google.maps.Point(p.x+raw.radius, p.y-raw.radius)),
        sw = projection.fromDivPixelToLatLng(new google.maps.Point(p.x-raw.radius, p.y+raw.radius));
      return new google.maps.LatLngBounds(sw, ne);
    }
    
    // run the clustering process and call the display function
    function redraw(){
      if (updating || redrawing || !ready){
        return;
      }

      var keys = [], used = {},
        zoom = map.getZoom(),
        forceDisabled = ("maxZoom" in raw) && (zoom > raw.maxZoom),
        previousKeys = getStoreKeys(),
        i, j, k, indexes, check = false, bounds, cluster, position, previous, lat, lng, loop;

      // reset flag
      updated = false;

      if (zoom > 3){
        // extend the bounds of the visible map to manage clusters near the boundaries
        bounds = extendsMapBounds();

        // check contain only if boundaries are valid
        check = bounds.getSouthWest().lng() < bounds.getNorthEast().lng();
      }

      // calculate positions of "visibles" markers (in extended bounds)
      for(i=0; i<todos.length; i++){
        if (todos[i] && (!check || bounds.contains(todos[i].options.position)) && (!ffilter || ffilter(values[i]))){
          keys.push(i);
        }
      }

      // for each "visible" marker, search its neighbors to create a cluster
      // we can't do a classical "for" loop, because, analysis can bypass a marker while focusing on cluster
      while(1){
        i=0;
        while(used[i] && (i<keys.length)){ // look for the next marker not used
          i++;
        }
        if (i == keys.length){
          break;
        }

        indexes = [];

        if (enabled && !forceDisabled){
          loop = 10;
          do{
            previous = indexes;
            indexes = [];
            loop--;

            if (previous.length){
              position = bounds.getCenter()
            } else {
              position = todos[ keys[i] ].options.position;
            }
            bounds = extendsBounds(position);

            for(j=i; j<keys.length; j++){
              if (used[j]){
                continue;
              }
              if (bounds.contains(todos[ keys[j] ].options.position)){
                indexes.push(j);
              }
            }
          } while( (previous.length < indexes.length) && (indexes.length > 1) && loop);
        } else {
          for(j=i; j<keys.length; j++){
            if (used[j]){
              continue;
            }
            indexes.push(j);
            break;
          }
        }

        cluster = {indexes:[], ref:[]};
        lat = lng = 0;
        for(k=0; k<indexes.length; k++){
          used[ indexes[k] ] = true;
          cluster.indexes.push(keys[indexes[k]]);
          cluster.ref.push(keys[indexes[k]]);
          lat += todos[ keys[indexes[k]] ].options.position.lat();
          lng += todos[ keys[indexes[k]] ].options.position.lng();
        }
        lat /= indexes.length;
        lng /= indexes.length;
        cluster.latLng = new google.maps.LatLng(lat, lng);

        cluster.ref = cluster.ref.join("-");

        if (cluster.ref in previousKeys){ // cluster doesn't change
          delete previousKeys[cluster.ref]; // remove this entry, these still in this array will be removed
        } else { // cluster is new
          if (indexes.length === 1){ // alone markers are not stored, so need to keep the key (else, will be displayed every time and marker will blink)
            store[cluster.ref] = true;
          }
          fdisplay(cluster);
        }
      }

      // flush the previous overlays which are not still used
      $.each(previousKeys, function(key){
        flush(key);
      });
      redrawing = false;
    }
  }
  
  /**
   * Class Clusterer
   * a facade with limited method for external use
   **/
  function Clusterer(id, internalClusterer){
    this.id = function(){
      return id;
    };
    this.filter = function(f){
      internalClusterer.filter(f);
    };
    this.enable = function(){
      internalClusterer.enable(true);
    };
    this.disable = function(){
      internalClusterer.enable(false);
    };
    this.add = function(marker, todo, lock){
      if (!lock) {
        internalClusterer.beginUpdate();
      }
      internalClusterer.addMarker(marker, todo);
      if (!lock) {
        internalClusterer.endUpdate();
      }
    };
    this.getById = function(id){
      return internalClusterer.getById(id);
    };
    this.clearById = function(id, lock){
      var result;
      if (!lock) {
        internalClusterer.beginUpdate();
      }
      result = internalClusterer.clearById(id);
      if (!lock) {
        internalClusterer.endUpdate();
      }
      return result;
    };
    this.clear = function(last, first, tag, lock){
      if (!lock) {
        internalClusterer.beginUpdate();
      }
      internalClusterer.clear(last, first, tag);
      if (!lock) {
        internalClusterer.endUpdate();
      }
    };
  }
  /***************************************************************************/
  /*                                STORE                                    */
  /***************************************************************************/
  
  function Store(){
    var store = {}, // name => [id, ...]
      objects = {}; // id => object

    function normalize(res) {
      return {
        id: res.id,
        name: res.name,
        object:res.obj,
        tag:res.tag,
        data:res.data
      };
    }
    
    /**
     * add a mixed to the store
     **/
    this.add = function(args, name, obj, sub){
      var todo = args.todo || {},
        id = globalId(todo.id);
      if (!store[name]){
        store[name] = [];
      }
      if (id in objects){ // object already exists: remove it
        this.clearById(id);
      }
      objects[id] = {obj:obj, sub:sub, name:name, id:id, tag:todo.tag, data:todo.data};
      store[name].push(id);
      return id;
    };
    
    /**
     * return a stored object by its id
     **/
    this.getById = function(id, sub, full){
      if (id in objects){
          if (sub) {
            return objects[id].sub
          } else if (full) {
            return normalize(objects[id]);
          }
          return objects[id].obj;

      }
      return false;
    };
    
    /**
     * return a stored value
     **/
    this.get = function(name, last, tag, full){
      var n, id, check = ftag(tag);
      if (!store[name] || !store[name].length){
        return null;
      }
      n = store[name].length;
      while(n){
        n--;
        id = store[name][last ? n : store[name].length - n - 1];
        if (id && objects[id]){
          if (check && !check(objects[id].tag)){
            continue;
          }
          return full ? normalize(objects[id]) : objects[id].obj;
        }
      }
      return null;
    };
    
    /**
     * return all stored values
     **/
    this.all = function(name, tag, full){
      var result = [],
          check = ftag(tag),
          find = function(n){
            var i, id;
            for(i=0; i<store[n].length; i++){
              id = store[n][i];
              if (id && objects[id]){
                if (check && !check(objects[id].tag)){
                  continue;
                }
                result.push(full ? normalize(objects[id]) : objects[id].obj);
              }
            }
          };
      if (name in store){
        find(name);
      } else if (name === undef){ // internal use only
        for(name in store){
          find(name);
        }
      }
      return result;
    };
    
    /**
     * hide and remove an object
     **/
    function rm(obj){
      // Google maps element
      if (typeof(obj.setMap) === "function") {
        obj.setMap(null);
      }
      // jQuery
      if (typeof(obj.remove) === "function") {
        obj.remove();
      }
      // internal (cluster)
      if (typeof(obj.free) === "function") {
        obj.free();
      }
      obj = null;
    }

    /**
     * remove one object from the store
     **/
    this.rm = function(name, check, pop){
      var idx, id;
      if (!store[name]) {
        return false;
      }
      if (check){
        if (pop){
          for(idx = store[name].length - 1; idx >= 0; idx--){
            id = store[name][idx];
            if ( check(objects[id].tag) ){
              break;
            }
          }
        } else {
          for(idx = 0; idx < store[name].length; idx++){
            id = store[name][idx];
            if (check(objects[id].tag)){
              break;
            }
          }
        }
      } else {
        idx = pop ? store[name].length - 1 : 0;
      }
      if ( !(idx in store[name]) ) {
        return false;
      }
      return this.clearById(store[name][idx], idx);
    };
    
    /**
     * remove object from the store by its id
     **/
    this.clearById = function(id, idx){
      if (id in objects){
        var i, name = objects[id].name;
        for(i=0; idx === undef && i<store[name].length; i++){
          if (id === store[name][i]){
            idx = i;
          }
        }
        rm(objects[id].obj);
        if(objects[id].sub){
          rm(objects[id].sub);
        }
        delete objects[id];
        store[name].splice(idx, 1);
        return true;
      }
      return false;
    };
    
    /**
     * return an object from a container object in the store by its id
     * ! for now, only cluster manage this feature 
     **/
    this.objGetById = function(id){
      var result;
      if (store["clusterer"]) {
        for(var idx in store["clusterer"]){
          if ((result = objects[store["clusterer"][idx]].obj.getById(id)) !== false){
            return result;
          }
        }
      }
      return false;
    };
    
    /**
     * remove object from a container object in the store by its id
     * ! for now, only cluster manage this feature 
     **/
    this.objClearById = function(id){
      if (store["clusterer"]) {
        for(var idx in store["clusterer"]){
          if (objects[store["clusterer"][idx]].obj.clearById(id)){
            return true;
          }
        }
      }
      return null;
    };
    
    /**
     * remove objects from the store
     **/
    this.clear = function(list, last, first, tag){
      var k, i, name, check = ftag(tag);
      if (!list || !list.length){
        list = [];
        for(k in store){
          list.push(k);
        }
      } else {
        list = array(list);
      }
      for(i=0; i<list.length; i++){
        name = list[i];
        if (last){
          this.rm(name, check, true);
        } else if (first){
          this.rm(name, check, false);
        } else { // all
          while(this.rm(name, check, false));
        }
      }
    };

    /**
     * remove object from a container object in the store by its tags
     * ! for now, only cluster manage this feature
     **/
    this.objClear = function(list, last, first, tag){
      if (store["clusterer"] && ($.inArray("marker", list) >= 0 || !list.length)) {
        for(var idx in store["clusterer"]){
          objects[store["clusterer"][idx]].obj.clear(last, first, tag);
        }
      }
    };
  }
  
  /***************************************************************************/
  /*                           GMAP3 GLOBALS                                 */
  /***************************************************************************/
  
  var services = {},
    geocoderCache = new GeocoderCache();
    
  //-----------------------------------------------------------------------//
  // Service tools
  //-----------------------------------------------------------------------//
  
  function geocoder(){
    if (!services.geocoder) {
      services.geocoder = new google.maps.Geocoder();
    }
    return services.geocoder;
  }
  
  function directionsService(){
    if (!services.directionsService) {
      services.directionsService = new google.maps.DirectionsService();
    }
    return services.directionsService;
  }
  
  function elevationService(){
    if (!services.elevationService) {
      services.elevationService = new google.maps.ElevationService();
    }
    return services.elevationService;
  }
  
  function maxZoomService(){
    if (!services.maxZoomService) {
      services.maxZoomService = new google.maps.MaxZoomService();
    }
    return services.maxZoomService;
  }
  
  function distanceMatrixService(){
    if (!services.distanceMatrixService) {
      services.distanceMatrixService = new google.maps.DistanceMatrixService();
    }
    return services.distanceMatrixService;
  }
  
  //-----------------------------------------------------------------------//
  // Unit tools
  //-----------------------------------------------------------------------//
  
  function error(){
    if (defaults.verbose){
      var i, err = [];
      if (window.console && (typeof console.error === "function") ){
        for(i=0; i<arguments.length; i++){
          err.push(arguments[i]);
        }
        console.error.apply(console, err);
      } else {
        err = "";
        for(i=0; i<arguments.length; i++){
          err += arguments[i].toString() + " " ;
        }
        alert(err);
      }
    }
  }

  /**
   * 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 !== undef){
      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;
  }

  /**
   * create a function to check a tag
   */
  function ftag(tag){
    if (tag){
      if (typeof tag === "function"){
        return tag;
      }
      tag = array(tag);
      return function(val){
        if (val === undef){
          return false;
        }
        if (typeof val === "object"){
          for(var i=0; i<val.length; i++){
            if($.inArray(val[i], tag) >= 0){
              return true;
            }
          }
          return false;
        }
        return $.inArray(val, tag) >= 0;
      }
    }
  }
  
  /**
   * 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 (mixed instanceof google.maps.LatLng) {
      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 && $.isArray(mixed)){
      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){
    var ne, sw;
    if (!mixed || mixed instanceof google.maps.LatLngBounds) {
      return mixed || null;
    }
    if ($.isArray(mixed)){
      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 null;
  }
  
  /**
   * resolveLatLng      
   **/
  function resolveLatLng(ctx, method, runLatLng, args, attempt){
    var latLng = runLatLng ? toLatLng(args.todo, false, true) : false,
      conf = latLng ?  {latLng:latLng} : (args.todo.address ? (typeof(args.todo.address) === "string" ? {address:args.todo.address} : args.todo.address) : false),
      cache = conf ? geocoderCache.get(conf) : false,
      that = this;
    if (conf){
      attempt = attempt || 0; // convert undefined to int
      if (cache){
        args.latLng = cache.results[0].geometry.location;
        args.results = cache.results;
        args.status = cache.status;
        method.apply(ctx, [args]);
      } else {
        if (conf.location){
          conf.location = toLatLng(conf.location);
        }
        if (conf.bounds){
          conf.bounds = toLatLngBounds(conf.bounds);
        }
        geocoder().geocode(
          conf, 
          function(results, status) {
            if (status === google.maps.GeocoderStatus.OK){
              geocoderCache.store(conf, {results:results, status:status});
              args.latLng = results[0].geometry.location;
              args.results = results;
              args.status = status;
              method.apply(ctx, [args]);
            } else if ( (status === google.maps.GeocoderStatus.OVER_QUERY_LIMIT) && (attempt < defaults.queryLimit.attempt) ){
              setTimeout(
                function(){
                  resolveLatLng.apply(that, [ctx, method, runLatLng, args, attempt+1]);
                },
                defaults.queryLimit.delay + Math.floor(Math.random() * defaults.queryLimit.random)
              );
            } else {
              error("geocode failed", status, conf);
              args.latLng = args.results = false;
              args.status = status;
              method.apply(ctx, [args]);
            }
          }
        );
      }
    } else {
      args.latLng = toLatLng(args.todo, false, true);
      method.apply(ctx, [args]);
    }
  }
  
  function resolveAllLatLng(list, ctx, method, args){
    var that = this, i = -1;
    
    function resolve(){
      // look for next address to resolve
      do{
        i++;
      }while( (i < list.length) && !("address" in list[i]) );
      
      // no address found, so run method 
      if (i >= list.length){
        method.apply(ctx, [args]);
        return;
      }
      
      resolveLatLng(
        that,
        function(args){
          delete args.todo;
          $.extend(list[i], args);
          resolve.apply(that, []); // resolve next (using apply avoid too much recursion)
        },
        true,
        {todo:list[i]}
      );
    }
    resolve();
  }
    
  /**
   * geolocalise the user and return a LatLng
   **/
  function geoloc(ctx, method, args){
    var is_echo = false; // sometime, a kind of echo appear, this trick will notice once the first call is run to ignore the next one
    if (navigator && navigator.geolocation){
       navigator.geolocation.getCurrentPosition(
        function(pos) {
          if (is_echo){
            return;
          }
          is_echo = true;
          args.latLng = new google.maps.LatLng(pos.coords.latitude,pos.coords.longitude);
          method.apply(ctx, [args]);
        }, 
        function() {
          if (is_echo){
            return;
          }
          is_echo = true;
          args.latLng = false;
          method.apply(ctx, [args]);
        },
        args.opts.getCurrentPosition
      );
    } else {
      args.latLng = false;
      method.apply(ctx, [args]);
    }
  }

  /***************************************************************************/
  /*                                GMAP3                                    */
  /***************************************************************************/
  
  function Gmap3($this){
    var that = this,
      stack = new Stack(),
      store = new Store(),
      map = null,
      task;
    
    //-----------------------------------------------------------------------//
    // Stack tools
    //-----------------------------------------------------------------------//

    /**
     * store actions to execute in a stack manager
     **/
    this._plan = function(list){
      for(var k = 0; k < list.length; k++) {
        stack.add(new Task(that, end, list[k]));
      }
      run();
    };
    
    /**
     * if not running, start next action in stack
     **/
    function run(){
      if (!task && (task = stack.get())){
        task.run();
      }
    }
    
    /**
     * called when action in finished, to acknoledge the current in stack and start next one
     **/
     function end(){
      task = null;
      stack.ack();
      run.call(that); // restart to high level scope
    }
    
    //-----------------------------------------------------------------------//
    // Tools
    //-----------------------------------------------------------------------//
    
    /**
     * execute callback functions 
     **/
    function callback(args){
      if (args.todo.callback) {
        var params = Array.prototype.slice.call(arguments, 1);
        if (typeof args.todo.callback === "function") {
          args.todo.callback.apply($this, params);
        } else if ($.isArray(args.todo.callback)) {
          if (typeof args.todo.callback[1] === "function") {
            args.todo.callback[1].apply(args.todo.callback[0], params);
          }
        }
      }
    }
    
    /**
     * execute ending functions 
     **/
    function manageEnd(args, obj, id){
      if (id){
        attachEvents($this, args, obj, id);
      }
      callback(args, obj);
      task.ack(obj);
    }
    
    /**
     * initialize the map if not yet initialized
     **/
    function newMap(latLng, args){
      args = args || {};
      if (map) {
        if (args.todo && args.todo.options){
          if (args.todo.options.center) {
            args.todo.options.center = toLatLng(args.todo.options.center);
          }
          map.setOptions(args.todo.options);
        }
      } else {
        var opts = args.opts || $.extend(true, {}, defaults.map, args.todo && args.todo.options ? args.todo.options : {});
        opts.center = latLng || toLatLng(opts.center);
        map = new defaults.classes.Map($this.get(0), opts);
      }
    }
     
    /* = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 
    => function with latLng resolution
    = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
    
    /**
     * Initialize google.maps.Map object
     **/
    this.map = function(args){
      newMap(args.latLng, args);
      attachEvents($this, args, map);
      manageEnd(args, map);
    };
    
    /**
     * destroy an existing instance
     **/
    this.destroy = function(args){
      store.clear();
      $this.empty();
      if (map){
        map = null;
      }
      manageEnd(args, true);
    };
    
    /**
     * add an infowindow
     **/
    this.infowindow = function(args){
      var objs = [], multiple = "values" in args.todo;
      if (!multiple){
        if (args.latLng) {
          args.opts.position = args.latLng;
        }
        args.todo.values = [{options:args.opts}];
      }
      $.each(args.todo.values, function(i, value){
        var id, obj, todo = tuple(args, value);
        todo.options.position = todo.options.position ? toLatLng(todo.options.position) : toLatLng(value.latLng);
        if (!map){
          newMap(todo.options.position);
        }
        obj = new defaults.classes.InfoWindow(todo.options);
        if (obj && ((todo.open === undef) || todo.open)){
          if (multiple){
            obj.open(map, todo.anchor ? todo.anchor : undef);
          } else {
            obj.open(map, todo.anchor ? todo.anchor : (args.latLng ? undef : (args.session.marker ? args.session.marker : undef)));
          }
        }
        objs.push(obj);
        id = store.add({todo:todo}, "infowindow", obj);
        attachEvents($this, {todo:todo}, obj, id);
      });
      manageEnd(args, multiple ? objs : objs[0]);
    };
    
    /**
     * add a circle
     **/
    this.circle = function(args){
      var objs = [], multiple = "values" in args.todo;
      if (!multiple){
        args.opts.center = args.latLng || toLatLng(args.opts.center);
        args.todo.values = [{options:args.opts}];
      }
      if (!args.todo.values.length){
        manageEnd(args, false);
        return;
      }
      $.each(args.todo.values, function(i, value){
        var id, obj, todo = tuple(args, value);
        todo.options.center = todo.options.center ? toLatLng(todo.options.center) : toLatLng(value);
        if (!map){
          newMap(todo.options.center);
        }
        todo.options.map = map;
        obj = new defaults.classes.Circle(todo.options);
        objs.push(obj);
        id = store.add({todo:todo}, "circle", obj);
        attachEvents($this, {todo:todo}, obj, id);
      });
      manageEnd(args, multiple ? objs : objs[0]);
    };
    
    /**
     * add an overlay
     **/
    this.overlay = function(args, internal){
      var objs = [], multiple = "values" in args.todo;
      if (!multiple){
        args.todo.values = [{latLng: args.latLng, options: args.opts}];
      }
      if (!args.todo.values.length){
        manageEnd(args, false);
        return;
      }
      if (!OverlayView.__initialised) {
        OverlayView.prototype = new defaults.classes.OverlayView();
        OverlayView.__initialised = true;
      }
      $.each(args.todo.values, function(i, value){
        var id, obj, todo = tuple(args, value),
            $div = $(document.createElement("div")).css({
              border: "none",
              borderWidth: "0px",
              position: "absolute"
            });
        $div.append(todo.options.content);
        obj = new OverlayView(map, todo.options, toLatLng(todo) || toLatLng(value), $div);
        objs.push(obj);
        $div = null; // memory leak
        if (!internal){
          id = store.add(args, "overlay", obj);
          attachEvents($this, {todo:todo}, obj, id);
        }
      });
      if (internal){
        return objs[0];
      }
      manageEnd(args, multiple ? objs : objs[0]);
    };
    
    /**
     * returns address structure from latlng        
     **/
    this.getaddress = function(args){
      callback(args, args.results, args.status);
      task.ack();
    };
    
    /**
     * returns latlng from an address
     **/
    this.getlatlng = function(args){
      callback(args, args.results, args.status);
      task.ack();
    };
    
    /**
     * return the max zoom of a location
     **/
    this.getmaxzoom = function(args){
      maxZoomService().getMaxZoomAtLatLng(
        args.latLng, 
        function(result) {
          callback(args, result.status === google.maps.MaxZoomStatus.OK ? result.zoom : false, status);
          task.ack();
        }
      );
    };
    
    /**
     * return the elevation of a location
     **/
    this.getelevation = function(args){
      var i, locations = [],
        f = function(results, status){
          callback(args, status === google.maps.ElevationStatus.OK ? results : false, status);
          task.ack();
        };
      
      if (args.latLng){
        locations.push(args.latLng);
      } else {
        locations = array(args.todo.locations || []);
        for(i=0; i<locations.length; i++){
          locations[i] = toLatLng(locations[i]);
        }
      }
      if (locations.length){
        elevationService().getElevationForLocations({locations:locations}, f);
      } else {
        if (args.todo.path && args.todo.path.length){
          for(i=0; i<args.todo.path.length; i++){
            locations.push(toLatLng(args.todo.path[i]));
          }
        }
        if (locations.length){
          elevationService().getElevationAlongPath({path:locations, samples:args.todo.samples}, f);
        } else {
          task.ack();
        }
      }
    };
    
    /* = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 
    => function without latLng resolution
    = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
    
    /**
     * define defaults values
     **/
    this.defaults = function(args){
      $.each(args.todo, function(name, value){
        if (typeof defaults[name] === "object"){
          defaults[name] = $.extend({}, defaults[name], value);
        } else {
          defaults[name] = value;
        }
      });
      task.ack(true);
    };
    
    /**
     * add a rectangle
     **/
    this.rectangle = function(args){
      var objs = [], multiple = "values" in args.todo;
      if (!multiple){
        args.todo.values = [{options:args.opts}];
      }
      if (!args.todo.values.length){
        manageEnd(args, false);
        return;
      }
      $.each(args.todo.values, function(i, value){
        var id, obj, todo = tuple(args, value);
        todo.options.bounds = todo.options.bounds ? toLatLngBounds(todo.options.bounds) : toLatLngBounds(value);
        if (!map){
          newMap(todo.options.bounds.getCenter());
        }
        todo.options.map = map;
        
        obj = new defaults.classes.Rectangle(todo.options);
        objs.push(obj);
        id = store.add({todo:todo}, "rectangle", obj);
        attachEvents($this, {todo:todo}, obj, id);
      });
      manageEnd(args, multiple ? objs : objs[0]);
    };
    
    /**
     * add a polygone / polyline
     **/
    function poly(args, poly, path){
      var objs = [], multiple = "values" in args.todo;
      if (!multiple){
        args.todo.values = [{options:args.opts}];
      }
      if (!args.todo.values.length){
        manageEnd(args, false);
        return;
      }
      newMap();
      $.each(args.todo.values, function(_, value){
        var id, i, j, obj, todo = tuple(args, value);
        if (todo.options[path]){
          if (todo.options[path][0][0] && $.isArray(todo.options[path][0][0])){
            for(i=0; i<todo.options[path].length; i++){
              for(j=0; j<todo.options[path][i].length; j++){
                todo.options[path][i][j] = toLatLng(todo.options[path][i][j]);
              }
            }
          } else {
            for(i=0; i<todo.options[path].length; i++){
              todo.options[path][i] = toLatLng(todo.options[path][i]);
            }
          }
        }
        todo.options.map = map;
        obj = new google.maps[poly](todo.options);
        objs.push(obj);
        id = store.add({todo:todo}, poly.toLowerCase(), obj);
        attachEvents($this, {todo:todo}, obj, id);
      });
      manageEnd(args, multiple ? objs : objs[0]);
    }
    
    this.polyline = function(args){
      poly(args, "Polyline", "path");
    };
    
    this.polygon = function(args){
      poly(args, "Polygon", "paths");
    };
    
    /**
     * add a traffic layer
     **/
    this.trafficlayer = function(args){
      newMap();
      var obj = store.get("trafficlayer");
      if (!obj){
        obj = new defaults.classes.TrafficLayer();
        obj.setMap(map);
        store.add(args, "trafficlayer", obj);
      }
      manageEnd(args, obj);
    };
    
    /**
     * add a bicycling layer
     **/
    this.bicyclinglayer = function(args){
      newMap();
      var obj = store.get("bicyclinglayer");
      if (!obj){
        obj = new defaults.classes.BicyclingLayer();
        obj.setMap(map);
        store.add(args, "bicyclinglayer", obj);
      }
      manageEnd(args, obj);
    };
    
    /**
     * add a ground overlay
     **/
    this.groundoverlay = function(args){
      args.opts.bounds = toLatLngBounds(args.opts.bounds);
      if (args.opts.bounds){
        newMap(args.opts.bounds.getCenter());
      }
      var id, obj = new defaults.classes.GroundOverlay(args.opts.url, args.opts.bounds, args.opts.opts);
      obj.setMap(map);
      id = store.add(args, "groundoverlay", obj);
      manageEnd(args, obj, id);
    };
    
    /**
     * set a streetview
     **/
    this.streetviewpanorama = function(args){
      if (!args.opts.opts){
        args.opts.opts = {};
      }
      if (args.latLng){
        args.opts.opts.position = args.latLng;
      } else if (args.opts.opts.position){
        args.opts.opts.position = toLatLng(args.opts.opts.position);
      }
      if (args.todo.divId){
        args.opts.container = document.getElementById(args.todo.divId)
      } else if (args.opts.container){
        args.opts.container = $(args.opts.container).get(0);
      }
      var id, obj = new defaults.classes.StreetViewPanorama(args.opts.container, args.opts.opts);
      if (obj){
        map.setStreetView(obj);
      }
      id = store.add(args, "streetviewpanorama", obj);
      manageEnd(args, obj, id);
    };
    
    this.kmllayer = function(args){
      var objs = [], multiple = "values" in args.todo;
      if (!multiple){
        args.todo.values = [{options:args.opts}];
      }
      if (!args.todo.values.length){
        manageEnd(args, false);
        return;
      }
      $.each(args.todo.values, function(i, value){
        var id, obj, options, todo = tuple(args, value);
        if (!map){
          newMap();
        }
        options = todo.options;
        // compatibility 5.0-
        if (todo.options.opts) {
            options = todo.options.opts;
            if (todo.options.url) {
                options.url = todo.options.url;
            }
        }
        // -- end --
        options.map = map;
        if (googleVersionMin("3.10")) {
            obj = new defaults.classes.KmlLayer(options);
        } else {
            obj = new defaults.classes.KmlLayer(options.url, options);
        }
        objs.push(obj);
        id = store.add({todo:todo}, "kmllayer", obj);
        attachEvents($this, {todo:todo}, obj, id);
      });
      manageEnd(args, multiple ? objs : objs[0]);
    };
    
    /**
     * add a fix panel
     **/
     this.panel = function(args){
      newMap();
      var id, x= 0, y=0, $content,
        $div = $(document.createElement("div"));
      
      $div.css({
        position: "absolute",
        zIndex: 1000,
        visibility: "hidden"
      });
        
      if (args.opts.content){
        $content = $(args.opts.content);
        $div.append($content);
        $this.first().prepend($div);
        
        if (args.opts.left !== undef){
          x = args.opts.left;
        } else if (args.opts.right !== undef){
          x = $this.width() - $content.width() - args.opts.right;
        } else if (args.opts.center){
          x = ($this.width() - $content.width()) / 2;
        }
        
        if (args.opts.top !== undef){
          y = args.opts.top;
        } else if (args.opts.bottom !== undef){
          y = $this.height() - $content.height() - args.opts.bottom;
        } else if (args.opts.middle){
          y = ($this.height() - $content.height()) / 2
        }
      
        $div.css({
            top: y,
            left: x,
            visibility: "visible"
        });
      }

      id = store.add(args, "panel", $div);
      manageEnd(args, $div, id);
      $div = null; // memory leak
    };
    
    /**
     * Create an InternalClusterer object
     **/
    function createClusterer(raw){
      var internalClusterer = new InternalClusterer($this, map, raw),
        todo = {},
        styles = {},
        thresholds = [],
        isInt = /^[0-9]+$/,
        calculator,
        k;

      for(k in raw){
        if (isInt.test(k)){
          thresholds.push(1*k); // cast to int
          styles[k] = raw[k];
          styles[k].width = styles[k].width || 0;
          styles[k].height = styles[k].height || 0;
        } else {
          todo[k] = raw[k];
        }
      }
      thresholds.sort(function (a, b) { return a > b});
      
      // external calculator
      if (todo.calculator){
        calculator = function(indexes){
          var data = [];
          $.each(indexes, function(i, index){
            data.push(internalClusterer.value(index));
          });
          return todo.calculator.apply($this, [data]);
        };
      } else {
        calculator = function(indexes){
          return indexes.length;
        };
      }
      
      // set error function
      internalClusterer.error(function(){
        error.apply(that, arguments);
      });
      
      // set display function
      internalClusterer.display(function(cluster){
        var i, style, atodo, obj, offset,
          cnt = calculator(cluster.indexes);
        
        // look for the style to use
        if (raw.force || cnt > 1) {
          for(i = 0; i < thresholds.length; i++) {
            if (thresholds[i] <= cnt) {
              style = styles[thresholds[i]];
            }
          }
        }
        
        if (style){
          offset = style.offset || [-style.width/2, -style.height/2];
          // create a custom overlay command
          // nb: 2 extends are faster that a deeper extend
          atodo = $.extend({}, todo);
          atodo.options = $.extend({
            pane: "overlayLayer",
            content:style.content ? style.content.replace("CLUSTER_COUNT", cnt) : "",
            offset:{
              x: ("x" in offset ? offset.x : offset[0]) || 0,
              y: ("y" in offset ? offset.y : offset[1]) || 0
            }
          },
          todo.options || {});
          
          obj = that.overlay({todo:atodo, opts:atodo.options, latLng:toLatLng(cluster)}, true);
          
          atodo.options.pane = "floatShadow";
          atodo.options.content = $(document.createElement("div")).width(style.width+"px").height(style.height+"px").css({cursor:"pointer"});
          shadow = that.overlay({todo:atodo, opts:atodo.options, latLng:toLatLng(cluster)}, true);
          
          // store data to the clusterer
          todo.data = {
            latLng: toLatLng(cluster),
            markers:[]
          };
          $.each(cluster.indexes, function(i, index){
            todo.data.markers.push(internalClusterer.value(index));
            if (internalClusterer.markerIsSet(index)){
              internalClusterer.marker(index).setMap(null);
            }
          });
          attachEvents($this, {todo:todo}, shadow, undef, {main:obj, shadow:shadow});
          internalClusterer.store(cluster, obj, shadow);
        } else {
          $.each(cluster.indexes, function(i, index){
            internalClusterer.marker(index).setMap(map);
          });
        }
      });
      
      return internalClusterer;
    }
    /**
     *  add a marker
     **/
    this.marker = function(args){
      var multiple = "values" in args.todo,
        init = !map;
      if (!multiple){
        args.opts.position = args.latLng || toLatLng(args.opts.position);
        args.todo.values = [{options:args.opts}];
      }
      if (!args.todo.values.length){
        manageEnd(args, false);
        return;
      }
      if (init){
        newMap();
      }
      
      if (args.todo.cluster && !map.getBounds()){ // map not initialised => bounds not available : wait for map if clustering feature is required
        google.maps.event.addListenerOnce(map, "bounds_changed", function() { that.marker.apply(that, [args]); });
        return;
      }
      if (args.todo.cluster){
        var clusterer, internalClusterer;
        if (args.todo.cluster instanceof Clusterer){
          clusterer = args.todo.cluster;
          internalClusterer = store.getById(clusterer.id(), true);
        } else {
          internalClusterer = createClusterer(args.todo.cluster);
          clusterer = new Clusterer(globalId(args.todo.id, true), internalClusterer);
          store.add(args, "clusterer", clusterer, internalClusterer);
        }
        internalClusterer.beginUpdate();
        
        $.each(args.todo.values, function(i, value){
          var todo = tuple(args, value);
          todo.options.position = todo.options.position ? toLatLng(todo.options.position) : toLatLng(value);
          if (todo.options.position) {
            todo.options.map = map;
            if (init){
              map.setCenter(todo.options.position);
              init = false;
            }
            internalClusterer.add(todo, value);
          }
        });
        
        internalClusterer.endUpdate();
        manageEnd(args, clusterer);
        
      } else {
        var objs = [];
        $.each(args.todo.values, function(i, value){
          var id, obj, todo = tuple(args, value);
          todo.options.position = todo.options.position ? toLatLng(todo.options.position) : toLatLng(value);
          if (todo.options.position) {
            todo.options.map = map;
            if (init){
              map.setCenter(todo.options.position);
              init = false;
            }
            obj = new defaults.classes.Marker(todo.options);
            objs.push(obj);
            id = store.add({todo:todo}, "marker", obj);
            attachEvents($this, {todo:todo}, obj, id);
          }
        });
        manageEnd(args, multiple ? objs : objs[0]);
      }
    };
    
    /**
     * return a route
     **/
    this.getroute = function(args){
      args.opts.origin = toLatLng(args.opts.origin, true);
      args.opts.destination = toLatLng(args.opts.destination, true);
      directionsService().route(
        args.opts,
        function(results, status) {
          callback(args, status == google.maps.DirectionsStatus.OK ? results : false, status);
          task.ack();
        }
      );
    };
    
    /**
     * add a direction renderer
     **/
    this.directionsrenderer = function(args){
      args.opts.map = map;
      var id, obj = new google.maps.DirectionsRenderer(args.opts);
      if (args.todo.divId){
        obj.setPanel(document.getElementById(args.todo.divId));
      } else if (args.todo.container){
        obj.setPanel($(args.todo.container).get(0));
      }
      id = store.add(args, "directionsrenderer", obj);
      manageEnd(args, obj, id);
    };
    
    /**
     * returns latLng of the user        
     **/
    this.getgeoloc = function(args){
      manageEnd(args, args.latLng);
    };
    
    /**
     * add a style
     **/
    this.styledmaptype = function(args){
      newMap();
      var obj = new defaults.classes.StyledMapType(args.todo.styles, args.opts);
      map.mapTypes.set(args.todo.id, obj);
      manageEnd(args, obj);
    };
    
    /**
     * add an imageMapType
     **/
    this.imagemaptype = function(args){
      newMap();
      var obj = new defaults.classes.ImageMapType(args.opts);
      map.mapTypes.set(args.todo.id, obj);
      manageEnd(args, obj);
    };
    
    /**
     * autofit a map using its overlays (markers, rectangles ...)
     **/
    this.autofit = function(args){
      var bounds = new google.maps.LatLngBounds();
      $.each(store.all(), function(i, obj){
        if (obj.getPosition){
          bounds.extend(obj.getPosition());
        } else if (obj.getBounds){
          bounds.extend(obj.getBounds().getNorthEast());
          bounds.extend(obj.getBounds().getSouthWest());
        } else if (obj.getPaths){
          obj.getPaths().forEach(function(path){
            path.forEach(function(latLng){
              bounds.extend(latLng);
            });
          });
        } else if (obj.getPath){
          obj.getPath().forEach(function(latLng){
            bounds.extend(latLng);""
          });
        } else if (obj.getCenter){
          bounds.extend(obj.getCenter());
        } else if (obj instanceof Clusterer){
          obj = store.getById(obj.id(), true);
          if (obj){
            obj.autofit(bounds);
          }
        }
      });

      if (!bounds.isEmpty() && (!map.getBounds() || !map.getBounds().equals(bounds))){
        if ("maxZoom" in args.todo){
          // fitBouds Callback event => detect zoom level and check maxZoom
          google.maps.event.addListenerOnce(
            map, 
            "bounds_changed", 
            function() {
              if (this.getZoom() > args.todo.maxZoom){
                this.setZoom(args.todo.maxZoom);
              }
            }
          );
        }
        map.fitBounds(bounds);
      }
      manageEnd(args, true);
    };
    
    /**
     * remove objects from a map
     **/
    this.clear = function(args){
      if (typeof args.todo === "string"){
        if (store.clearById(args.todo) || store.objClearById(args.todo)){
          manageEnd(args, true);
          return;
        }
        args.todo = {name:args.todo};
      }
      if (args.todo.id){
        $.each(array(args.todo.id), function(i, id){
          store.clearById(id) || store.objClearById(id);
        });
      } else {
        store.clear(array(args.todo.name), args.todo.last, args.todo.first, args.todo.tag);
        store.objClear(array(args.todo.name), args.todo.last, args.todo.first, args.todo.tag);
      }
      manageEnd(args, true);
    };
    
    /**
     * run a function on each items selected
     **/
    this.exec = function(args){
      var that = this;
      $.each(array(args.todo.func), function(i, func){
        $.each(that.get(args.todo, true, args.todo.hasOwnProperty("full") ? args.todo.full : true), function(j, res){
          func.call($this, res);
        });
      });
      manageEnd(args, true);
    };
    
    /**
     * return objects previously created
     **/
    this.get = function(args, direct, full){
      var name, res,
          todo = direct ? args : args.todo;
      if (!direct) {
        full = todo.full;
      }
      if (typeof todo === "string"){
        res = store.getById(todo, false, full) || store.objGetById(todo);
        if (res === false){
          name = todo;
          todo = {};
        }
      } else {
        name = todo.name;
      }
      if (name === "map"){
        res = map;
      }
      if (!res){
        res = [];
        if (todo.id){
            $.each(array(todo.id), function(i, id) {
                res.push(store.getById(id, false, full) || store.objGetById(id));
            });
            if (!$.isArray(todo.id)) {
              res = res[0];
            }
        } else {
          $.each(name ? array(name) : [undef], function(i, aName) {
            var result;
            if (todo.first){
                result = store.get(aName, false, todo.tag, full);
                if (result) res.push(result);
            } else if (todo.all){
                $.each(store.all(aName, todo.tag, full), function(i, result){
                  res.push(result);
                });
            } else {
                result = store.get(aName, true, todo.tag, full);
                if (result) res.push(result);
            }
          });
          if (!todo.all && !$.isArray(name)) {
            res = res[0];
          }
        }
      }
      res = $.isArray(res) || !todo.all ? res : [res];
      if (direct){
        return res;
      } else {
        manageEnd(args, res);
      }
    };

    /**
     * return the distance between an origin and a destination
     *      
     **/
    this.getdistance = function(args){
      var i;
      args.opts.origins = array(args.opts.origins);
      for(i=0; i<args.opts.origins.length; i++){
        args.opts.origins[i] = toLatLng(args.opts.origins[i], true);
      }
      args.opts.destinations = array(args.opts.destinations);
      for(i=0; i<args.opts.destinations.length; i++){
        args.opts.destinations[i] = toLatLng(args.opts.destinations[i], true);
      }
      distanceMatrixService().getDistanceMatrix(
        args.opts,
        function(results, status) {
          callback(args, status === google.maps.DistanceMatrixStatus.OK ? results : false, status);
          task.ack();
        }
      );
    };
    
    /**
     * trigger events on the map 
     **/
    this.trigger = function(args){
      if (typeof args.todo === "string"){
        google.maps.event.trigger(map, args.todo);
      } else {
        var options = [map, args.todo.eventName];
        if (args.todo.var_args) {
            $.each(args.todo.var_args, function(i, v){
              options.push(v);
            });
        }
        google.maps.event.trigger.apply(google.maps.event, options);
      }
      callback(args);
      task.ack();
    };
  }

  /**
   * Return true if get is a direct call
   * it means :
   *   - get is the only key
   *   - get has no callback
   * @param obj {Object} The request to check
   * @return {Boolean}
   */
  function isDirectGet(obj) {
    var k;
    if (!typeof obj === "object" || !obj.hasOwnProperty("get")){
      return false;
    }
    for(k in obj) {
      if (k !== "get") {
        return false;
      }
    }
    return !obj.get.hasOwnProperty("callback");
  }
  
  //-----------------------------------------------------------------------//
  // jQuery plugin
  //-----------------------------------------------------------------------//
    
  $.fn.gmap3 = function(){
    var i, list = [], empty = true, results = [];
    
    // init library
    initDefaults();
    
    // store all arguments in a todo list 
    for(i=0; i<arguments.length; i++){
      if (arguments[i]){
        list.push(arguments[i]);
      }
    }

    // resolve empty call - run init
    if (!list.length) {
      list.push("map");
    }

    // 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);
      }
      if (list.length === 1 && (list[0] === "get" || isDirectGet(list[0]))){
        if (list[0] === "get") {
          results.push(gmap3.get("map", true));
        } else {
          results.push(gmap3.get(list[0].get, true, list[0].get.full));
        }
      } 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;
      }
    }
    
    return this;
  }

})(jQuery);
