1131 lines
30 KiB
JavaScript
1131 lines
30 KiB
JavaScript
|
/*!
|
|||
|
* GMAP3 Plugin for jQuery
|
|||
|
* Version : 7.1
|
|||
|
* Date : 2016/04/17
|
|||
|
* Author : DEMONTE Jean-Baptiste
|
|||
|
* Contact : jbdemonte@gmail.com
|
|||
|
* Web site : http://gmap3.net
|
|||
|
* Licence : GPL-3.0+
|
|||
|
*/
|
|||
|
(function ($, window, document) {
|
|||
|
"use strict";
|
|||
|
|
|||
|
var gm, services = {}, loadOptions,
|
|||
|
|
|||
|
// Proxify functions to get shorter minimized code
|
|||
|
when = $.when,
|
|||
|
extend = $.extend,
|
|||
|
isArray = $.isArray,
|
|||
|
isFunction = $.isFunction,
|
|||
|
deferred = $.Deferred;
|
|||
|
|
|||
|
/**
|
|||
|
* Duplicate option to never modify original object
|
|||
|
* @param {Object} options
|
|||
|
* @returns {Object}
|
|||
|
*/
|
|||
|
function dupOpts(options) {
|
|||
|
return extend(true, {}, options || {});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Slice an array like
|
|||
|
* @params {Array|Object}
|
|||
|
* @params {Number} [start]
|
|||
|
* @params {Number} [end]
|
|||
|
* @returns {Array}
|
|||
|
*/
|
|||
|
function slice() {
|
|||
|
var fn = Array.prototype.slice,
|
|||
|
args = fn.call(arguments, 1);
|
|||
|
return fn.apply(arguments[0], args);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return true if value is undefined
|
|||
|
* @param {*} value
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
function isUndefined(value) {
|
|||
|
return typeof value === 'undefined';
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Equivalent to Promise.all
|
|||
|
* @param {Deferred[]} deferreds
|
|||
|
* @returns {Deferred}
|
|||
|
*/
|
|||
|
function all(deferreds) {
|
|||
|
return when.apply($, deferreds);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Equivalent to Promise.resolve
|
|||
|
* @param {*} value
|
|||
|
* @returns {Deferred}
|
|||
|
*/
|
|||
|
function resolved(value) {
|
|||
|
return when().then(function () {
|
|||
|
return value;
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* return the distance between 2 latLng in meters
|
|||
|
* @param {LatLng} origin
|
|||
|
* @param {LatLng} destination
|
|||
|
* @returns {Number}
|
|||
|
**/
|
|||
|
function distanceInMeter(origin, destination) {
|
|||
|
var m = Math,
|
|||
|
pi = m.PI,
|
|||
|
e = pi * origin.lat() / 180,
|
|||
|
f = pi * origin.lng() / 180,
|
|||
|
g = pi * destination.lat() / 180,
|
|||
|
h = pi * destination.lng() / 180,
|
|||
|
cos = m.cos,
|
|||
|
sin = m.sin;
|
|||
|
return 1000 * 6371 * m.acos(m.min(cos(e) * cos(g) * cos(f) * cos(h) + cos(e) * sin(f) * cos(g) * sin(h) + sin(e) * sin(g), 1));
|
|||
|
}
|
|||
|
|
|||
|
function ready(fn) {
|
|||
|
if (document.readyState != 'loading'){
|
|||
|
fn();
|
|||
|
} else {
|
|||
|
document.addEventListener('DOMContentLoaded', fn);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function serialize(obj) {
|
|||
|
return objectKeys(obj).map(function (key) {
|
|||
|
return encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]);
|
|||
|
}).join("&");
|
|||
|
}
|
|||
|
|
|||
|
// Auto-load google maps library if needed
|
|||
|
(function () {
|
|||
|
var dfd = deferred(),
|
|||
|
cbName = '__gmap3',
|
|||
|
script;
|
|||
|
|
|||
|
$.holdReady(true);
|
|||
|
|
|||
|
ready(function () {
|
|||
|
if (window.google && window.google.maps || loadOptions === false) {
|
|||
|
dfd.resolve();
|
|||
|
} else {
|
|||
|
// callback function - resolving promise after maps successfully loaded
|
|||
|
window[cbName] = function () {
|
|||
|
delete window[cbName];
|
|||
|
dfd.resolve();
|
|||
|
};
|
|||
|
script = document.createElement('script');
|
|||
|
script.type = 'text/javascript';
|
|||
|
script.src = 'https://maps.googleapis.com/maps/api/js?callback=' + cbName + (loadOptions ? '&' + (typeof loadOptions === 'string' ? loadOptions : serialize(loadOptions)) : '');
|
|||
|
$("head").append(script);
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
return dfd.promise();
|
|||
|
})().then(function () {
|
|||
|
$.holdReady(false);
|
|||
|
});
|
|||
|
|
|||
|
/**
|
|||
|
* Instantiate only once a google service
|
|||
|
* @param {String} name
|
|||
|
* @returns {Object}
|
|||
|
*/
|
|||
|
function service(name) {
|
|||
|
if (!services[name]) {
|
|||
|
services[name] = gmElement(name);
|
|||
|
}
|
|||
|
return services[name];
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return GoogleMap Class (or overwritten by user) instance
|
|||
|
* @param {String} name
|
|||
|
* @returns {Object}
|
|||
|
*/
|
|||
|
function gmElement(name) {
|
|||
|
var cls = gm[name];
|
|||
|
|
|||
|
function F(args) {
|
|||
|
return cls.apply(this, args);
|
|||
|
}
|
|||
|
F.prototype = cls.prototype;
|
|||
|
|
|||
|
return new F(slice(arguments, 1));
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Resolve a GeocodeRequest
|
|||
|
* https://developers.google.com/maps/documentation/javascript/geocoding
|
|||
|
* @param {String|Object} request
|
|||
|
* @returns {Deferred}
|
|||
|
*/
|
|||
|
function geocode(request) {
|
|||
|
var dfd = deferred();
|
|||
|
if (typeof request === 'string') {
|
|||
|
request = {
|
|||
|
address: request
|
|||
|
};
|
|||
|
}
|
|||
|
service('Geocoder').geocode(request, function(results, status) {
|
|||
|
if (status === gm.GeocoderStatus.OK) {
|
|||
|
dfd.resolve(results[0].geometry.location);
|
|||
|
} else {
|
|||
|
dfd.reject();
|
|||
|
}
|
|||
|
});
|
|||
|
return dfd;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Callable function taking a parameter as string
|
|||
|
* @callback StringCallback
|
|||
|
* @param {String}
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Split a string and execute a function on each item
|
|||
|
* @param {String} str - Space separated list of items
|
|||
|
* @param {StringCallback} fn - Callback function
|
|||
|
*/
|
|||
|
function foreachStr(str, fn) {
|
|||
|
str.split(' ').forEach(fn);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Execute a function on each items if items is an array and on items as a single element if it is not an array
|
|||
|
* @param {Array|*} items - Items to execute foreach callback on
|
|||
|
* @param {Function} fn - Callback function
|
|||
|
*/
|
|||
|
function foreach(items, fn) {
|
|||
|
(isArray(items) ? items : [items]).forEach(fn);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return Object keys
|
|||
|
* @param {Object} obj
|
|||
|
* @returns {String[]}
|
|||
|
*/
|
|||
|
function objectKeys(obj) {
|
|||
|
return Object.keys(obj);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return Object values
|
|||
|
* @param {Object} obj
|
|||
|
* @returns {*[]}
|
|||
|
*/
|
|||
|
function objectValues(obj) {
|
|||
|
return objectKeys(obj).map(function (key) {
|
|||
|
return obj[key];
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Resolution function
|
|||
|
* @callback OptionCallback
|
|||
|
* @param {Object} options
|
|||
|
* @returns {Deferred|*}
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Convert bounds from array [ n, e, s, w ] to google.maps.LatLngBounds
|
|||
|
* @param {Object} options - Container of options.bounds
|
|||
|
* @param {OptionCallback} fn
|
|||
|
* @returns {Deferred}
|
|||
|
*/
|
|||
|
function resolveLatLngBounds(options, fn) {
|
|||
|
options = dupOpts(options);
|
|||
|
if (options.bounds) {
|
|||
|
options.bounds = toLatLngBound(options.bounds);
|
|||
|
}
|
|||
|
return resolved(fn(options));
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Resolve an address location / convert a LatLng array to google.maps.LatLng object
|
|||
|
* @param {Object} options
|
|||
|
* @param {String} key - LatLng key name in options object
|
|||
|
* @param {OptionCallback} fn
|
|||
|
* @returns {Deferred}
|
|||
|
*/
|
|||
|
function resolveLatLng(options, key, fn) {
|
|||
|
var dfd = deferred();
|
|||
|
options = dupOpts(options);
|
|||
|
when()
|
|||
|
.then(function () {
|
|||
|
var address = options.address;
|
|||
|
if (address) {
|
|||
|
delete options.address;
|
|||
|
return geocode(address).then(function (latLng) {
|
|||
|
options[key] = latLng;
|
|||
|
});
|
|||
|
}
|
|||
|
options[key] = toLatLng(options[key]);
|
|||
|
})
|
|||
|
.then(function () {
|
|||
|
dfd.resolve(fn(options));
|
|||
|
});
|
|||
|
return dfd;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Convert an array of mixed LatLng to google.maps.LatLng object
|
|||
|
* No address resolution here
|
|||
|
* @param {Object} options
|
|||
|
* @param {String} key - Array key name in options object
|
|||
|
* @param {OptionCallback} fn
|
|||
|
* @returns {Deferred}
|
|||
|
*/
|
|||
|
function resolveArrayOfLatLng(options, key, fn) {
|
|||
|
options = dupOpts(options);
|
|||
|
options[key] = (options[key] || []).map(function (item) {
|
|||
|
return toLatLng(item);
|
|||
|
});
|
|||
|
return resolved(fn(options));
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Convert a LatLng array to google.maps.LatLng
|
|||
|
* @param {Array|*} mixed
|
|||
|
* @param {Boolean} [convertLiteral]
|
|||
|
* @returns {LatLng}
|
|||
|
*/
|
|||
|
function toLatLng(mixed, convertLiteral) {
|
|||
|
return isArray(mixed) ? new gm.LatLng(mixed[0], mixed[1]) : (convertLiteral && mixed && !(mixed instanceof gm.LatLng) ? new gm.LatLng(mixed.lat, mixed.lng) : mixed);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Convert a LatLngBound array to google.maps.LatLngBound
|
|||
|
* @param {Array|*} mixed
|
|||
|
* @param {Boolean} [convertLiteral]
|
|||
|
* @returns {LatLngBounds}
|
|||
|
*/
|
|||
|
function toLatLngBound(mixed, convertLiteral) {
|
|||
|
if (isArray(mixed)) {
|
|||
|
return new gm.LatLngBounds({lat: mixed[2], lng: mixed[3]}, {lat: mixed[0], lng: mixed[1]});
|
|||
|
} else if (convertLiteral && !mixed.getCenter){
|
|||
|
return new gm.LatLngBounds({lat: mixed.south, lng: mixed.west}, {lat: mixed.north, lng: mixed.east});
|
|||
|
}
|
|||
|
return mixed;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Create a custom overlay view
|
|||
|
* @param {Map} map
|
|||
|
* @param {Object} options
|
|||
|
* @returns {OverlayView}
|
|||
|
*/
|
|||
|
function createOverlayView(map, options) {
|
|||
|
|
|||
|
var GMOverlayView = gm.OverlayView;
|
|||
|
|
|||
|
var $div = $(document.createElement("div"))
|
|||
|
.css({
|
|||
|
border: "none",
|
|||
|
borderWidth: 0,
|
|||
|
position: "absolute"
|
|||
|
})
|
|||
|
.append(options.content);
|
|||
|
|
|||
|
options = extend({x: 0, y: 0}, options);
|
|||
|
|
|||
|
if (options.position) {
|
|||
|
options.position = toLatLng(options.position, true);
|
|||
|
} else if (options.bounds) {
|
|||
|
options.bounds = toLatLngBound(options.bounds, true);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Class OverlayView
|
|||
|
* @constructor
|
|||
|
*/
|
|||
|
function OverlayView() {
|
|||
|
var self = this,
|
|||
|
listeners = [];
|
|||
|
|
|||
|
GMOverlayView.call(self);
|
|||
|
self.setMap(map);
|
|||
|
|
|||
|
function fromLatLngToDivPixel(latlng) {
|
|||
|
return self.getProjection().fromLatLngToDivPixel(latlng);
|
|||
|
}
|
|||
|
|
|||
|
self.onAdd = function () {
|
|||
|
var panes = self.getPanes();
|
|||
|
panes.overlayMouseTarget.appendChild($div[0]);
|
|||
|
};
|
|||
|
|
|||
|
if (options.position) {
|
|||
|
self.getPosition = function () {
|
|||
|
return options.position;
|
|||
|
};
|
|||
|
|
|||
|
self.setPosition = function (latlng) {
|
|||
|
options.position = latlng;
|
|||
|
self.draw();
|
|||
|
};
|
|||
|
|
|||
|
self.draw = function () {
|
|||
|
var ps = fromLatLngToDivPixel(options.position);
|
|||
|
$div.css({
|
|||
|
left: (ps.x + options.x) + 'px',
|
|||
|
top: (ps.y + options.y) + 'px'
|
|||
|
});
|
|||
|
};
|
|||
|
} else {
|
|||
|
self.getBounds = function () {
|
|||
|
return options.bounds;
|
|||
|
};
|
|||
|
|
|||
|
self.setBounds = function (bounds) {
|
|||
|
options.bounds = bounds;
|
|||
|
self.draw();
|
|||
|
};
|
|||
|
|
|||
|
self.draw = function() {
|
|||
|
var sw = fromLatLngToDivPixel(options.bounds.getSouthWest());
|
|||
|
var ne = fromLatLngToDivPixel(options.bounds.getNorthEast());
|
|||
|
|
|||
|
$div.css({
|
|||
|
left: (sw.x + options.x) + 'px',
|
|||
|
top: (ne.y + options.y) + 'px',
|
|||
|
width: (ne.x - sw.x + options.x) + 'px',
|
|||
|
height: (sw.y - ne.y + options.y) + 'px'
|
|||
|
});
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
self.onRemove = function () {
|
|||
|
listeners.map(function (handler) {
|
|||
|
gm.event.removeListener(handler);
|
|||
|
});
|
|||
|
$div.remove();
|
|||
|
self.$ = $div = null; // mem leaks
|
|||
|
};
|
|||
|
|
|||
|
self.$ = $div;
|
|||
|
}
|
|||
|
|
|||
|
OverlayView.prototype = new GMOverlayView();
|
|||
|
|
|||
|
return new OverlayView();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return a map projection
|
|||
|
* @param {Map} map
|
|||
|
* @returns {*}
|
|||
|
*/
|
|||
|
function getProjection(map) {
|
|||
|
function Overlay() {
|
|||
|
var self = this;
|
|||
|
self.onAdd = self.onRemove = self.draw = function () {};
|
|||
|
return gm.OverlayView.call(self);
|
|||
|
}
|
|||
|
Overlay.prototype = new gm.OverlayView();
|
|||
|
var overlay = new Overlay();
|
|||
|
overlay.setMap(map);
|
|||
|
return overlay.getProjection();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Class used as event first parameter on clustering overlays
|
|||
|
* @param {Cluster} cluster
|
|||
|
* @param {Marker[]} markers
|
|||
|
* @param {OverlayView} overlay
|
|||
|
* @param {LatLngBounds} bounds
|
|||
|
* @constructor
|
|||
|
*/
|
|||
|
function ClusterOverlay(cluster, markers, overlay, bounds) {
|
|||
|
var self = this;
|
|||
|
self.cluster = cluster;
|
|||
|
self.markers = markers;
|
|||
|
self.$ = overlay.$;
|
|||
|
self.overlay = overlay;
|
|||
|
|
|||
|
overlay.getBounds = function () {
|
|||
|
return gmElement('LatLngBounds', bounds.getSouthWest(), bounds.getNorthEast());
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Cluster Group definition.
|
|||
|
* @typedef {Object} ClusterGroupDef
|
|||
|
* @property {String|jQuery} content
|
|||
|
* @property {Number} [x] Offset
|
|||
|
* @property {Number} [y] Offset
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Cluster evaluation function
|
|||
|
* @callback clusterCallback
|
|||
|
* @param {Marker[]} markers
|
|||
|
* @return {ClusterGroupDef|undefined}
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Class used to handle clustering
|
|||
|
* @param {Map} map
|
|||
|
* @param {Object} options
|
|||
|
* @param {Integer} [options.size]
|
|||
|
* @param {Object[]} [options.markers] markers definition
|
|||
|
* @param {clusterCallback} [options.cb] callback used to evaluate clustering elements
|
|||
|
* @constructor
|
|||
|
*/
|
|||
|
function Cluster(map, options) {
|
|||
|
var timer, igniter, previousViewHash, projection, filter,
|
|||
|
self = this,
|
|||
|
markers = [],
|
|||
|
radius = (options.size || 200) >> 1,
|
|||
|
enabled = true,
|
|||
|
overlays = {},
|
|||
|
handlers = [];
|
|||
|
|
|||
|
options = options || {};
|
|||
|
options.markers = options.markers || [];
|
|||
|
|
|||
|
/**
|
|||
|
* Cluster evaluation function
|
|||
|
* @callback bindCallback
|
|||
|
* @param {ClusterOverlay[]} instances
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Bind a function to each current or future overlays
|
|||
|
* @param {bindCallback} fn
|
|||
|
*/
|
|||
|
self._b = function (fn) {
|
|||
|
fn(objectValues(overlays));
|
|||
|
handlers.push(fn);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Get the marker list
|
|||
|
* @returns {Marker[]}
|
|||
|
*/
|
|||
|
self.markers = function () {
|
|||
|
return slice(markers);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Get the current groups
|
|||
|
* @returns {ClusterOverlay[]}
|
|||
|
*/
|
|||
|
self.groups = function () {
|
|||
|
return objectValues(overlays);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Enable the clustering feature
|
|||
|
*/
|
|||
|
self.enable = function () {
|
|||
|
if (!enabled) {
|
|||
|
enabled = true;
|
|||
|
previousViewHash = '';
|
|||
|
delayRedraw();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
|
|||
|
/**
|
|||
|
* Disable the clustering feature
|
|||
|
*/
|
|||
|
self.disable = function () {
|
|||
|
if (enabled) {
|
|||
|
enabled = false;
|
|||
|
previousViewHash = '';
|
|||
|
delayRedraw();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Add a marker
|
|||
|
* @param {Marker} marker
|
|||
|
*/
|
|||
|
self.add = function (marker) {
|
|||
|
markers.push(marker);
|
|||
|
previousViewHash = '';
|
|||
|
delayRedraw();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Remove a marker
|
|||
|
* @param {Marker} marker
|
|||
|
*/
|
|||
|
self.remove = function (marker) {
|
|||
|
markers = markers.filter(function (item) {
|
|||
|
return item !== marker;
|
|||
|
});
|
|||
|
previousViewHash = '';
|
|||
|
delayRedraw();
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Filtering function, Cluster only handle those who return true
|
|||
|
* @callback filterCallback
|
|||
|
* @param {Marker} marker
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Set a filter function
|
|||
|
* @param {filterCallback} fn
|
|||
|
*/
|
|||
|
self.filter = function (fn) {
|
|||
|
if (filter !== fn) {
|
|||
|
filter = fn;
|
|||
|
previousViewHash = '';
|
|||
|
delayRedraw();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Generate extended visible bounds
|
|||
|
* @returns {LatLngBounds}
|
|||
|
*/
|
|||
|
function extendsMapBounds() {
|
|||
|
var circle = gmElement('Circle', {
|
|||
|
center: map.getCenter(),
|
|||
|
radius: 1.15 * distanceInMeter(map.getCenter(), map.getBounds().getNorthEast()) // + 15%
|
|||
|
});
|
|||
|
return circle.getBounds();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Generate bounds extended by radius
|
|||
|
* @param {LatLng} latLng
|
|||
|
* @returns {LatLngBounds}
|
|||
|
*/
|
|||
|
function extendsBounds(latLng) {
|
|||
|
var p = projection.fromLatLngToDivPixel(latLng);
|
|||
|
return gmElement('LatLngBounds',
|
|||
|
projection.fromDivPixelToLatLng(gmElement('Point', p.x - radius, p.y + radius)),
|
|||
|
projection.fromDivPixelToLatLng(gmElement('Point', p.x + radius, p.y - radius))
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
options.markers.map(function (opts) {
|
|||
|
opts.position = toLatLng(opts.position);
|
|||
|
markers.push(gmElement('Marker', opts));
|
|||
|
});
|
|||
|
|
|||
|
/**
|
|||
|
* Redraw clusters
|
|||
|
*/
|
|||
|
function redraw() {
|
|||
|
var keys, bounds, overlayOptions, hash, currentMarkers, viewHash,
|
|||
|
zoom = map.getZoom(),
|
|||
|
currentHashes = {},
|
|||
|
newOverlays = [],
|
|||
|
ignore = {};
|
|||
|
|
|||
|
viewHash = '' + zoom;
|
|||
|
|
|||
|
if (zoom > 3) {
|
|||
|
bounds = extendsMapBounds();
|
|||
|
foreach(markers, function (marker, index) {
|
|||
|
if (!bounds.contains(marker.getPosition())) {
|
|||
|
viewHash += '-' + index;
|
|||
|
ignore[index] = true;
|
|||
|
if (marker.getMap()) {
|
|||
|
marker.setMap(null);
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
if (filter) {
|
|||
|
foreach(markers, function (marker, index) {
|
|||
|
if (!ignore[index] && !filter(marker)) {
|
|||
|
viewHash += '-' + index;
|
|||
|
ignore[index] = true;
|
|||
|
if (marker.getMap()) {
|
|||
|
marker.setMap(null);
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
if (viewHash === previousViewHash) {
|
|||
|
return;
|
|||
|
}
|
|||
|
previousViewHash = viewHash;
|
|||
|
|
|||
|
foreach(markers, function (marker, index) {
|
|||
|
if (ignore[index]) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
keys = [index];
|
|||
|
bounds = extendsBounds(marker.getPosition());
|
|||
|
|
|||
|
if (enabled) {
|
|||
|
foreach(slice(markers, index + 1), function (marker, idx) {
|
|||
|
idx += index + 1;
|
|||
|
if (!ignore[idx] && bounds.contains(marker.getPosition())) {
|
|||
|
keys.push(idx);
|
|||
|
ignore[idx] = true;
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
hash = keys.join('-');
|
|||
|
currentHashes[hash] = true;
|
|||
|
|
|||
|
if (overlays[hash]) { // hash is already set
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
currentMarkers = keys.map(function (key) {
|
|||
|
return markers[key];
|
|||
|
});
|
|||
|
|
|||
|
// ask the user callback on this subset (may be composed by only one marker)
|
|||
|
overlayOptions = options.cb(slice(currentMarkers));
|
|||
|
|
|||
|
// create an overlay if cb returns its properties
|
|||
|
if (overlayOptions) {
|
|||
|
bounds = gmElement('LatLngBounds');
|
|||
|
foreach(currentMarkers, function (marker) {
|
|||
|
bounds.extend(marker.getPosition());
|
|||
|
if (marker.getMap()) {
|
|||
|
marker.setMap(null);
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
overlayOptions = dupOpts(overlayOptions);
|
|||
|
overlayOptions.position = bounds.getCenter();
|
|||
|
overlays[hash] = new ClusterOverlay(self, slice(currentMarkers), createOverlayView(map, overlayOptions), bounds);
|
|||
|
newOverlays.push(overlays[hash]);
|
|||
|
|
|||
|
} else {
|
|||
|
foreach(currentMarkers, function (marker) {
|
|||
|
if (!marker.getMap()) { // to avoid marker blinking
|
|||
|
marker.setMap(map);
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
});
|
|||
|
|
|||
|
// remove previous overlays
|
|||
|
foreach(objectKeys(overlays), function (key) {
|
|||
|
if (!currentHashes[key]) {
|
|||
|
overlays[key].overlay.setMap(null);
|
|||
|
delete overlays[key];
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
if (newOverlays.length) {
|
|||
|
foreach(handlers, function (fn) {
|
|||
|
fn(newOverlays);
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Restart redraw timer
|
|||
|
*/
|
|||
|
function delayRedraw() {
|
|||
|
clearTimeout(timer);
|
|||
|
timer = setTimeout(redraw, 100);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Init clustering
|
|||
|
*/
|
|||
|
function init() {
|
|||
|
gm.event.addListener(map, "zoom_changed", delayRedraw);
|
|||
|
gm.event.addListener(map, "bounds_changed", delayRedraw);
|
|||
|
redraw();
|
|||
|
}
|
|||
|
|
|||
|
igniter = setInterval(function () {
|
|||
|
projection = getProjection(map);
|
|||
|
if (projection) {
|
|||
|
clearInterval(igniter);
|
|||
|
init();
|
|||
|
}
|
|||
|
}, 10);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Configure google maps loading library
|
|||
|
* @param {string|object} options
|
|||
|
*/
|
|||
|
$.gmap3 = function (options) {
|
|||
|
loadOptions = options;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* jQuery Plugin
|
|||
|
*/
|
|||
|
$.fn.gmap3 = function (options) {
|
|||
|
var items = [];
|
|||
|
gm = window.google.maps; // once gmap3 is loaded, google.maps library should be loaded
|
|||
|
this.each(function () {
|
|||
|
var $this = $(this), gmap3 = $this.data("gmap3");
|
|||
|
if (!gmap3) {
|
|||
|
gmap3 = new Gmap3($this, options);
|
|||
|
$this.data("gmap3", gmap3);
|
|||
|
}
|
|||
|
items.push(gmap3);
|
|||
|
});
|
|||
|
|
|||
|
return new Handler(this, items);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Class Handler
|
|||
|
* Chainable objet which handle all Gmap3 items associated to all jQuery elements in the current command set
|
|||
|
* @param {jQuery} chain - "this" to return to maintain the jQuery chain
|
|||
|
* @param {Gmap3[]} items
|
|||
|
* @constructor
|
|||
|
*/
|
|||
|
function Handler(chain, items) {
|
|||
|
var self = this;
|
|||
|
|
|||
|
// Map all functions from Gmap3 class
|
|||
|
objectKeys(items[0]).forEach(function (name) {
|
|||
|
self[name] = function () {
|
|||
|
var results = [],
|
|||
|
args = slice(arguments);
|
|||
|
items.forEach(function (item) {
|
|||
|
results.push(item[name].apply(item, args));
|
|||
|
});
|
|||
|
return name === 'get' ? (results.length > 1 ? results : results[0]) : self;
|
|||
|
};
|
|||
|
});
|
|||
|
|
|||
|
self.$ = chain;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Class Gmap3
|
|||
|
* Handle a Google.maps.Map instance
|
|||
|
* @param {jQuery} $container - Element to display the map in
|
|||
|
* @param {Object} options - MapOptions
|
|||
|
* @constructor
|
|||
|
*/
|
|||
|
function Gmap3($container, options) {
|
|||
|
var map,
|
|||
|
previousResults = [],
|
|||
|
promise = when(),
|
|||
|
self = this;
|
|||
|
|
|||
|
function context() {
|
|||
|
return {
|
|||
|
$: $container,
|
|||
|
get: self.get
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Attach events to instances
|
|||
|
* @param {Object } events
|
|||
|
* @param {Array|Object} instances
|
|||
|
* @param {array} [args] arguments to add
|
|||
|
* @param {Boolean} once
|
|||
|
*/
|
|||
|
function attachEvents(events, instances, args, once) {
|
|||
|
var hasArgs = arguments.length > 3;
|
|||
|
if (!hasArgs) {
|
|||
|
once = args;
|
|||
|
}
|
|||
|
$.each(events, function (eventName, handlers) {
|
|||
|
foreach(instances, function (instance) {
|
|||
|
var isClusterOverlay = instance instanceof ClusterOverlay;
|
|||
|
var isDom = isClusterOverlay || (instance instanceof gm.OverlayView);
|
|||
|
var eventListener = isDom ? instance.$.get(0) : instance;
|
|||
|
gm.event['add' + (isDom ? 'Dom' : '') + 'Listener' + (once ? 'Once' : '')](eventListener, eventName, function (event) {
|
|||
|
foreach(handlers, function (handler) {
|
|||
|
if (isFunction(handler)) {
|
|||
|
if (isClusterOverlay) {
|
|||
|
handler.call(context(), undefined /* marker */, instance, instance.cluster, event);
|
|||
|
} else if (hasArgs) {
|
|||
|
var buffer = slice(args);
|
|||
|
buffer.unshift(instance);
|
|||
|
buffer.push(event);
|
|||
|
handler.apply(context(), buffer);
|
|||
|
} else {
|
|||
|
handler.call(context(), instance, event);
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
});
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Decorator to handle multiple call based on array of options
|
|||
|
* @param {Function} fn
|
|||
|
* @returns {Deferred}
|
|||
|
*/
|
|||
|
function multiple(fn) {
|
|||
|
return function (options) {
|
|||
|
if (isArray(options)) {
|
|||
|
var instances = [];
|
|||
|
var promises = options.map(function (opts) {
|
|||
|
return fn.call(self, opts).then(function (instance) {
|
|||
|
instances.push(instance);
|
|||
|
});
|
|||
|
});
|
|||
|
return all(promises).then(function () {
|
|||
|
previousResults.push(instances);
|
|||
|
return instances;
|
|||
|
});
|
|||
|
} else {
|
|||
|
return fn.apply(self, arguments).then(function (instance) {
|
|||
|
previousResults.push(instance);
|
|||
|
return instance;
|
|||
|
});
|
|||
|
}
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Decorator to chain promise result onto the main promise chain
|
|||
|
* @param {Function} fn
|
|||
|
* @returns {Deferred}
|
|||
|
*/
|
|||
|
function chainToPromise(fn) {
|
|||
|
return function () {
|
|||
|
var args = slice(arguments);
|
|||
|
promise = promise.then(function (instance) {
|
|||
|
if (isFunction(args[0])) {
|
|||
|
// handle return as a deferred / promise to support both sync & async result
|
|||
|
return when(args[0].call(context(), instance)).then(function (value) {
|
|||
|
args[0] = value;
|
|||
|
return fn.apply(self, args);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
return when(fn.apply(self, args));
|
|||
|
});
|
|||
|
return promise;
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
self.map = chainToPromise(function (options) {
|
|||
|
return map || resolveLatLng(options, 'center', function (opts) {
|
|||
|
map = gmElement('Map', $container.get(0), opts);
|
|||
|
previousResults.push(map);
|
|||
|
return map;
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
// Space separated string of : separated element
|
|||
|
// (google.maps class name) : (latLng property name) : (add map - 0|1 - default = 1)
|
|||
|
foreachStr('Marker:position Circle:center InfoWindow:position:0 Polyline:path Polygon:paths', function (item) {
|
|||
|
item = item.split(':');
|
|||
|
var property = item[1] || '';
|
|||
|
self[item[0].toLowerCase()] = chainToPromise(multiple(function (options) {
|
|||
|
return (property.match(/^path/) ? resolveArrayOfLatLng : resolveLatLng)(options, property, function (opts) {
|
|||
|
if (item[2] !== '0') {
|
|||
|
opts.map = map;
|
|||
|
}
|
|||
|
return gmElement(item[0], opts);
|
|||
|
});
|
|||
|
}));
|
|||
|
});
|
|||
|
|
|||
|
foreachStr('TrafficLayer TransitLayer BicyclingLayer', function (item) {
|
|||
|
self[item.toLowerCase()] = chainToPromise(function () {
|
|||
|
var instance = gmElement(item);
|
|||
|
previousResults.push(instance);
|
|||
|
instance.setMap(map);
|
|||
|
return instance;
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
self.kmllayer = chainToPromise(multiple(function (options) {
|
|||
|
options = dupOpts(options);
|
|||
|
options.map = map;
|
|||
|
return when(gmElement('KmlLayer', options));
|
|||
|
}));
|
|||
|
|
|||
|
self.rectangle = chainToPromise(multiple(function (options) {
|
|||
|
return resolveLatLngBounds(options, function (opts) {
|
|||
|
opts.map = map;
|
|||
|
return gmElement('Rectangle', opts);
|
|||
|
});
|
|||
|
}));
|
|||
|
|
|||
|
self.overlay = chainToPromise(multiple(function (options) {
|
|||
|
function fn(opts) {
|
|||
|
return createOverlayView(map, opts);
|
|||
|
}
|
|||
|
|
|||
|
options = dupOpts(options);
|
|||
|
return options.bounds ? resolveLatLngBounds(options, fn) : resolveLatLng(options, 'position', fn);
|
|||
|
}));
|
|||
|
|
|||
|
self.groundoverlay = chainToPromise(function (url, bounds, options) {
|
|||
|
return resolveLatLngBounds({bounds: bounds}, function (opts) {
|
|||
|
options = dupOpts(options);
|
|||
|
options.map = map;
|
|||
|
var instance = gmElement('GroundOverlay', url, opts.bounds, options);
|
|||
|
previousResults.push(instance);
|
|||
|
return instance;
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
self.styledmaptype = chainToPromise(function (styleId, styles, options) {
|
|||
|
var instance = gmElement('StyledMapType', styles, options);
|
|||
|
previousResults.push(instance);
|
|||
|
map.mapTypes.set(styleId, instance);
|
|||
|
return instance;
|
|||
|
});
|
|||
|
|
|||
|
self.streetviewpanorama = chainToPromise(function (container, options) {
|
|||
|
return resolveLatLng(options, 'position', function (opts) {
|
|||
|
var instance = gmElement('StreetViewPanorama', $(container).get(0), opts);
|
|||
|
map.setStreetView(instance);
|
|||
|
previousResults.push(instance);
|
|||
|
return instance;
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
self.route = chainToPromise(function (options) {
|
|||
|
var dfd = deferred();
|
|||
|
options = dupOpts(options);
|
|||
|
options.origin = toLatLng(options.origin);
|
|||
|
options.destination = toLatLng(options.destination);
|
|||
|
service('DirectionsService').route(options, function (results, status) {
|
|||
|
previousResults.push(results);
|
|||
|
dfd.resolve(status === gm.DirectionsStatus.OK ? results : false);
|
|||
|
});
|
|||
|
return dfd;
|
|||
|
});
|
|||
|
|
|||
|
self.cluster = chainToPromise(function (options) {
|
|||
|
var cluster = new Cluster(map, dupOpts(options));
|
|||
|
previousResults.push(cluster);
|
|||
|
return resolved(cluster);
|
|||
|
});
|
|||
|
|
|||
|
self.directionsrenderer = chainToPromise(function (options) {
|
|||
|
var instance;
|
|||
|
if (options) {
|
|||
|
options = dupOpts(options);
|
|||
|
options.map = map;
|
|||
|
if (options.panel) {
|
|||
|
options.panel = $(options.panel).get(0);
|
|||
|
}
|
|||
|
instance = gmElement('DirectionsRenderer', options);
|
|||
|
}
|
|||
|
previousResults.push(instance);
|
|||
|
return instance;
|
|||
|
});
|
|||
|
|
|||
|
self.latlng = chainToPromise(multiple(function (options) {
|
|||
|
return resolveLatLng(options, 'latlng', function (opts) {
|
|||
|
previousResults.push(opts.latlng);
|
|||
|
return opts.latlng;
|
|||
|
});
|
|||
|
}));
|
|||
|
|
|||
|
self.fit = chainToPromise(function () {
|
|||
|
var bounds = gmElement('LatLngBounds');
|
|||
|
foreach(previousResults, function (instances) {
|
|||
|
if (instances !== map) {
|
|||
|
foreach(instances, function (instance) {
|
|||
|
if (instance) {
|
|||
|
if (instance.getPosition && instance.getPosition()) {
|
|||
|
bounds.extend(instance.getPosition());
|
|||
|
} else if (instance.getBounds && instance.getBounds()) {
|
|||
|
bounds.extend(instance.getBounds().getNorthEast());
|
|||
|
bounds.extend(instance.getBounds().getSouthWest());
|
|||
|
} else if (instance.getPaths && instance.getPaths()) {
|
|||
|
foreach(instance.getPaths().getArray(), function (path) {
|
|||
|
foreach(path.getArray(), function (latLng) {
|
|||
|
bounds.extend(latLng);
|
|||
|
});
|
|||
|
});
|
|||
|
} else if (instance.getPath && instance.getPath()) {
|
|||
|
foreach(instance.getPath().getArray(), function (latLng) {
|
|||
|
bounds.extend(latLng);
|
|||
|
});
|
|||
|
} else if (instance.getCenter && instance.getCenter()) {
|
|||
|
bounds.extend(instance.getCenter());
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
});
|
|||
|
if (!bounds.isEmpty()) {
|
|||
|
map.fitBounds(bounds);
|
|||
|
}
|
|||
|
return true;
|
|||
|
});
|
|||
|
|
|||
|
self.wait = function (duration) {
|
|||
|
promise = promise.then(function (instance) {
|
|||
|
var dfd = deferred();
|
|||
|
setTimeout(function () {
|
|||
|
dfd.resolve(instance);
|
|||
|
}, duration);
|
|||
|
return dfd;
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
self.then = function (fn) {
|
|||
|
if (isFunction(fn)) {
|
|||
|
promise = promise.then(function (instance) {
|
|||
|
return when(fn.call(context(), instance)).then(function (newInstance) {
|
|||
|
return isUndefined(newInstance) ? instance : newInstance;
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
foreachStr('on once', function (name, once) {
|
|||
|
self[name] = function () {
|
|||
|
var events = arguments[0];
|
|||
|
if (events) {
|
|||
|
if (typeof events === 'string') { // cast call on('click', handler) to on({click: handler})
|
|||
|
events = {};
|
|||
|
events[arguments[0]] = slice(arguments, 1);
|
|||
|
}
|
|||
|
promise.then(function (instances) {
|
|||
|
if (instances) {
|
|||
|
if (instances instanceof Cluster) {
|
|||
|
instances._b(function (items) {
|
|||
|
if (items && items.length) {
|
|||
|
attachEvents(events, items, once);
|
|||
|
}
|
|||
|
});
|
|||
|
return attachEvents(events, instances.markers(), [undefined, instances], once);
|
|||
|
}
|
|||
|
attachEvents(events, instances, once);
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
};
|
|||
|
});
|
|||
|
|
|||
|
self.get = function (index) {
|
|||
|
if (isUndefined(index)) {
|
|||
|
return previousResults.map(function (instance) {
|
|||
|
return isArray(instance) ? instance.slice() : instance;
|
|||
|
});
|
|||
|
} else {
|
|||
|
if (index < 0) {
|
|||
|
index = previousResults.length + index;
|
|||
|
}
|
|||
|
return isArray(previousResults[index]) ? previousResults[index].slice() : previousResults[index];
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
if (options) {
|
|||
|
self.map(options);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
})(jQuery, window, document);
|