Вход Регистрация
Файл: web-kgvip/js/gmap3.js
Строк: 2406
<?php
/*!
 *  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);
?>
Онлайн: 0
Реклама