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