1131 lines
No EOL
30 KiB
JavaScript
Executable file
1131 lines
No EOL
30 KiB
JavaScript
Executable file
/*!
|
||
* 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); |