├── requirements.txt ├── images ├── layers.png ├── layers-2x.png ├── marker-icon.png ├── marker-icon-2x.png ├── marker-shadow.png ├── marker-icon-blue.png ├── marker-icon-red.png ├── marker-icon-2x-blue.png ├── marker-icon-2x-red.png ├── marker-icon-black.png ├── marker-icon-green.png ├── marker-icon-orange.png ├── marker-icon-yellow.png ├── marker-icon-2x-black.png ├── marker-icon-2x-green.png ├── marker-icon-2x-orange.png └── marker-icon-2x-yellow.png ├── dist ├── images │ ├── geocoder.png │ └── throbber.gif ├── MarkerCluster.css ├── MarkerCluster.Default.css ├── leaflet.markercluster.js └── leaflet.markercluster-src.js ├── download_data.sh ├── download_stations.sh ├── README.md ├── read.py └── map.html /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml 2 | numpy 3 | -------------------------------------------------------------------------------- /images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/layers.png -------------------------------------------------------------------------------- /images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/layers-2x.png -------------------------------------------------------------------------------- /images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon.png -------------------------------------------------------------------------------- /dist/images/geocoder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/dist/images/geocoder.png -------------------------------------------------------------------------------- /dist/images/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/dist/images/throbber.gif -------------------------------------------------------------------------------- /images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-2x.png -------------------------------------------------------------------------------- /images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-shadow.png -------------------------------------------------------------------------------- /images/marker-icon-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-blue.png -------------------------------------------------------------------------------- /images/marker-icon-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-red.png -------------------------------------------------------------------------------- /images/marker-icon-2x-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-2x-blue.png -------------------------------------------------------------------------------- /images/marker-icon-2x-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-2x-red.png -------------------------------------------------------------------------------- /images/marker-icon-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-black.png -------------------------------------------------------------------------------- /images/marker-icon-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-green.png -------------------------------------------------------------------------------- /images/marker-icon-orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-orange.png -------------------------------------------------------------------------------- /images/marker-icon-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-yellow.png -------------------------------------------------------------------------------- /images/marker-icon-2x-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-2x-black.png -------------------------------------------------------------------------------- /images/marker-icon-2x-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-2x-green.png -------------------------------------------------------------------------------- /images/marker-icon-2x-orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-2x-orange.png -------------------------------------------------------------------------------- /images/marker-icon-2x-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mael-Le-Garrec/opencarbumap/HEAD/images/marker-icon-2x-yellow.png -------------------------------------------------------------------------------- /download_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | data_type=instantane 3 | 4 | wget https://donnees.roulez-eco.fr/opendata/$data_type 5 | unzip $data_type 6 | rm $data_type 7 | 8 | mv PrixCarburants* data.xml 9 | -------------------------------------------------------------------------------- /download_stations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl --output stations.json https://z.overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%3B%28nwr%5B%22ref%3AFR%3Aprix-carburants%22%5D%28area%3A3601403916%29%3B%29%3Bout%20tags%20center%3B 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opencarbumap 2 | 3 | This is a map of french gas station. The color of markers depends of prices. 4 | There is one layer per fuel. 5 | 6 | Data come from french government open data: https://www.prix-carburants.gouv.fr/rubrique/opendata/. 7 | This project uses the daily (*jour*) file. 8 | 9 | ## Example map 10 | 11 | Here is an example map: https://hatrix.fr/maps/opencarbumap/map.html 12 | 13 | ## Install 14 | 15 | ```bash 16 | ./download_data.sh 17 | python3 read.py 18 | firefox map.html 19 | ``` 20 | 21 | ## Regular data download 22 | 23 | Government's database is updated everyday at 5am. Thus, data can be downloaded 24 | safely at 5:30am each day: 25 | 26 | ``` 27 | echo "30 5 * * * hatrix cd /path/to/dir/ && sh download_data.sh && python3 read.py" >> /etc/crontab 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /dist/MarkerCluster.css: -------------------------------------------------------------------------------- 1 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { 2 | -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; 3 | -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; 4 | -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; 5 | transition: transform 0.3s ease-out, opacity 0.3s ease-in; 6 | } 7 | 8 | .leaflet-cluster-spider-leg { 9 | /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ 10 | -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in; 11 | -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in; 12 | -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in; 13 | transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; 14 | } 15 | -------------------------------------------------------------------------------- /dist/MarkerCluster.Default.css: -------------------------------------------------------------------------------- 1 | .marker-cluster-small { 2 | background-color: rgba(181, 226, 140, 0.6); 3 | } 4 | .marker-cluster-small div { 5 | background-color: rgba(110, 204, 57, 0.6); 6 | } 7 | 8 | .marker-cluster-medium { 9 | background-color: rgba(241, 211, 87, 0.6); 10 | } 11 | .marker-cluster-medium div { 12 | background-color: rgba(240, 194, 12, 0.6); 13 | } 14 | 15 | .marker-cluster-large { 16 | background-color: rgba(253, 156, 115, 0.6); 17 | } 18 | .marker-cluster-large div { 19 | background-color: rgba(241, 128, 23, 0.6); 20 | } 21 | 22 | /* IE 6-8 fallback colors */ 23 | .leaflet-oldie .marker-cluster-small { 24 | background-color: rgb(181, 226, 140); 25 | } 26 | .leaflet-oldie .marker-cluster-small div { 27 | background-color: rgb(110, 204, 57); 28 | } 29 | 30 | .leaflet-oldie .marker-cluster-medium { 31 | background-color: rgb(241, 211, 87); 32 | } 33 | .leaflet-oldie .marker-cluster-medium div { 34 | background-color: rgb(240, 194, 12); 35 | } 36 | 37 | .leaflet-oldie .marker-cluster-large { 38 | background-color: rgb(253, 156, 115); 39 | } 40 | .leaflet-oldie .marker-cluster-large div { 41 | background-color: rgb(241, 128, 23); 42 | } 43 | 44 | .marker-cluster { 45 | background-clip: padding-box; 46 | border-radius: 20px; 47 | } 48 | .marker-cluster div { 49 | width: 30px; 50 | height: 30px; 51 | margin-left: 5px; 52 | margin-top: 5px; 53 | 54 | text-align: center; 55 | border-radius: 15px; 56 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; 57 | } 58 | .marker-cluster span { 59 | line-height: 30px; 60 | } -------------------------------------------------------------------------------- /read.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import os.path 6 | from datetime import datetime, timedelta 7 | from lxml import etree 8 | import json 9 | import numpy as np 10 | import logging 11 | 12 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 13 | 14 | def get_stations(): 15 | with open('stations.json') as h: 16 | j = json.load(h) 17 | stations = dict() 18 | for h in j['elements']: 19 | id = h['tags']['ref:FR:prix-carburants'] 20 | name = h['tags'].get('name') 21 | if name is None: 22 | name = h['tags'].get('brand') 23 | if name is None: 24 | name = h['tags'].get('operator') 25 | lat = h.get('lat') 26 | lon = h.get('lon') 27 | if lat is None: 28 | lat = h['center']['lat'] 29 | if lon is None: 30 | lon = h['center']['lon'] 31 | stations[id] = {'name': name, 'lat': lat, 'lon': lon} 32 | return stations 33 | 34 | class Data(): 35 | fuel_names = sorted(['Gazole', 'SP95', 'SP98', 'GPLc', 'E10', 'E85']) 36 | 37 | class JsData(): 38 | def __init__(self): 39 | self.directory = "json" 40 | if not os.path.exists(self.directory): 41 | os.mkdir(self.directory) 42 | 43 | def write_markers(self, addressPoints): 44 | handle = open(os.path.join(self.directory, "address_points.js"), "w") 45 | handle.write("var addressPoints=") 46 | text = json.dumps(addressPoints, separators=(',', ':')) 47 | handle.write(text) 48 | handle.close() 49 | 50 | def get_coords(pdv, stations): 51 | id = get_id(pdv) 52 | if id in stations: 53 | return stations[id]['lat'], stations[id]['lon'], stations[id]['name'] 54 | try: 55 | latitude = float(pdv.get('latitude')) 56 | longitude = float(pdv.get('longitude')) 57 | 58 | # sometimes latitude and longitude are switch 59 | if latitude < longitude: 60 | latitude, longitude = longitude, latitude 61 | 62 | # sometimes coordinate are already in WGS84 63 | if latitude > 100 or latitude < -100: 64 | latitude = latitude / 100000 65 | longitude = longitude / 100000 66 | except: 67 | latitude, longitude = '', '' 68 | 69 | return latitude, longitude, None 70 | 71 | 72 | def get_id(pdv): 73 | return pdv.get('id') 74 | 75 | 76 | def check_price_update(prices, node, delta={'days':14}): 77 | time_delta = timedelta(**delta) 78 | now = datetime.now() 79 | pdv_date = datetime.strptime(node.get('maj'), "%Y-%m-%d %H:%M:%S") 80 | 81 | # If the last update was before , return a warning 82 | prices[node.get('nom')] = float(node.get('valeur')) 83 | 84 | if pdv_date + time_delta < now: 85 | d = node.get('maj').split(' ')[0] 86 | return d 87 | return None 88 | 89 | 90 | def get_children(pdv): 91 | prices = {} 92 | sold_out = [] 93 | city = '' 94 | remark = None 95 | 96 | for child in pdv.getchildren(): 97 | # Check that the last update wasn't too long ago 98 | if child.tag == "prix": 99 | remark = check_price_update(prices, child) 100 | 101 | if child.tag == "rupture": 102 | sold_out.append(child.get('nom')) 103 | 104 | if child.tag == "ville": 105 | city = child.text.title() if child.text else None # Sometimes ville is empty 106 | 107 | return prices, city, sold_out, remark 108 | 109 | 110 | def parse_xml(filename): 111 | output = JsData() 112 | 113 | tree = etree.parse(filename) 114 | addressPoints = [] 115 | price_list = [] 116 | 117 | stations = get_stations() 118 | 119 | for i, pdv in enumerate(tree.xpath('/pdv_liste/pdv')): 120 | addressPoint = [] 121 | pdv_id = get_id(pdv) 122 | latitude, longitude, brand = get_coords(pdv, stations) 123 | fuels, city, sold_out, remark = get_children(pdv) 124 | 125 | if latitude and longitude and fuels: 126 | addressPoint.append(latitude) 127 | addressPoint.append(longitude) 128 | addressPoint.append(city) 129 | addressPoint.append(fuels) 130 | addressPoint.append(brand) 131 | addressPoint.append(remark) 132 | 133 | addressPoints.append(addressPoint) 134 | price_list.append(fuels) 135 | 136 | reject_outliers_prices(addressPoints) 137 | output.write_markers(addressPoints) 138 | 139 | 140 | def reject_outliers_prices(addressPoints, reject_factor=10): 141 | """ Filter outliers price from addressPoints using standard 142 | deviation and mean. 143 | 144 | reject condition: abs(price - mean) > reject_factor * std 145 | 146 | :param addressPoints: address point list 147 | :param reject_factor: used in reject condition 148 | """ 149 | # extract all fuel price by fuel Id in a dict like 150 | # {'E10': [1.604, 1.602, ...], ...} 151 | fuelPrices = {} 152 | for address in addressPoints: 153 | for fuelId, price in iter(address[3].items()): 154 | if not fuelId in fuelPrices: 155 | fuelPrices[fuelId] = [price] # first entry 156 | else: 157 | fuelPrices[fuelId].append(price) 158 | 159 | # compute mean and standard deviation for all fuel Id 160 | fuelMeanStd = {} 161 | for fuelId, prices in iter(fuelPrices.items()): 162 | fuelMeanStd[fuelId] = { 163 | 'mean': np.mean(prices), 164 | 'std': np.std(prices) 165 | } 166 | 167 | # now we can filter previous result 168 | for address in addressPoints: 169 | for fuelId in list(address[3]): 170 | if abs(address[3][fuelId] - fuelMeanStd[fuelId]['mean']) > reject_factor * fuelMeanStd[fuelId]['std']: 171 | logging.info('rejecting fuel price: %s=%f (mean=%f, std=%f)', fuelId, 172 | address[3][fuelId], fuelMeanStd[fuelId]['mean'], fuelMeanStd[fuelId]['std']) 173 | # remove the entry 174 | del address[3][fuelId] 175 | 176 | if __name__ == "__main__": 177 | parse_xml("data.xml") 178 | -------------------------------------------------------------------------------- /map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Carburants 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 31 | 32 | 112 | 113 | 114 | 115 | 116 | 117 | Fork me on GitHub 118 |
119 | 120 | 271 | 272 | 273 | -------------------------------------------------------------------------------- /dist/leaflet.markercluster.js: -------------------------------------------------------------------------------- 1 | /* 2 | Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. 3 | https://github.com/Leaflet/Leaflet.markercluster 4 | (c) 2012-2013, Dave Leaver, smartrak 5 | */ 6 | !function(e,t,i){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animate:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,spiderLegPolylineOptions:{weight:1.5,color:"#222",opacity:.5},chunkedLoading:!1,chunkInterval:200,chunkDelay:50,chunkProgress:null,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.addEventParent(this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.addEventParent(this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[],this._childMarkerEventHandlers={dragstart:this._childMarkerDragStart,move:this._childMarkerMoved,dragend:this._childMarkerDragEnd};var t=L.DomUtil.TRANSITION&&this.options.animate;L.extend(this,t?this._withAnimation:this._noAnimation),this._markerCluster=t?L.MarkerCluster:L.MarkerClusterNonAnimated},addLayer:function(e){if(e instanceof L.LayerGroup)return this.addLayers([e]);if(!e.getLatLng)return this._nonPointGroup.addLayer(e),this.fire("layeradd",{layer:e}),this;if(!this._map)return this._needsClustering.push(e),this.fire("layeradd",{layer:e}),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom),this.fire("layeradd",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons();var t=e,i=this._zoom;if(e.__parent)for(;t.__parent._zoom>=i;)t=t.__parent;return this._currentShownBounds.contains(t.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,t):this._animationAddLayerNonAnimated(e,t)),this},removeLayer:function(e){return e instanceof L.LayerGroup?this.removeLayers([e]):e.getLatLng?this._map?e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),this.fire("layerremove",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),e.off(this._childMarkerEventHandlers,this),this._featureGroup.hasLayer(e)&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow()),this):this:(!this._arraySplice(this._needsClustering,e)&&this.hasLayer(e)&&this._needsRemoving.push({layer:e,latlng:e._latlng}),this.fire("layerremove",{layer:e}),this):(this._nonPointGroup.removeLayer(e),this.fire("layerremove",{layer:e}),this)},addLayers:function(e,t){if(!L.Util.isArray(e))return this.addLayer(e);var i,n=this._featureGroup,r=this._nonPointGroup,s=this.options.chunkedLoading,o=this.options.chunkInterval,a=this.options.chunkProgress,h=e.length,l=0,_=!0;if(this._map){var u=(new Date).getTime(),d=L.bind(function(){for(var c=(new Date).getTime();h>l;l++){if(s&&0===l%200){var p=(new Date).getTime()-c;if(p>o)break}if(i=e[l],i instanceof L.LayerGroup)_&&(e=e.slice(),_=!1),this._extractNonGroupLayers(i,e),h=e.length;else if(i.getLatLng){if(!this.hasLayer(i)&&(this._addLayer(i,this._maxZoom),t||this.fire("layeradd",{layer:i}),i.__parent&&2===i.__parent.getChildCount())){var f=i.__parent.getAllChildMarkers(),m=f[0]===i?f[1]:f[0];n.removeLayer(m)}}else r.addLayer(i),t||this.fire("layeradd",{layer:i})}a&&a(l,h,(new Date).getTime()-u),l===h?(this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)):setTimeout(d,this.options.chunkDelay)},this);d()}else for(var c=this._needsClustering;h>l;l++)i=e[l],i instanceof L.LayerGroup?(_&&(e=e.slice(),_=!1),this._extractNonGroupLayers(i,e),h=e.length):i.getLatLng?this.hasLayer(i)||c.push(i):r.addLayer(i);return this},removeLayers:function(e){var t,i,n=e.length,r=this._featureGroup,s=this._nonPointGroup,o=!0;if(!this._map){for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):(this._arraySplice(this._needsClustering,i),s.removeLayer(i),this.hasLayer(i)&&this._needsRemoving.push({layer:i,latlng:i._latlng}),this.fire("layerremove",{layer:i}));return this}if(this._unspiderfy){this._unspiderfy();var a=e.slice(),h=n;for(t=0;h>t;t++)i=a[t],i instanceof L.LayerGroup?(this._extractNonGroupLayers(i,a),h=a.length):this._unspiderfyLayer(i)}for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):i.__parent?(this._removeLayer(i,!0,!0),this.fire("layerremove",{layer:i}),r.hasLayer(i)&&(r.removeLayer(i),i.clusterShow&&i.clusterShow())):(s.removeLayer(i),this.fire("layerremove",{layer:i}));return this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),this},clearLayers:function(){return this._map||(this._needsClustering=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(e){e.off(this._childMarkerEventHandlers,this),delete e.__parent},this),this._map&&this._generateInitialClusters(),this},getBounds:function(){var e=new L.LatLngBounds;this._topClusterLevel&&e.extend(this._topClusterLevel._bounds);for(var t=this._needsClustering.length-1;t>=0;t--)e.extend(this._needsClustering[t].getLatLng());return e.extend(this._nonPointGroup.getBounds()),e},eachLayer:function(e,t){var i,n,r,s=this._needsClustering.slice(),o=this._needsRemoving;for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(s),n=s.length-1;n>=0;n--){for(i=!0,r=o.length-1;r>=0;r--)if(o[r].layer===s[n]){i=!1;break}i&&e.call(t,s[n])}this._nonPointGroup.eachLayer(e,t)},getLayers:function(){var e=[];return this.eachLayer(function(t){e.push(t)}),e},getLayer:function(e){var t=null;return e=parseInt(e,10),this.eachLayer(function(i){L.stamp(i)===e&&(t=i)}),t},hasLayer:function(e){if(!e)return!1;var t,i=this._needsClustering;for(t=i.length-1;t>=0;t--)if(i[t]===e)return!0;for(i=this._needsRemoving,t=i.length-1;t>=0;t--)if(i[t].layer===e)return!1;return!(!e.__parent||e.__parent._group!==this)||this._nonPointGroup.hasLayer(e)},zoomToShowLayer:function(e,t){"function"!=typeof t&&(t=function(){});var i=function(){!e._icon&&!e.__parent._icon||this._inZoomAnimation||(this._map.off("moveend",i,this),this.off("animationend",i,this),e._icon?t():e.__parent._icon&&(this.once("spiderfied",t,this),e.__parent.spiderfy()))};e._icon&&this._map.getBounds().contains(e.getLatLng())?t():e.__parent._zoomt;t++)n=this._needsRemoving[t],n.newlatlng=n.layer._latlng,n.layer._latlng=n.latlng;for(t=0,i=this._needsRemoving.length;i>t;t++)n=this._needsRemoving[t],this._removeLayer(n.layer,!0),n.layer._latlng=n.newlatlng;this._needsRemoving=[],this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds(),this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),i=this._needsClustering,this._needsClustering=[],this.addLayers(i,!0)},onRemove:function(e){e.off("zoomend",this._zoomEnd,this),e.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),delete this._maxLat,this._hideCoverage(),this._featureGroup.remove(),this._nonPointGroup.remove(),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(e){for(var t=e;t&&!t._icon;)t=t.__parent;return t||null},_arraySplice:function(e,t){for(var i=e.length-1;i>=0;i--)if(e[i]===t)return e.splice(i,1),!0},_removeFromGridUnclustered:function(e,t){for(var i=this._map,n=this._gridUnclustered,r=this._map.getMinZoom();t>=r&&n[t].removeObject(e,i.project(e.getLatLng(),t));t--);},_childMarkerDragStart:function(e){e.target.__dragStart=e.target._latlng},_childMarkerMoved:function(e){if(!this._ignoreMove&&!e.target.__dragStart){var t=e.target._popup&&e.target._popup.isOpen();this._moveChild(e.target,e.oldLatLng,e.latlng),t&&e.target.openPopup()}},_moveChild:function(e,t,i){e._latlng=t,this.removeLayer(e),e._latlng=i,this.addLayer(e)},_childMarkerDragEnd:function(e){e.target.__dragStart&&this._moveChild(e.target,e.target.__dragStart,e.target._latlng),delete e.target.__dragStart},_removeLayer:function(e,t,i){var n=this._gridClusters,r=this._gridUnclustered,s=this._featureGroup,o=this._map,a=this._map.getMinZoom();t&&this._removeFromGridUnclustered(e,this._maxZoom);var h,l=e.__parent,_=l._markers;for(this._arraySplice(_,e);l&&(l._childCount--,l._boundsNeedUpdate=!0,!(l._zoomt?"small":100>t?"medium":"large",new L.DivIcon({html:"
"+t+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=this._map,t=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(t||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),e.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(e){for(var t=e.layer,i=t;1===i._childClusters.length;)i=i._childClusters[0];i._zoom===this._maxZoom&&i._childCount===t._childCount&&this.options.spiderfyOnMaxZoom?t.spiderfy():this.options.zoomToBoundsOnClick&&t.zoomToBounds(),e.originalEvent&&13===e.originalEvent.keyCode&&this._map._container.focus()},_showCoverage:function(e){var t=this._map;this._inZoomAnimation||(this._shownPolygon&&t.removeLayer(this._shownPolygon),e.layer.getChildCount()>2&&e.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(e.layer.getConvexHull(),this.options.polygonOptions),t.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var e=this.options.spiderfyOnMaxZoom,t=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(e||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),t&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var e=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._map.getMinZoom(),this._zoom,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,Math.round(this._map._zoom),e),this._currentShownBounds=e}},_generateInitialClusters:function(){var e=this._map.getMaxZoom(),t=this._map.getMinZoom(),i=this.options.maxClusterRadius,n=i;"function"!=typeof i&&(n=function(){return i}),null!==this.options.disableClusteringAtZoom&&(e=this.options.disableClusteringAtZoom-1),this._maxZoom=e,this._gridClusters={},this._gridUnclustered={};for(var r=e;r>=t;r--)this._gridClusters[r]=new L.DistanceGrid(n(r)),this._gridUnclustered[r]=new L.DistanceGrid(n(r));this._topClusterLevel=new this._markerCluster(this,t-1)},_addLayer:function(e,t){var i,n,r=this._gridClusters,s=this._gridUnclustered,o=this._map.getMinZoom();for(this.options.singleMarkerMode&&this._overrideMarkerIcon(e),e.on(this._childMarkerEventHandlers,this);t>=o;t--){i=this._map.project(e.getLatLng(),t);var a=r[t].getNearObject(i);if(a)return a._addChild(e),e.__parent=a,void 0;if(a=s[t].getNearObject(i)){var h=a.__parent;h&&this._removeLayer(a,!1);var l=new this._markerCluster(this,t,a,e);r[t].addObject(l,this._map.project(l._cLatLng,t)),a.__parent=l,e.__parent=l;var _=l;for(n=t-1;n>h._zoom;n--)_=new this._markerCluster(this,n,_),r[n].addObject(_,this._map.project(a.getLatLng(),n));return h._addChild(_),this._removeFromGridUnclustered(a,t),void 0}s[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(e){e instanceof L.MarkerCluster&&e._iconNeedsUpdate&&e._updateIcon()})},_enqueue:function(e){this._queue.push(e),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var e=0;ee?(this._animationStart(),this._animationZoomOut(this._zoom,e)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(e){var t=this._maxLat;return t!==i&&(e.getNorth()>=t&&(e._northEast.lat=1/0),e.getSouth()<=-t&&(e._southWest.lat=-1/0)),e},_animationAddLayerNonAnimated:function(e,t){if(t===e)this._featureGroup.addLayer(e);else if(2===t._childCount){t._addToMap();var i=t.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else t._updateIcon()},_extractNonGroupLayers:function(e,t){var i,n=e.getLayers(),r=0;for(t=t||[];r=0;i--)o=h[i],n.contains(o._latlng)||r.removeLayer(o)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(n,t),r.eachLayer(function(e){e instanceof L.MarkerCluster||!e._icon||e.clusterShow()}),this._topClusterLevel._recursively(n,e,t,function(e){e._recursivelyRestoreChildPositions(t)}),this._ignoreMove=!1,this._enqueue(function(){this._topClusterLevel._recursively(n,e,s,function(e){r.removeLayer(e),e.clusterShow()}),this._animationEnd()})},_animationZoomOut:function(e,t){this._animationZoomOutSingle(this._topClusterLevel,e-1,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._map.getMinZoom(),e,this._getExpandedVisibleBounds())},_animationAddLayer:function(e,t){var i=this,n=this._featureGroup;n.addLayer(e),t!==e&&(t._childCount>2?(t._updateIcon(),this._forceLayout(),this._animationStart(),e._setPos(this._map.latLngToLayerPoint(t.getLatLng())),e.clusterHide(),this._enqueue(function(){n.removeLayer(e),e.clusterShow(),i._animationEnd()})):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(t,this._map.getMaxZoom(),this._zoom)))}},_animationZoomOutSingle:function(e,t,i){var n=this._getExpandedVisibleBounds(),r=this._map.getMinZoom();e._recursivelyAnimateChildrenInAndAddSelfToMap(n,r,t+1,i);var s=this;this._forceLayout(),e._recursivelyBecomeVisible(n,i),this._enqueue(function(){if(1===e._childCount){var o=e._markers[0];this._ignoreMove=!0,o.setLatLng(o.getLatLng()),this._ignoreMove=!1,o.clusterShow&&o.clusterShow()}else e._recursively(n,i,r,function(e){e._recursivelyRemoveChildrenFromMap(n,r,t+1)});s._animationEnd()})},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_forceLayout:function(){L.Util.falseFn(t.body.offsetWidth)}}),L.markerClusterGroup=function(e){return new L.MarkerClusterGroup(e)},L.MarkerCluster=L.Marker.extend({initialize:function(e,t,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=e,this._zoom=t,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(e){e=e||[];for(var t=this._childClusters.length-1;t>=0;t--)this._childClusters[t].getAllChildMarkers(e);for(var i=this._markers.length-1;i>=0;i--)e.push(this._markers[i]);return e},getChildCount:function(){return this._childCount},zoomToBounds:function(e){for(var t,i=this._childClusters.slice(),n=this._group._map,r=n.getBoundsZoom(this._bounds),s=this._zoom+1,o=n.getZoom();i.length>0&&r>s;){s++;var a=[];for(t=0;ts?this._group._map.setView(this._latlng,s):o>=r?this._group._map.setView(this._latlng,o+1):this._group._map.fitBounds(this._bounds,e)},getBounds:function(){var e=new L.LatLngBounds;return e.extend(this._bounds),e},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(e,t){this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._setClusterCenter(e),e instanceof L.MarkerCluster?(t||(this._childClusters.push(e),e.__parent=this),this._childCount+=e._childCount):(t||this._markers.push(e),this._childCount++),this.__parent&&this.__parent._addChild(e,!0)},_setClusterCenter:function(e){this._cLatLng||(this._cLatLng=e._cLatLng||e._latlng)},_resetBounds:function(){var e=this._bounds;e._southWest&&(e._southWest.lat=1/0,e._southWest.lng=1/0),e._northEast&&(e._northEast.lat=-1/0,e._northEast.lng=-1/0)},_recalculateBounds:function(){var e,t,i,n,r=this._markers,s=this._childClusters,o=0,a=0,h=this._childCount;if(0!==h){for(this._resetBounds(),e=0;e=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())},function(e){var i,n,r=e._childClusters;for(i=r.length-1;i>=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(e,t,i,n){this._recursively(e,n,t,function(r){r._recursivelyAnimateChildrenIn(e,r._group._map.latLngToLayerPoint(r.getLatLng()).round(),i),r._isSingleParent()&&i-1===n?(r.clusterShow(),r._recursivelyRemoveChildrenFromMap(e,t,i)):r.clusterHide(),r._addToMap()})},_recursivelyBecomeVisible:function(e,t){this._recursively(e,this._group._map.getMinZoom(),t,null,function(e){e.clusterShow()})},_recursivelyAddChildrenToMap:function(e,t,i){this._recursively(i,this._group._map.getMinZoom()-1,t,function(n){if(t!==n._zoom)for(var r=n._markers.length-1;r>=0;r--){var s=n._markers[r];i.contains(s._latlng)&&(e&&(s._backupLatlng=s.getLatLng(),s.setLatLng(e),s.clusterHide&&s.clusterHide()),n._group._featureGroup.addLayer(s))}},function(t){t._addToMap(e)})},_recursivelyRestoreChildPositions:function(e){for(var t=this._markers.length-1;t>=0;t--){var i=this._markers[t];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(e-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var r=this._childClusters.length-1;r>=0;r--)this._childClusters[r]._recursivelyRestoreChildPositions(e)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(e,t,i,n){var r,s;this._recursively(e,t-1,i-1,function(e){for(s=e._markers.length-1;s>=0;s--)r=e._markers[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())},function(e){for(s=e._childClusters.length-1;s>=0;s--)r=e._childClusters[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())})},_recursively:function(e,t,i,n,r){var s,o,a=this._childClusters,h=this._zoom;if(h>=t&&(n&&n(this),r&&h===i&&r(this)),t>h||i>h)for(s=a.length-1;s>=0;s--)o=a[s],e.intersects(o._bounds)&&o._recursively(e,t,i,n,r)},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.Marker.include({clusterHide:function(){return this.options.opacityWhenUnclustered=this.options.opacity||1,this.setOpacity(0)},clusterShow:function(){var e=this.setOpacity(this.options.opacity||this.options.opacityWhenUnclustered);return delete this.options.opacityWhenUnclustered,e}}),L.DistanceGrid=function(e){this._cellSize=e,this._sqCellSize=e*e,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(e,t){var i=this._getCoord(t.x),n=this._getCoord(t.y),r=this._grid,s=r[n]=r[n]||{},o=s[i]=s[i]||[],a=L.Util.stamp(e);this._objectPoint[a]=t,o.push(e)},updateObject:function(e,t){this.removeObject(e),this.addObject(e,t)},removeObject:function(e,t){var i,n,r=this._getCoord(t.x),s=this._getCoord(t.y),o=this._grid,a=o[s]=o[s]||{},h=a[r]=a[r]||[];for(delete this._objectPoint[L.Util.stamp(e)],i=0,n=h.length;n>i;i++)if(h[i]===e)return h.splice(i,1),1===n&&delete a[r],!0},eachObject:function(e,t){var i,n,r,s,o,a,h,l=this._grid;for(i in l){o=l[i];for(n in o)for(a=o[n],r=0,s=a.length;s>r;r++)h=e.call(t,a[r]),h&&(r--,s--)}},getNearObject:function(e){var t,i,n,r,s,o,a,h,l=this._getCoord(e.x),_=this._getCoord(e.y),u=this._objectPoint,d=this._sqCellSize,c=null;for(t=_-1;_+1>=t;t++)if(r=this._grid[t])for(i=l-1;l+1>=i;i++)if(s=r[i])for(n=0,o=s.length;o>n;n++)a=s[n],h=this._sqDist(u[L.Util.stamp(a)],e),d>h&&(d=h,c=a);return c},_getCoord:function(e){return Math.floor(e/this._cellSize)},_sqDist:function(e,t){var i=t.x-e.x,n=t.y-e.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(e,t){var i=t[1].lat-t[0].lat,n=t[0].lng-t[1].lng;return n*(e.lat-t[0].lat)+i*(e.lng-t[0].lng)},findMostDistantPointFromBaseLine:function(e,t){var i,n,r,s=0,o=null,a=[];for(i=t.length-1;i>=0;i--)n=t[i],r=this.getDistant(n,e),r>0&&(a.push(n),r>s&&(s=r,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(e,t){var i=[],n=this.findMostDistantPointFromBaseLine(e,t);return n.maxPoint?(i=i.concat(this.buildConvexHull([e[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,e[1]],n.newPoints))):[e[0]]},getConvexHull:function(e){var t,i=!1,n=!1,r=!1,s=!1,o=null,a=null,h=null,l=null,_=null,u=null;for(t=e.length-1;t>=0;t--){var d=e[t];(i===!1||d.lat>i)&&(o=d,i=d.lat),(n===!1||d.latr)&&(h=d,r=d.lng),(s===!1||d.lng=0;t--)e=i[t].getLatLng(),n.push(e);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var e,t=this.getAllChildMarkers(),i=this._group,n=i._map,r=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,t.length>=this._circleSpiralSwitchover?e=this._generatePointsSpiral(t.length,r):(r.y+=10,e=this._generatePointsCircle(t.length,r)),this._animationSpiderfy(t,e)}},unspiderfy:function(e){this._group._inZoomAnimation||(this._animationUnspiderfy(e),this._group._spiderfied=null)},_generatePointsCircle:function(e,t){var i,n,r=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e),s=r/this._2PI,o=this._2PI/e,a=[];for(a.length=e,i=e-1;i>=0;i--)n=this._circleStartAngle+i*o,a[i]=new L.Point(t.x+s*Math.cos(n),t.y+s*Math.sin(n))._round();return a},_generatePointsSpiral:function(e,t){var i,n=this._group.options.spiderfyDistanceMultiplier,r=n*this._spiralLengthStart,s=n*this._spiralFootSeparation,o=n*this._spiralLengthFactor*this._2PI,a=0,h=[];for(h.length=e,i=e-1;i>=0;i--)a+=s/r+5e-4*i,h[i]=new L.Point(t.x+r*Math.cos(a),t.y+r*Math.sin(a))._round(),r+=o/a;return h},_noanimationUnspiderfy:function(){var e,t,i=this._group,n=i._map,r=i._featureGroup,s=this.getAllChildMarkers();for(i._ignoreMove=!0,this.setOpacity(1),t=s.length-1;t>=0;t--)e=s[t],r.removeLayer(e),e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng),e.setZIndexOffset&&e.setZIndexOffset(0),e._spiderLeg&&(n.removeLayer(e._spiderLeg),delete e._spiderLeg);i.fire("unspiderfied",{cluster:this,markers:s}),i._ignoreMove=!1,i._spiderfied=null}}),L.MarkerClusterNonAnimated=L.MarkerCluster.extend({_animationSpiderfy:function(e,t){var i,n,r,s,o=this._group,a=o._map,h=o._featureGroup,l=this._group.options.spiderLegPolylineOptions;for(o._ignoreMove=!0,i=0;i=0;n--)h=u.layerPointToLatLng(t[n]),r=e[n],r._preSpiderfyLatlng=r._latlng,r.setLatLng(h),r.clusterShow&&r.clusterShow(),f&&(s=r._spiderLeg,o=s._path,o.style.strokeDashoffset=0,s.setStyle({opacity:g}));this.setOpacity(.3),_._ignoreMove=!1,setTimeout(function(){_._animationEnd(),_.fire("spiderfied",{cluster:l,markers:e})},200)},_animationUnspiderfy:function(e){var t,i,n,r,s,o,a=this,h=this._group,l=h._map,_=h._featureGroup,u=e?l._latLngToNewLayerPoint(this._latlng,e.zoom,e.center):l.latLngToLayerPoint(this._latlng),d=this.getAllChildMarkers(),c=L.Path.SVG;for(h._ignoreMove=!0,h._animationStart(),this.setOpacity(1),i=d.length-1;i>=0;i--)t=d[i],t._preSpiderfyLatlng&&(t.closePopup(),t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng,o=!0,t._setPos&&(t._setPos(u),o=!1),t.clusterHide&&(t.clusterHide(),o=!1),o&&_.removeLayer(t),c&&(n=t._spiderLeg,r=n._path,s=r.getTotalLength()+.1,r.style.strokeDashoffset=s,n.setStyle({opacity:0})));h._ignoreMove=!1,setTimeout(function(){var e=0;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&e++;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&(t.clusterShow&&t.clusterShow(),t.setZIndexOffset&&t.setZIndexOffset(0),e>1&&_.removeLayer(t),l.removeLayer(t._spiderLeg),delete t._spiderLeg);h._animationEnd(),h.fire("unspiderfied",{cluster:a,markers:d})},200)}}),L.MarkerClusterGroup.include({_spiderfied:null,unspiderfy:function(){this._unspiderfy.apply(this,arguments)},_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Browser.touch||this._map.getRenderer(this)},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._map.off("zoomend",this._noanimationUnspiderfy,this),this._noanimationUnspiderfy()},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(e){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(e))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(e){this._spiderfied&&this._spiderfied.unspiderfy(e)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(e){e._spiderLeg&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow(),e.setZIndexOffset&&e.setZIndexOffset(0),this._map.removeLayer(e._spiderLeg),delete e._spiderLeg) 7 | }}),L.MarkerClusterGroup.include({refreshClusters:function(e){return e?e instanceof L.MarkerClusterGroup?e=e._topClusterLevel.getAllChildMarkers():e instanceof L.LayerGroup?e=e._layers:e instanceof L.MarkerCluster?e=e.getAllChildMarkers():e instanceof L.Marker&&(e=[e]):e=this._topClusterLevel.getAllChildMarkers(),this._flagParentsIconsNeedUpdate(e),this._refreshClustersIcons(),this.options.singleMarkerMode&&this._refreshSingleMarkerModeMarkers(e),this},_flagParentsIconsNeedUpdate:function(e){var t,i;for(t in e)for(i=e[t].__parent;i;)i._iconNeedsUpdate=!0,i=i.__parent},_refreshSingleMarkerModeMarkers:function(e){var t,i;for(t in e)i=e[t],this.hasLayer(i)&&i.setIcon(this._overrideMarkerIcon(i))}}),L.Marker.include({refreshIconOptions:function(e,t){var i=this.options.icon;return L.setOptions(i,e),this.setIcon(i),t&&this.__parent&&this.__parent._group.refreshClusters(this),this}})}(window,document); -------------------------------------------------------------------------------- /dist/leaflet.markercluster-src.js: -------------------------------------------------------------------------------- 1 | /* 2 | Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. 3 | https://github.com/Leaflet/Leaflet.markercluster 4 | (c) 2012-2013, Dave Leaver, smartrak 5 | */ 6 | (function (window, document, undefined) {/* 7 | * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within 8 | */ 9 | 10 | L.MarkerClusterGroup = L.FeatureGroup.extend({ 11 | 12 | options: { 13 | maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center 14 | iconCreateFunction: null, 15 | 16 | spiderfyOnMaxZoom: true, 17 | showCoverageOnHover: true, 18 | zoomToBoundsOnClick: true, 19 | singleMarkerMode: false, 20 | 21 | disableClusteringAtZoom: null, 22 | 23 | // Setting this to false prevents the removal of any clusters outside of the viewpoint, which 24 | // is the default behaviour for performance reasons. 25 | removeOutsideVisibleBounds: true, 26 | 27 | // Set to false to disable all animations (zoom and spiderfy). 28 | // If false, option animateAddingMarkers below has no effect. 29 | // If L.DomUtil.TRANSITION is falsy, this option has no effect. 30 | animate: true, 31 | 32 | //Whether to animate adding markers after adding the MarkerClusterGroup to the map 33 | // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains. 34 | animateAddingMarkers: false, 35 | 36 | //Increase to increase the distance away that spiderfied markers appear from the center 37 | spiderfyDistanceMultiplier: 1, 38 | 39 | // Make it possible to specify a polyline options on a spider leg 40 | spiderLegPolylineOptions: { weight: 1.5, color: '#222', opacity: 0.5 }, 41 | 42 | // When bulk adding layers, adds markers in chunks. Means addLayers may not add all the layers in the call, others will be loaded during setTimeouts 43 | chunkedLoading: false, 44 | chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback) 45 | chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser 46 | chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator) 47 | 48 | //Options to pass to the L.Polygon constructor 49 | polygonOptions: {} 50 | }, 51 | 52 | initialize: function (options) { 53 | L.Util.setOptions(this, options); 54 | if (!this.options.iconCreateFunction) { 55 | this.options.iconCreateFunction = this._defaultIconCreateFunction; 56 | } 57 | 58 | this._featureGroup = L.featureGroup(); 59 | this._featureGroup.addEventParent(this); 60 | 61 | this._nonPointGroup = L.featureGroup(); 62 | this._nonPointGroup.addEventParent(this); 63 | 64 | this._inZoomAnimation = 0; 65 | this._needsClustering = []; 66 | this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of 67 | //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move 68 | this._currentShownBounds = null; 69 | 70 | this._queue = []; 71 | 72 | this._childMarkerEventHandlers = { 73 | 'dragstart': this._childMarkerDragStart, 74 | 'move': this._childMarkerMoved, 75 | 'dragend': this._childMarkerDragEnd, 76 | }; 77 | 78 | // Hook the appropriate animation methods. 79 | var animate = L.DomUtil.TRANSITION && this.options.animate; 80 | L.extend(this, animate ? this._withAnimation : this._noAnimation); 81 | // Remember which MarkerCluster class to instantiate (animated or not). 82 | this._markerCluster = animate ? L.MarkerCluster : L.MarkerClusterNonAnimated; 83 | }, 84 | 85 | addLayer: function (layer) { 86 | 87 | if (layer instanceof L.LayerGroup) { 88 | return this.addLayers([layer]); 89 | } 90 | 91 | //Don't cluster non point data 92 | if (!layer.getLatLng) { 93 | this._nonPointGroup.addLayer(layer); 94 | this.fire('layeradd', { layer: layer }); 95 | return this; 96 | } 97 | 98 | if (!this._map) { 99 | this._needsClustering.push(layer); 100 | this.fire('layeradd', { layer: layer }); 101 | return this; 102 | } 103 | 104 | if (this.hasLayer(layer)) { 105 | return this; 106 | } 107 | 108 | 109 | //If we have already clustered we'll need to add this one to a cluster 110 | 111 | if (this._unspiderfy) { 112 | this._unspiderfy(); 113 | } 114 | 115 | this._addLayer(layer, this._maxZoom); 116 | this.fire('layeradd', { layer: layer }); 117 | 118 | // Refresh bounds and weighted positions. 119 | this._topClusterLevel._recalculateBounds(); 120 | 121 | this._refreshClustersIcons(); 122 | 123 | //Work out what is visible 124 | var visibleLayer = layer, 125 | currentZoom = this._zoom; 126 | if (layer.__parent) { 127 | while (visibleLayer.__parent._zoom >= currentZoom) { 128 | visibleLayer = visibleLayer.__parent; 129 | } 130 | } 131 | 132 | if (this._currentShownBounds.contains(visibleLayer.getLatLng())) { 133 | if (this.options.animateAddingMarkers) { 134 | this._animationAddLayer(layer, visibleLayer); 135 | } else { 136 | this._animationAddLayerNonAnimated(layer, visibleLayer); 137 | } 138 | } 139 | return this; 140 | }, 141 | 142 | removeLayer: function (layer) { 143 | 144 | if (layer instanceof L.LayerGroup) { 145 | return this.removeLayers([layer]); 146 | } 147 | 148 | //Non point layers 149 | if (!layer.getLatLng) { 150 | this._nonPointGroup.removeLayer(layer); 151 | this.fire('layerremove', { layer: layer }); 152 | return this; 153 | } 154 | 155 | if (!this._map) { 156 | if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) { 157 | this._needsRemoving.push({ layer: layer, latlng: layer._latlng }); 158 | } 159 | this.fire('layerremove', { layer: layer }); 160 | return this; 161 | } 162 | 163 | if (!layer.__parent) { 164 | return this; 165 | } 166 | 167 | if (this._unspiderfy) { 168 | this._unspiderfy(); 169 | this._unspiderfyLayer(layer); 170 | } 171 | 172 | //Remove the marker from clusters 173 | this._removeLayer(layer, true); 174 | this.fire('layerremove', { layer: layer }); 175 | 176 | // Refresh bounds and weighted positions. 177 | this._topClusterLevel._recalculateBounds(); 178 | 179 | this._refreshClustersIcons(); 180 | 181 | layer.off(this._childMarkerEventHandlers, this); 182 | 183 | if (this._featureGroup.hasLayer(layer)) { 184 | this._featureGroup.removeLayer(layer); 185 | if (layer.clusterShow) { 186 | layer.clusterShow(); 187 | } 188 | } 189 | 190 | return this; 191 | }, 192 | 193 | //Takes an array of markers and adds them in bulk 194 | addLayers: function (layersArray, skipLayerAddEvent) { 195 | if (!L.Util.isArray(layersArray)) { 196 | return this.addLayer(layersArray); 197 | } 198 | 199 | var fg = this._featureGroup, 200 | npg = this._nonPointGroup, 201 | chunked = this.options.chunkedLoading, 202 | chunkInterval = this.options.chunkInterval, 203 | chunkProgress = this.options.chunkProgress, 204 | l = layersArray.length, 205 | offset = 0, 206 | originalArray = true, 207 | m; 208 | 209 | if (this._map) { 210 | var started = (new Date()).getTime(); 211 | var process = L.bind(function () { 212 | var start = (new Date()).getTime(); 213 | for (; offset < l; offset++) { 214 | if (chunked && offset % 200 === 0) { 215 | // every couple hundred markers, instrument the time elapsed since processing started: 216 | var elapsed = (new Date()).getTime() - start; 217 | if (elapsed > chunkInterval) { 218 | break; // been working too hard, time to take a break :-) 219 | } 220 | } 221 | 222 | m = layersArray[offset]; 223 | 224 | // Group of layers, append children to layersArray and skip. 225 | // Side effects: 226 | // - Total increases, so chunkProgress ratio jumps backward. 227 | // - Groups are not included in this group, only their non-group child layers (hasLayer). 228 | // Changing array length while looping does not affect performance in current browsers: 229 | // http://jsperf.com/for-loop-changing-length/6 230 | if (m instanceof L.LayerGroup) { 231 | if (originalArray) { 232 | layersArray = layersArray.slice(); 233 | originalArray = false; 234 | } 235 | this._extractNonGroupLayers(m, layersArray); 236 | l = layersArray.length; 237 | continue; 238 | } 239 | 240 | //Not point data, can't be clustered 241 | if (!m.getLatLng) { 242 | npg.addLayer(m); 243 | if (!skipLayerAddEvent) { 244 | this.fire('layeradd', { layer: m }); 245 | } 246 | continue; 247 | } 248 | 249 | if (this.hasLayer(m)) { 250 | continue; 251 | } 252 | 253 | this._addLayer(m, this._maxZoom); 254 | if (!skipLayerAddEvent) { 255 | this.fire('layeradd', { layer: m }); 256 | } 257 | 258 | //If we just made a cluster of size 2 then we need to remove the other marker from the map (if it is) or we never will 259 | if (m.__parent) { 260 | if (m.__parent.getChildCount() === 2) { 261 | var markers = m.__parent.getAllChildMarkers(), 262 | otherMarker = markers[0] === m ? markers[1] : markers[0]; 263 | fg.removeLayer(otherMarker); 264 | } 265 | } 266 | } 267 | 268 | if (chunkProgress) { 269 | // report progress and time elapsed: 270 | chunkProgress(offset, l, (new Date()).getTime() - started); 271 | } 272 | 273 | // Completed processing all markers. 274 | if (offset === l) { 275 | 276 | // Refresh bounds and weighted positions. 277 | this._topClusterLevel._recalculateBounds(); 278 | 279 | this._refreshClustersIcons(); 280 | 281 | this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); 282 | } else { 283 | setTimeout(process, this.options.chunkDelay); 284 | } 285 | }, this); 286 | 287 | process(); 288 | } else { 289 | var needsClustering = this._needsClustering; 290 | 291 | for (; offset < l; offset++) { 292 | m = layersArray[offset]; 293 | 294 | // Group of layers, append children to layersArray and skip. 295 | if (m instanceof L.LayerGroup) { 296 | if (originalArray) { 297 | layersArray = layersArray.slice(); 298 | originalArray = false; 299 | } 300 | this._extractNonGroupLayers(m, layersArray); 301 | l = layersArray.length; 302 | continue; 303 | } 304 | 305 | //Not point data, can't be clustered 306 | if (!m.getLatLng) { 307 | npg.addLayer(m); 308 | continue; 309 | } 310 | 311 | if (this.hasLayer(m)) { 312 | continue; 313 | } 314 | 315 | needsClustering.push(m); 316 | } 317 | } 318 | return this; 319 | }, 320 | 321 | //Takes an array of markers and removes them in bulk 322 | removeLayers: function (layersArray) { 323 | var i, m, 324 | l = layersArray.length, 325 | fg = this._featureGroup, 326 | npg = this._nonPointGroup, 327 | originalArray = true; 328 | 329 | if (!this._map) { 330 | for (i = 0; i < l; i++) { 331 | m = layersArray[i]; 332 | 333 | // Group of layers, append children to layersArray and skip. 334 | if (m instanceof L.LayerGroup) { 335 | if (originalArray) { 336 | layersArray = layersArray.slice(); 337 | originalArray = false; 338 | } 339 | this._extractNonGroupLayers(m, layersArray); 340 | l = layersArray.length; 341 | continue; 342 | } 343 | 344 | this._arraySplice(this._needsClustering, m); 345 | npg.removeLayer(m); 346 | if (this.hasLayer(m)) { 347 | this._needsRemoving.push({ layer: m, latlng: m._latlng }); 348 | } 349 | this.fire('layerremove', { layer: m }); 350 | } 351 | return this; 352 | } 353 | 354 | if (this._unspiderfy) { 355 | this._unspiderfy(); 356 | 357 | // Work on a copy of the array, so that next loop is not affected. 358 | var layersArray2 = layersArray.slice(), 359 | l2 = l; 360 | for (i = 0; i < l2; i++) { 361 | m = layersArray2[i]; 362 | 363 | // Group of layers, append children to layersArray and skip. 364 | if (m instanceof L.LayerGroup) { 365 | this._extractNonGroupLayers(m, layersArray2); 366 | l2 = layersArray2.length; 367 | continue; 368 | } 369 | 370 | this._unspiderfyLayer(m); 371 | } 372 | } 373 | 374 | for (i = 0; i < l; i++) { 375 | m = layersArray[i]; 376 | 377 | // Group of layers, append children to layersArray and skip. 378 | if (m instanceof L.LayerGroup) { 379 | if (originalArray) { 380 | layersArray = layersArray.slice(); 381 | originalArray = false; 382 | } 383 | this._extractNonGroupLayers(m, layersArray); 384 | l = layersArray.length; 385 | continue; 386 | } 387 | 388 | if (!m.__parent) { 389 | npg.removeLayer(m); 390 | this.fire('layerremove', { layer: m }); 391 | continue; 392 | } 393 | 394 | this._removeLayer(m, true, true); 395 | this.fire('layerremove', { layer: m }); 396 | 397 | if (fg.hasLayer(m)) { 398 | fg.removeLayer(m); 399 | if (m.clusterShow) { 400 | m.clusterShow(); 401 | } 402 | } 403 | } 404 | 405 | // Refresh bounds and weighted positions. 406 | this._topClusterLevel._recalculateBounds(); 407 | 408 | this._refreshClustersIcons(); 409 | 410 | //Fix up the clusters and markers on the map 411 | this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); 412 | 413 | return this; 414 | }, 415 | 416 | //Removes all layers from the MarkerClusterGroup 417 | clearLayers: function () { 418 | //Need our own special implementation as the LayerGroup one doesn't work for us 419 | 420 | //If we aren't on the map (yet), blow away the markers we know of 421 | if (!this._map) { 422 | this._needsClustering = []; 423 | delete this._gridClusters; 424 | delete this._gridUnclustered; 425 | } 426 | 427 | if (this._noanimationUnspiderfy) { 428 | this._noanimationUnspiderfy(); 429 | } 430 | 431 | //Remove all the visible layers 432 | this._featureGroup.clearLayers(); 433 | this._nonPointGroup.clearLayers(); 434 | 435 | this.eachLayer(function (marker) { 436 | marker.off(this._childMarkerEventHandlers, this); 437 | delete marker.__parent; 438 | }, this); 439 | 440 | if (this._map) { 441 | //Reset _topClusterLevel and the DistanceGrids 442 | this._generateInitialClusters(); 443 | } 444 | 445 | return this; 446 | }, 447 | 448 | //Override FeatureGroup.getBounds as it doesn't work 449 | getBounds: function () { 450 | var bounds = new L.LatLngBounds(); 451 | 452 | if (this._topClusterLevel) { 453 | bounds.extend(this._topClusterLevel._bounds); 454 | } 455 | 456 | for (var i = this._needsClustering.length - 1; i >= 0; i--) { 457 | bounds.extend(this._needsClustering[i].getLatLng()); 458 | } 459 | 460 | bounds.extend(this._nonPointGroup.getBounds()); 461 | 462 | return bounds; 463 | }, 464 | 465 | //Overrides LayerGroup.eachLayer 466 | eachLayer: function (method, context) { 467 | var markers = this._needsClustering.slice(), 468 | needsRemoving = this._needsRemoving, 469 | thisNeedsRemoving, i, j; 470 | 471 | if (this._topClusterLevel) { 472 | this._topClusterLevel.getAllChildMarkers(markers); 473 | } 474 | 475 | for (i = markers.length - 1; i >= 0; i--) { 476 | thisNeedsRemoving = true; 477 | 478 | for (j = needsRemoving.length - 1; j >= 0; j--) { 479 | if (needsRemoving[j].layer === markers[i]) { 480 | thisNeedsRemoving = false; 481 | break; 482 | } 483 | } 484 | 485 | if (thisNeedsRemoving) { 486 | method.call(context, markers[i]); 487 | } 488 | } 489 | 490 | this._nonPointGroup.eachLayer(method, context); 491 | }, 492 | 493 | //Overrides LayerGroup.getLayers 494 | getLayers: function () { 495 | var layers = []; 496 | this.eachLayer(function (l) { 497 | layers.push(l); 498 | }); 499 | return layers; 500 | }, 501 | 502 | //Overrides LayerGroup.getLayer, WARNING: Really bad performance 503 | getLayer: function (id) { 504 | var result = null; 505 | 506 | id = parseInt(id, 10); 507 | 508 | this.eachLayer(function (l) { 509 | if (L.stamp(l) === id) { 510 | result = l; 511 | } 512 | }); 513 | 514 | return result; 515 | }, 516 | 517 | //Returns true if the given layer is in this MarkerClusterGroup 518 | hasLayer: function (layer) { 519 | if (!layer) { 520 | return false; 521 | } 522 | 523 | var i, anArray = this._needsClustering; 524 | 525 | for (i = anArray.length - 1; i >= 0; i--) { 526 | if (anArray[i] === layer) { 527 | return true; 528 | } 529 | } 530 | 531 | anArray = this._needsRemoving; 532 | for (i = anArray.length - 1; i >= 0; i--) { 533 | if (anArray[i].layer === layer) { 534 | return false; 535 | } 536 | } 537 | 538 | return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer); 539 | }, 540 | 541 | //Zoom down to show the given layer (spiderfying if necessary) then calls the callback 542 | zoomToShowLayer: function (layer, callback) { 543 | 544 | if (typeof callback !== 'function') { 545 | callback = function () {}; 546 | } 547 | 548 | var showMarker = function () { 549 | if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) { 550 | this._map.off('moveend', showMarker, this); 551 | this.off('animationend', showMarker, this); 552 | 553 | if (layer._icon) { 554 | callback(); 555 | } else if (layer.__parent._icon) { 556 | this.once('spiderfied', callback, this); 557 | layer.__parent.spiderfy(); 558 | } 559 | } 560 | }; 561 | 562 | if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) { 563 | //Layer is visible ond on screen, immediate return 564 | callback(); 565 | } else if (layer.__parent._zoom < Math.round(this._map._zoom)) { 566 | //Layer should be visible at this zoom level. It must not be on screen so just pan over to it 567 | this._map.on('moveend', showMarker, this); 568 | this._map.panTo(layer.getLatLng()); 569 | } else { 570 | this._map.on('moveend', showMarker, this); 571 | this.on('animationend', showMarker, this); 572 | layer.__parent.zoomToBounds(); 573 | } 574 | }, 575 | 576 | //Overrides FeatureGroup.onAdd 577 | onAdd: function (map) { 578 | this._map = map; 579 | var i, l, layer; 580 | 581 | if (!isFinite(this._map.getMaxZoom())) { 582 | throw "Map has no maxZoom specified"; 583 | } 584 | 585 | this._featureGroup.addTo(map); 586 | this._nonPointGroup.addTo(map); 587 | 588 | if (!this._gridClusters) { 589 | this._generateInitialClusters(); 590 | } 591 | 592 | this._maxLat = map.options.crs.projection.MAX_LATITUDE; 593 | 594 | //Restore all the positions as they are in the MCG before removing them 595 | for (i = 0, l = this._needsRemoving.length; i < l; i++) { 596 | layer = this._needsRemoving[i]; 597 | layer.newlatlng = layer.layer._latlng; 598 | layer.layer._latlng = layer.latlng; 599 | } 600 | //Remove them, then restore their new positions 601 | for (i = 0, l = this._needsRemoving.length; i < l; i++) { 602 | layer = this._needsRemoving[i]; 603 | this._removeLayer(layer.layer, true); 604 | layer.layer._latlng = layer.newlatlng; 605 | } 606 | this._needsRemoving = []; 607 | 608 | //Remember the current zoom level and bounds 609 | this._zoom = Math.round(this._map._zoom); 610 | this._currentShownBounds = this._getExpandedVisibleBounds(); 611 | 612 | this._map.on('zoomend', this._zoomEnd, this); 613 | this._map.on('moveend', this._moveEnd, this); 614 | 615 | if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely 616 | this._spiderfierOnAdd(); 617 | } 618 | 619 | this._bindEvents(); 620 | 621 | //Actually add our markers to the map: 622 | l = this._needsClustering; 623 | this._needsClustering = []; 624 | this.addLayers(l, true); 625 | }, 626 | 627 | //Overrides FeatureGroup.onRemove 628 | onRemove: function (map) { 629 | map.off('zoomend', this._zoomEnd, this); 630 | map.off('moveend', this._moveEnd, this); 631 | 632 | this._unbindEvents(); 633 | 634 | //In case we are in a cluster animation 635 | this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); 636 | 637 | if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely 638 | this._spiderfierOnRemove(); 639 | } 640 | 641 | delete this._maxLat; 642 | 643 | //Clean up all the layers we added to the map 644 | this._hideCoverage(); 645 | this._featureGroup.remove(); 646 | this._nonPointGroup.remove(); 647 | 648 | this._featureGroup.clearLayers(); 649 | 650 | this._map = null; 651 | }, 652 | 653 | getVisibleParent: function (marker) { 654 | var vMarker = marker; 655 | while (vMarker && !vMarker._icon) { 656 | vMarker = vMarker.__parent; 657 | } 658 | return vMarker || null; 659 | }, 660 | 661 | //Remove the given object from the given array 662 | _arraySplice: function (anArray, obj) { 663 | for (var i = anArray.length - 1; i >= 0; i--) { 664 | if (anArray[i] === obj) { 665 | anArray.splice(i, 1); 666 | return true; 667 | } 668 | } 669 | }, 670 | 671 | /** 672 | * Removes a marker from all _gridUnclustered zoom levels, starting at the supplied zoom. 673 | * @param marker to be removed from _gridUnclustered. 674 | * @param z integer bottom start zoom level (included) 675 | * @private 676 | */ 677 | _removeFromGridUnclustered: function (marker, z) { 678 | var map = this._map, 679 | gridUnclustered = this._gridUnclustered, 680 | minZoom = this._map.getMinZoom(); 681 | 682 | for (; z >= minZoom; z--) { 683 | if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) { 684 | break; 685 | } 686 | } 687 | }, 688 | 689 | _childMarkerDragStart: function (e) { 690 | e.target.__dragStart = e.target._latlng; 691 | }, 692 | 693 | _childMarkerMoved: function (e) { 694 | if (!this._ignoreMove && !e.target.__dragStart) { 695 | var isPopupOpen = e.target._popup && e.target._popup.isOpen(); 696 | 697 | this._moveChild(e.target, e.oldLatLng, e.latlng); 698 | 699 | if (isPopupOpen) { 700 | e.target.openPopup(); 701 | } 702 | } 703 | }, 704 | 705 | _moveChild: function (layer, from, to) { 706 | layer._latlng = from; 707 | this.removeLayer(layer); 708 | 709 | layer._latlng = to; 710 | this.addLayer(layer); 711 | }, 712 | 713 | _childMarkerDragEnd: function (e) { 714 | if (e.target.__dragStart) { 715 | this._moveChild(e.target, e.target.__dragStart, e.target._latlng); 716 | } 717 | delete e.target.__dragStart; 718 | }, 719 | 720 | 721 | //Internal function for removing a marker from everything. 722 | //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions) 723 | _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) { 724 | var gridClusters = this._gridClusters, 725 | gridUnclustered = this._gridUnclustered, 726 | fg = this._featureGroup, 727 | map = this._map, 728 | minZoom = this._map.getMinZoom(); 729 | 730 | //Remove the marker from distance clusters it might be in 731 | if (removeFromDistanceGrid) { 732 | this._removeFromGridUnclustered(marker, this._maxZoom); 733 | } 734 | 735 | //Work our way up the clusters removing them as we go if required 736 | var cluster = marker.__parent, 737 | markers = cluster._markers, 738 | otherMarker; 739 | 740 | //Remove the marker from the immediate parents marker list 741 | this._arraySplice(markers, marker); 742 | 743 | while (cluster) { 744 | cluster._childCount--; 745 | cluster._boundsNeedUpdate = true; 746 | 747 | if (cluster._zoom < minZoom) { 748 | //Top level, do nothing 749 | break; 750 | } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required 751 | //We need to push the other marker up to the parent 752 | otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0]; 753 | 754 | //Update distance grid 755 | gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom)); 756 | gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom)); 757 | 758 | //Move otherMarker up to parent 759 | this._arraySplice(cluster.__parent._childClusters, cluster); 760 | cluster.__parent._markers.push(otherMarker); 761 | otherMarker.__parent = cluster.__parent; 762 | 763 | if (cluster._icon) { 764 | //Cluster is currently on the map, need to put the marker on the map instead 765 | fg.removeLayer(cluster); 766 | if (!dontUpdateMap) { 767 | fg.addLayer(otherMarker); 768 | } 769 | } 770 | } else { 771 | cluster._iconNeedsUpdate = true; 772 | } 773 | 774 | cluster = cluster.__parent; 775 | } 776 | 777 | delete marker.__parent; 778 | }, 779 | 780 | _isOrIsParent: function (el, oel) { 781 | while (oel) { 782 | if (el === oel) { 783 | return true; 784 | } 785 | oel = oel.parentNode; 786 | } 787 | return false; 788 | }, 789 | 790 | //Override L.Evented.fire 791 | fire: function (type, data, propagate) { 792 | if (data && data.layer instanceof L.MarkerCluster) { 793 | //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget) 794 | if (data.originalEvent && this._isOrIsParent(data.layer._icon, data.originalEvent.relatedTarget)) { 795 | return; 796 | } 797 | type = 'cluster' + type; 798 | } 799 | 800 | L.FeatureGroup.prototype.fire.call(this, type, data, propagate); 801 | }, 802 | 803 | //Override L.Evented.listens 804 | listens: function (type, propagate) { 805 | return L.FeatureGroup.prototype.listens.call(this, type, propagate) || L.FeatureGroup.prototype.listens.call(this, 'cluster' + type, propagate); 806 | }, 807 | 808 | //Default functionality 809 | _defaultIconCreateFunction: function (cluster) { 810 | var childCount = cluster.getChildCount(); 811 | 812 | var c = ' marker-cluster-'; 813 | if (childCount < 10) { 814 | c += 'small'; 815 | } else if (childCount < 100) { 816 | c += 'medium'; 817 | } else { 818 | c += 'large'; 819 | } 820 | 821 | return new L.DivIcon({ html: '
' + childCount + '
', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) }); 822 | }, 823 | 824 | _bindEvents: function () { 825 | var map = this._map, 826 | spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, 827 | showCoverageOnHover = this.options.showCoverageOnHover, 828 | zoomToBoundsOnClick = this.options.zoomToBoundsOnClick; 829 | 830 | //Zoom on cluster click or spiderfy if we are at the lowest level 831 | if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { 832 | this.on('clusterclick', this._zoomOrSpiderfy, this); 833 | } 834 | 835 | //Show convex hull (boundary) polygon on mouse over 836 | if (showCoverageOnHover) { 837 | this.on('clustermouseover', this._showCoverage, this); 838 | this.on('clustermouseout', this._hideCoverage, this); 839 | map.on('zoomend', this._hideCoverage, this); 840 | } 841 | }, 842 | 843 | _zoomOrSpiderfy: function (e) { 844 | var cluster = e.layer, 845 | bottomCluster = cluster; 846 | 847 | while (bottomCluster._childClusters.length === 1) { 848 | bottomCluster = bottomCluster._childClusters[0]; 849 | } 850 | 851 | if (bottomCluster._zoom === this._maxZoom && 852 | bottomCluster._childCount === cluster._childCount && 853 | this.options.spiderfyOnMaxZoom) { 854 | 855 | // All child markers are contained in a single cluster from this._maxZoom to this cluster. 856 | cluster.spiderfy(); 857 | } else if (this.options.zoomToBoundsOnClick) { 858 | cluster.zoomToBounds(); 859 | } 860 | 861 | // Focus the map again for keyboard users. 862 | if (e.originalEvent && e.originalEvent.keyCode === 13) { 863 | this._map._container.focus(); 864 | } 865 | }, 866 | 867 | _showCoverage: function (e) { 868 | var map = this._map; 869 | if (this._inZoomAnimation) { 870 | return; 871 | } 872 | if (this._shownPolygon) { 873 | map.removeLayer(this._shownPolygon); 874 | } 875 | if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) { 876 | this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions); 877 | map.addLayer(this._shownPolygon); 878 | } 879 | }, 880 | 881 | _hideCoverage: function () { 882 | if (this._shownPolygon) { 883 | this._map.removeLayer(this._shownPolygon); 884 | this._shownPolygon = null; 885 | } 886 | }, 887 | 888 | _unbindEvents: function () { 889 | var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, 890 | showCoverageOnHover = this.options.showCoverageOnHover, 891 | zoomToBoundsOnClick = this.options.zoomToBoundsOnClick, 892 | map = this._map; 893 | 894 | if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { 895 | this.off('clusterclick', this._zoomOrSpiderfy, this); 896 | } 897 | if (showCoverageOnHover) { 898 | this.off('clustermouseover', this._showCoverage, this); 899 | this.off('clustermouseout', this._hideCoverage, this); 900 | map.off('zoomend', this._hideCoverage, this); 901 | } 902 | }, 903 | 904 | _zoomEnd: function () { 905 | if (!this._map) { //May have been removed from the map by a zoomEnd handler 906 | return; 907 | } 908 | this._mergeSplitClusters(); 909 | 910 | this._zoom = Math.round(this._map._zoom); 911 | this._currentShownBounds = this._getExpandedVisibleBounds(); 912 | }, 913 | 914 | _moveEnd: function () { 915 | if (this._inZoomAnimation) { 916 | return; 917 | } 918 | 919 | var newBounds = this._getExpandedVisibleBounds(); 920 | 921 | this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._map.getMinZoom(), this._zoom, newBounds); 922 | this._topClusterLevel._recursivelyAddChildrenToMap(null, Math.round(this._map._zoom), newBounds); 923 | 924 | this._currentShownBounds = newBounds; 925 | return; 926 | }, 927 | 928 | _generateInitialClusters: function () { 929 | var maxZoom = this._map.getMaxZoom(), 930 | minZoom = this._map.getMinZoom(), 931 | radius = this.options.maxClusterRadius, 932 | radiusFn = radius; 933 | 934 | //If we just set maxClusterRadius to a single number, we need to create 935 | //a simple function to return that number. Otherwise, we just have to 936 | //use the function we've passed in. 937 | if (typeof radius !== "function") { 938 | radiusFn = function () { return radius; }; 939 | } 940 | 941 | if (this.options.disableClusteringAtZoom !== null) { 942 | maxZoom = this.options.disableClusteringAtZoom - 1; 943 | } 944 | this._maxZoom = maxZoom; 945 | this._gridClusters = {}; 946 | this._gridUnclustered = {}; 947 | 948 | //Set up DistanceGrids for each zoom 949 | for (var zoom = maxZoom; zoom >= minZoom; zoom--) { 950 | this._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom)); 951 | this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom)); 952 | } 953 | 954 | // Instantiate the appropriate L.MarkerCluster class (animated or not). 955 | this._topClusterLevel = new this._markerCluster(this, minZoom - 1); 956 | }, 957 | 958 | //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom) 959 | _addLayer: function (layer, zoom) { 960 | var gridClusters = this._gridClusters, 961 | gridUnclustered = this._gridUnclustered, 962 | minZoom = this._map.getMinZoom(), 963 | markerPoint, z; 964 | 965 | if (this.options.singleMarkerMode) { 966 | this._overrideMarkerIcon(layer); 967 | } 968 | 969 | layer.on(this._childMarkerEventHandlers, this); 970 | 971 | //Find the lowest zoom level to slot this one in 972 | for (; zoom >= minZoom; zoom--) { 973 | markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position 974 | 975 | //Try find a cluster close by 976 | var closest = gridClusters[zoom].getNearObject(markerPoint); 977 | if (closest) { 978 | closest._addChild(layer); 979 | layer.__parent = closest; 980 | return; 981 | } 982 | 983 | //Try find a marker close by to form a new cluster with 984 | closest = gridUnclustered[zoom].getNearObject(markerPoint); 985 | if (closest) { 986 | var parent = closest.__parent; 987 | if (parent) { 988 | this._removeLayer(closest, false); 989 | } 990 | 991 | //Create new cluster with these 2 in it 992 | 993 | var newCluster = new this._markerCluster(this, zoom, closest, layer); 994 | gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom)); 995 | closest.__parent = newCluster; 996 | layer.__parent = newCluster; 997 | 998 | //First create any new intermediate parent clusters that don't exist 999 | var lastParent = newCluster; 1000 | for (z = zoom - 1; z > parent._zoom; z--) { 1001 | lastParent = new this._markerCluster(this, z, lastParent); 1002 | gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z)); 1003 | } 1004 | parent._addChild(lastParent); 1005 | 1006 | //Remove closest from this zoom level and any above that it is in, replace with newCluster 1007 | this._removeFromGridUnclustered(closest, zoom); 1008 | 1009 | return; 1010 | } 1011 | 1012 | //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards 1013 | gridUnclustered[zoom].addObject(layer, markerPoint); 1014 | } 1015 | 1016 | //Didn't get in anything, add us to the top 1017 | this._topClusterLevel._addChild(layer); 1018 | layer.__parent = this._topClusterLevel; 1019 | return; 1020 | }, 1021 | 1022 | /** 1023 | * Refreshes the icon of all "dirty" visible clusters. 1024 | * Non-visible "dirty" clusters will be updated when they are added to the map. 1025 | * @private 1026 | */ 1027 | _refreshClustersIcons: function () { 1028 | this._featureGroup.eachLayer(function (c) { 1029 | if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) { 1030 | c._updateIcon(); 1031 | } 1032 | }); 1033 | }, 1034 | 1035 | //Enqueue code to fire after the marker expand/contract has happened 1036 | _enqueue: function (fn) { 1037 | this._queue.push(fn); 1038 | if (!this._queueTimeout) { 1039 | this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300); 1040 | } 1041 | }, 1042 | _processQueue: function () { 1043 | for (var i = 0; i < this._queue.length; i++) { 1044 | this._queue[i].call(this); 1045 | } 1046 | this._queue.length = 0; 1047 | clearTimeout(this._queueTimeout); 1048 | this._queueTimeout = null; 1049 | }, 1050 | 1051 | //Merge and split any existing clusters that are too big or small 1052 | _mergeSplitClusters: function () { 1053 | var mapZoom = Math.round(this._map._zoom); 1054 | 1055 | //In case we are starting to split before the animation finished 1056 | this._processQueue(); 1057 | 1058 | if (this._zoom < mapZoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split 1059 | this._animationStart(); 1060 | //Remove clusters now off screen 1061 | this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._map.getMinZoom(), this._zoom, this._getExpandedVisibleBounds()); 1062 | 1063 | this._animationZoomIn(this._zoom, mapZoom); 1064 | 1065 | } else if (this._zoom > mapZoom) { //Zoom out, merge 1066 | this._animationStart(); 1067 | 1068 | this._animationZoomOut(this._zoom, mapZoom); 1069 | } else { 1070 | this._moveEnd(); 1071 | } 1072 | }, 1073 | 1074 | //Gets the maps visible bounds expanded in each direction by the size of the screen (so the user cannot see an area we do not cover in one pan) 1075 | _getExpandedVisibleBounds: function () { 1076 | if (!this.options.removeOutsideVisibleBounds) { 1077 | return this._mapBoundsInfinite; 1078 | } else if (L.Browser.mobile) { 1079 | return this._checkBoundsMaxLat(this._map.getBounds()); 1080 | } 1081 | 1082 | return this._checkBoundsMaxLat(this._map.getBounds().pad(1)); // Padding expands the bounds by its own dimensions but scaled with the given factor. 1083 | }, 1084 | 1085 | /** 1086 | * Expands the latitude to Infinity (or -Infinity) if the input bounds reach the map projection maximum defined latitude 1087 | * (in the case of Web/Spherical Mercator, it is 85.0511287798 / see https://en.wikipedia.org/wiki/Web_Mercator#Formulas). 1088 | * Otherwise, the removeOutsideVisibleBounds option will remove markers beyond that limit, whereas the same markers without 1089 | * this option (or outside MCG) will have their position floored (ceiled) by the projection and rendered at that limit, 1090 | * making the user think that MCG "eats" them and never displays them again. 1091 | * @param bounds L.LatLngBounds 1092 | * @returns {L.LatLngBounds} 1093 | * @private 1094 | */ 1095 | _checkBoundsMaxLat: function (bounds) { 1096 | var maxLat = this._maxLat; 1097 | 1098 | if (maxLat !== undefined) { 1099 | if (bounds.getNorth() >= maxLat) { 1100 | bounds._northEast.lat = Infinity; 1101 | } 1102 | if (bounds.getSouth() <= -maxLat) { 1103 | bounds._southWest.lat = -Infinity; 1104 | } 1105 | } 1106 | 1107 | return bounds; 1108 | }, 1109 | 1110 | //Shared animation code 1111 | _animationAddLayerNonAnimated: function (layer, newCluster) { 1112 | if (newCluster === layer) { 1113 | this._featureGroup.addLayer(layer); 1114 | } else if (newCluster._childCount === 2) { 1115 | newCluster._addToMap(); 1116 | 1117 | var markers = newCluster.getAllChildMarkers(); 1118 | this._featureGroup.removeLayer(markers[0]); 1119 | this._featureGroup.removeLayer(markers[1]); 1120 | } else { 1121 | newCluster._updateIcon(); 1122 | } 1123 | }, 1124 | 1125 | /** 1126 | * Extracts individual (i.e. non-group) layers from a Layer Group. 1127 | * @param group to extract layers from. 1128 | * @param output {Array} in which to store the extracted layers. 1129 | * @returns {*|Array} 1130 | * @private 1131 | */ 1132 | _extractNonGroupLayers: function (group, output) { 1133 | var layers = group.getLayers(), 1134 | i = 0, 1135 | layer; 1136 | 1137 | output = output || []; 1138 | 1139 | for (; i < layers.length; i++) { 1140 | layer = layers[i]; 1141 | 1142 | if (layer instanceof L.LayerGroup) { 1143 | this._extractNonGroupLayers(layer, output); 1144 | continue; 1145 | } 1146 | 1147 | output.push(layer); 1148 | } 1149 | 1150 | return output; 1151 | }, 1152 | 1153 | /** 1154 | * Implements the singleMarkerMode option. 1155 | * @param layer Marker to re-style using the Clusters iconCreateFunction. 1156 | * @returns {L.Icon} The newly created icon. 1157 | * @private 1158 | */ 1159 | _overrideMarkerIcon: function (layer) { 1160 | var icon = layer.options.icon = this.options.iconCreateFunction({ 1161 | getChildCount: function () { 1162 | return 1; 1163 | }, 1164 | getAllChildMarkers: function () { 1165 | return [layer]; 1166 | } 1167 | }); 1168 | 1169 | return icon; 1170 | } 1171 | }); 1172 | 1173 | // Constant bounds used in case option "removeOutsideVisibleBounds" is set to false. 1174 | L.MarkerClusterGroup.include({ 1175 | _mapBoundsInfinite: new L.LatLngBounds(new L.LatLng(-Infinity, -Infinity), new L.LatLng(Infinity, Infinity)) 1176 | }); 1177 | 1178 | L.MarkerClusterGroup.include({ 1179 | _noAnimation: { 1180 | //Non Animated versions of everything 1181 | _animationStart: function () { 1182 | //Do nothing... 1183 | }, 1184 | _animationZoomIn: function (previousZoomLevel, newZoomLevel) { 1185 | this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._map.getMinZoom(), previousZoomLevel); 1186 | this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); 1187 | 1188 | //We didn't actually animate, but we use this event to mean "clustering animations have finished" 1189 | this.fire('animationend'); 1190 | }, 1191 | _animationZoomOut: function (previousZoomLevel, newZoomLevel) { 1192 | this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._map.getMinZoom(), previousZoomLevel); 1193 | this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); 1194 | 1195 | //We didn't actually animate, but we use this event to mean "clustering animations have finished" 1196 | this.fire('animationend'); 1197 | }, 1198 | _animationAddLayer: function (layer, newCluster) { 1199 | this._animationAddLayerNonAnimated(layer, newCluster); 1200 | } 1201 | }, 1202 | 1203 | _withAnimation: { 1204 | //Animated versions here 1205 | _animationStart: function () { 1206 | this._map._mapPane.className += ' leaflet-cluster-anim'; 1207 | this._inZoomAnimation++; 1208 | }, 1209 | 1210 | _animationZoomIn: function (previousZoomLevel, newZoomLevel) { 1211 | var bounds = this._getExpandedVisibleBounds(), 1212 | fg = this._featureGroup, 1213 | minZoom = this._map.getMinZoom(), 1214 | i; 1215 | 1216 | this._ignoreMove = true; 1217 | 1218 | //Add all children of current clusters to map and remove those clusters from map 1219 | this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) { 1220 | var startPos = c._latlng, 1221 | markers = c._markers, 1222 | m; 1223 | 1224 | if (!bounds.contains(startPos)) { 1225 | startPos = null; 1226 | } 1227 | 1228 | if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us 1229 | fg.removeLayer(c); 1230 | c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds); 1231 | } else { 1232 | //Fade out old cluster 1233 | c.clusterHide(); 1234 | c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds); 1235 | } 1236 | 1237 | //Remove all markers that aren't visible any more 1238 | //TODO: Do we actually need to do this on the higher levels too? 1239 | for (i = markers.length - 1; i >= 0; i--) { 1240 | m = markers[i]; 1241 | if (!bounds.contains(m._latlng)) { 1242 | fg.removeLayer(m); 1243 | } 1244 | } 1245 | 1246 | }); 1247 | 1248 | this._forceLayout(); 1249 | 1250 | //Update opacities 1251 | this._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel); 1252 | //TODO Maybe? Update markers in _recursivelyBecomeVisible 1253 | fg.eachLayer(function (n) { 1254 | if (!(n instanceof L.MarkerCluster) && n._icon) { 1255 | n.clusterShow(); 1256 | } 1257 | }); 1258 | 1259 | //update the positions of the just added clusters/markers 1260 | this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) { 1261 | c._recursivelyRestoreChildPositions(newZoomLevel); 1262 | }); 1263 | 1264 | this._ignoreMove = false; 1265 | 1266 | //Remove the old clusters and close the zoom animation 1267 | this._enqueue(function () { 1268 | //update the positions of the just added clusters/markers 1269 | this._topClusterLevel._recursively(bounds, previousZoomLevel, minZoom, function (c) { 1270 | fg.removeLayer(c); 1271 | c.clusterShow(); 1272 | }); 1273 | 1274 | this._animationEnd(); 1275 | }); 1276 | }, 1277 | 1278 | _animationZoomOut: function (previousZoomLevel, newZoomLevel) { 1279 | this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel); 1280 | 1281 | //Need to add markers for those that weren't on the map before but are now 1282 | this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); 1283 | //Remove markers that were on the map before but won't be now 1284 | this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._map.getMinZoom(), previousZoomLevel, this._getExpandedVisibleBounds()); 1285 | }, 1286 | 1287 | _animationAddLayer: function (layer, newCluster) { 1288 | var me = this, 1289 | fg = this._featureGroup; 1290 | 1291 | fg.addLayer(layer); 1292 | if (newCluster !== layer) { 1293 | if (newCluster._childCount > 2) { //Was already a cluster 1294 | 1295 | newCluster._updateIcon(); 1296 | this._forceLayout(); 1297 | this._animationStart(); 1298 | 1299 | layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng())); 1300 | layer.clusterHide(); 1301 | 1302 | this._enqueue(function () { 1303 | fg.removeLayer(layer); 1304 | layer.clusterShow(); 1305 | 1306 | me._animationEnd(); 1307 | }); 1308 | 1309 | } else { //Just became a cluster 1310 | this._forceLayout(); 1311 | 1312 | me._animationStart(); 1313 | me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._zoom); 1314 | } 1315 | } 1316 | } 1317 | }, 1318 | 1319 | // Private methods for animated versions. 1320 | _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) { 1321 | var bounds = this._getExpandedVisibleBounds(), 1322 | minZoom = this._map.getMinZoom(); 1323 | 1324 | //Animate all of the markers in the clusters to move to their cluster center point 1325 | cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, minZoom, previousZoomLevel + 1, newZoomLevel); 1326 | 1327 | var me = this; 1328 | 1329 | //Update the opacity (If we immediately set it they won't animate) 1330 | this._forceLayout(); 1331 | cluster._recursivelyBecomeVisible(bounds, newZoomLevel); 1332 | 1333 | //TODO: Maybe use the transition timing stuff to make this more reliable 1334 | //When the animations are done, tidy up 1335 | this._enqueue(function () { 1336 | 1337 | //This cluster stopped being a cluster before the timeout fired 1338 | if (cluster._childCount === 1) { 1339 | var m = cluster._markers[0]; 1340 | //If we were in a cluster animation at the time then the opacity and position of our child could be wrong now, so fix it 1341 | this._ignoreMove = true; 1342 | m.setLatLng(m.getLatLng()); 1343 | this._ignoreMove = false; 1344 | if (m.clusterShow) { 1345 | m.clusterShow(); 1346 | } 1347 | } else { 1348 | cluster._recursively(bounds, newZoomLevel, minZoom, function (c) { 1349 | c._recursivelyRemoveChildrenFromMap(bounds, minZoom, previousZoomLevel + 1); 1350 | }); 1351 | } 1352 | me._animationEnd(); 1353 | }); 1354 | }, 1355 | 1356 | _animationEnd: function () { 1357 | if (this._map) { 1358 | this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); 1359 | } 1360 | this._inZoomAnimation--; 1361 | this.fire('animationend'); 1362 | }, 1363 | 1364 | //Force a browser layout of stuff in the map 1365 | // Should apply the current opacity and location to all elements so we can update them again for an animation 1366 | _forceLayout: function () { 1367 | //In my testing this works, infact offsetWidth of any element seems to work. 1368 | //Could loop all this._layers and do this for each _icon if it stops working 1369 | 1370 | L.Util.falseFn(document.body.offsetWidth); 1371 | } 1372 | }); 1373 | 1374 | L.markerClusterGroup = function (options) { 1375 | return new L.MarkerClusterGroup(options); 1376 | }; 1377 | 1378 | 1379 | L.MarkerCluster = L.Marker.extend({ 1380 | initialize: function (group, zoom, a, b) { 1381 | 1382 | L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), { icon: this }); 1383 | 1384 | 1385 | this._group = group; 1386 | this._zoom = zoom; 1387 | 1388 | this._markers = []; 1389 | this._childClusters = []; 1390 | this._childCount = 0; 1391 | this._iconNeedsUpdate = true; 1392 | this._boundsNeedUpdate = true; 1393 | 1394 | this._bounds = new L.LatLngBounds(); 1395 | 1396 | if (a) { 1397 | this._addChild(a); 1398 | } 1399 | if (b) { 1400 | this._addChild(b); 1401 | } 1402 | }, 1403 | 1404 | //Recursively retrieve all child markers of this cluster 1405 | getAllChildMarkers: function (storageArray) { 1406 | storageArray = storageArray || []; 1407 | 1408 | for (var i = this._childClusters.length - 1; i >= 0; i--) { 1409 | this._childClusters[i].getAllChildMarkers(storageArray); 1410 | } 1411 | 1412 | for (var j = this._markers.length - 1; j >= 0; j--) { 1413 | storageArray.push(this._markers[j]); 1414 | } 1415 | 1416 | return storageArray; 1417 | }, 1418 | 1419 | //Returns the count of how many child markers we have 1420 | getChildCount: function () { 1421 | return this._childCount; 1422 | }, 1423 | 1424 | //Zoom to the minimum of showing all of the child markers, or the extents of this cluster 1425 | zoomToBounds: function (fitBoundsOptions) { 1426 | var childClusters = this._childClusters.slice(), 1427 | map = this._group._map, 1428 | boundsZoom = map.getBoundsZoom(this._bounds), 1429 | zoom = this._zoom + 1, 1430 | mapZoom = map.getZoom(), 1431 | i; 1432 | 1433 | //calculate how far we need to zoom down to see all of the markers 1434 | while (childClusters.length > 0 && boundsZoom > zoom) { 1435 | zoom++; 1436 | var newClusters = []; 1437 | for (i = 0; i < childClusters.length; i++) { 1438 | newClusters = newClusters.concat(childClusters[i]._childClusters); 1439 | } 1440 | childClusters = newClusters; 1441 | } 1442 | 1443 | if (boundsZoom > zoom) { 1444 | this._group._map.setView(this._latlng, zoom); 1445 | } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead 1446 | this._group._map.setView(this._latlng, mapZoom + 1); 1447 | } else { 1448 | this._group._map.fitBounds(this._bounds, fitBoundsOptions); 1449 | } 1450 | }, 1451 | 1452 | getBounds: function () { 1453 | var bounds = new L.LatLngBounds(); 1454 | bounds.extend(this._bounds); 1455 | return bounds; 1456 | }, 1457 | 1458 | _updateIcon: function () { 1459 | this._iconNeedsUpdate = true; 1460 | if (this._icon) { 1461 | this.setIcon(this); 1462 | } 1463 | }, 1464 | 1465 | //Cludge for Icon, we pretend to be an icon for performance 1466 | createIcon: function () { 1467 | if (this._iconNeedsUpdate) { 1468 | this._iconObj = this._group.options.iconCreateFunction(this); 1469 | this._iconNeedsUpdate = false; 1470 | } 1471 | return this._iconObj.createIcon(); 1472 | }, 1473 | createShadow: function () { 1474 | return this._iconObj.createShadow(); 1475 | }, 1476 | 1477 | 1478 | _addChild: function (new1, isNotificationFromChild) { 1479 | 1480 | this._iconNeedsUpdate = true; 1481 | 1482 | this._boundsNeedUpdate = true; 1483 | this._setClusterCenter(new1); 1484 | 1485 | if (new1 instanceof L.MarkerCluster) { 1486 | if (!isNotificationFromChild) { 1487 | this._childClusters.push(new1); 1488 | new1.__parent = this; 1489 | } 1490 | this._childCount += new1._childCount; 1491 | } else { 1492 | if (!isNotificationFromChild) { 1493 | this._markers.push(new1); 1494 | } 1495 | this._childCount++; 1496 | } 1497 | 1498 | if (this.__parent) { 1499 | this.__parent._addChild(new1, true); 1500 | } 1501 | }, 1502 | 1503 | /** 1504 | * Makes sure the cluster center is set. If not, uses the child center if it is a cluster, or the marker position. 1505 | * @param child L.MarkerCluster|L.Marker that will be used as cluster center if not defined yet. 1506 | * @private 1507 | */ 1508 | _setClusterCenter: function (child) { 1509 | if (!this._cLatLng) { 1510 | // when clustering, take position of the first point as the cluster center 1511 | this._cLatLng = child._cLatLng || child._latlng; 1512 | } 1513 | }, 1514 | 1515 | /** 1516 | * Assigns impossible bounding values so that the next extend entirely determines the new bounds. 1517 | * This method avoids having to trash the previous L.LatLngBounds object and to create a new one, which is much slower for this class. 1518 | * As long as the bounds are not extended, most other methods would probably fail, as they would with bounds initialized but not extended. 1519 | * @private 1520 | */ 1521 | _resetBounds: function () { 1522 | var bounds = this._bounds; 1523 | 1524 | if (bounds._southWest) { 1525 | bounds._southWest.lat = Infinity; 1526 | bounds._southWest.lng = Infinity; 1527 | } 1528 | if (bounds._northEast) { 1529 | bounds._northEast.lat = -Infinity; 1530 | bounds._northEast.lng = -Infinity; 1531 | } 1532 | }, 1533 | 1534 | _recalculateBounds: function () { 1535 | var markers = this._markers, 1536 | childClusters = this._childClusters, 1537 | latSum = 0, 1538 | lngSum = 0, 1539 | totalCount = this._childCount, 1540 | i, child, childLatLng, childCount; 1541 | 1542 | // Case where all markers are removed from the map and we are left with just an empty _topClusterLevel. 1543 | if (totalCount === 0) { 1544 | return; 1545 | } 1546 | 1547 | // Reset rather than creating a new object, for performance. 1548 | this._resetBounds(); 1549 | 1550 | // Child markers. 1551 | for (i = 0; i < markers.length; i++) { 1552 | childLatLng = markers[i]._latlng; 1553 | 1554 | this._bounds.extend(childLatLng); 1555 | 1556 | latSum += childLatLng.lat; 1557 | lngSum += childLatLng.lng; 1558 | } 1559 | 1560 | // Child clusters. 1561 | for (i = 0; i < childClusters.length; i++) { 1562 | child = childClusters[i]; 1563 | 1564 | // Re-compute child bounds and weighted position first if necessary. 1565 | if (child._boundsNeedUpdate) { 1566 | child._recalculateBounds(); 1567 | } 1568 | 1569 | this._bounds.extend(child._bounds); 1570 | 1571 | childLatLng = child._wLatLng; 1572 | childCount = child._childCount; 1573 | 1574 | latSum += childLatLng.lat * childCount; 1575 | lngSum += childLatLng.lng * childCount; 1576 | } 1577 | 1578 | this._latlng = this._wLatLng = new L.LatLng(latSum / totalCount, lngSum / totalCount); 1579 | 1580 | // Reset dirty flag. 1581 | this._boundsNeedUpdate = false; 1582 | }, 1583 | 1584 | //Set our markers position as given and add it to the map 1585 | _addToMap: function (startPos) { 1586 | if (startPos) { 1587 | this._backupLatlng = this._latlng; 1588 | this.setLatLng(startPos); 1589 | } 1590 | this._group._featureGroup.addLayer(this); 1591 | }, 1592 | 1593 | _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) { 1594 | this._recursively(bounds, this._group._map.getMinZoom(), maxZoom - 1, 1595 | function (c) { 1596 | var markers = c._markers, 1597 | i, m; 1598 | for (i = markers.length - 1; i >= 0; i--) { 1599 | m = markers[i]; 1600 | 1601 | //Only do it if the icon is still on the map 1602 | if (m._icon) { 1603 | m._setPos(center); 1604 | m.clusterHide(); 1605 | } 1606 | } 1607 | }, 1608 | function (c) { 1609 | var childClusters = c._childClusters, 1610 | j, cm; 1611 | for (j = childClusters.length - 1; j >= 0; j--) { 1612 | cm = childClusters[j]; 1613 | if (cm._icon) { 1614 | cm._setPos(center); 1615 | cm.clusterHide(); 1616 | } 1617 | } 1618 | } 1619 | ); 1620 | }, 1621 | 1622 | _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, mapMinZoom, previousZoomLevel, newZoomLevel) { 1623 | this._recursively(bounds, newZoomLevel, mapMinZoom, 1624 | function (c) { 1625 | c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel); 1626 | 1627 | //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be. 1628 | //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate 1629 | if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) { 1630 | c.clusterShow(); 1631 | c._recursivelyRemoveChildrenFromMap(bounds, mapMinZoom, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds 1632 | } else { 1633 | c.clusterHide(); 1634 | } 1635 | 1636 | c._addToMap(); 1637 | } 1638 | ); 1639 | }, 1640 | 1641 | _recursivelyBecomeVisible: function (bounds, zoomLevel) { 1642 | this._recursively(bounds, this._group._map.getMinZoom(), zoomLevel, null, function (c) { 1643 | c.clusterShow(); 1644 | }); 1645 | }, 1646 | 1647 | _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) { 1648 | this._recursively(bounds, this._group._map.getMinZoom() - 1, zoomLevel, 1649 | function (c) { 1650 | if (zoomLevel === c._zoom) { 1651 | return; 1652 | } 1653 | 1654 | //Add our child markers at startPos (so they can be animated out) 1655 | for (var i = c._markers.length - 1; i >= 0; i--) { 1656 | var nm = c._markers[i]; 1657 | 1658 | if (!bounds.contains(nm._latlng)) { 1659 | continue; 1660 | } 1661 | 1662 | if (startPos) { 1663 | nm._backupLatlng = nm.getLatLng(); 1664 | 1665 | nm.setLatLng(startPos); 1666 | if (nm.clusterHide) { 1667 | nm.clusterHide(); 1668 | } 1669 | } 1670 | 1671 | c._group._featureGroup.addLayer(nm); 1672 | } 1673 | }, 1674 | function (c) { 1675 | c._addToMap(startPos); 1676 | } 1677 | ); 1678 | }, 1679 | 1680 | _recursivelyRestoreChildPositions: function (zoomLevel) { 1681 | //Fix positions of child markers 1682 | for (var i = this._markers.length - 1; i >= 0; i--) { 1683 | var nm = this._markers[i]; 1684 | if (nm._backupLatlng) { 1685 | nm.setLatLng(nm._backupLatlng); 1686 | delete nm._backupLatlng; 1687 | } 1688 | } 1689 | 1690 | if (zoomLevel - 1 === this._zoom) { 1691 | //Reposition child clusters 1692 | for (var j = this._childClusters.length - 1; j >= 0; j--) { 1693 | this._childClusters[j]._restorePosition(); 1694 | } 1695 | } else { 1696 | for (var k = this._childClusters.length - 1; k >= 0; k--) { 1697 | this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel); 1698 | } 1699 | } 1700 | }, 1701 | 1702 | _restorePosition: function () { 1703 | if (this._backupLatlng) { 1704 | this.setLatLng(this._backupLatlng); 1705 | delete this._backupLatlng; 1706 | } 1707 | }, 1708 | 1709 | //exceptBounds: If set, don't remove any markers/clusters in it 1710 | _recursivelyRemoveChildrenFromMap: function (previousBounds, mapMinZoom, zoomLevel, exceptBounds) { 1711 | var m, i; 1712 | this._recursively(previousBounds, mapMinZoom - 1, zoomLevel - 1, 1713 | function (c) { 1714 | //Remove markers at every level 1715 | for (i = c._markers.length - 1; i >= 0; i--) { 1716 | m = c._markers[i]; 1717 | if (!exceptBounds || !exceptBounds.contains(m._latlng)) { 1718 | c._group._featureGroup.removeLayer(m); 1719 | if (m.clusterShow) { 1720 | m.clusterShow(); 1721 | } 1722 | } 1723 | } 1724 | }, 1725 | function (c) { 1726 | //Remove child clusters at just the bottom level 1727 | for (i = c._childClusters.length - 1; i >= 0; i--) { 1728 | m = c._childClusters[i]; 1729 | if (!exceptBounds || !exceptBounds.contains(m._latlng)) { 1730 | c._group._featureGroup.removeLayer(m); 1731 | if (m.clusterShow) { 1732 | m.clusterShow(); 1733 | } 1734 | } 1735 | } 1736 | } 1737 | ); 1738 | }, 1739 | 1740 | //Run the given functions recursively to this and child clusters 1741 | // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to 1742 | // zoomLevelToStart: zoom level to start running functions (inclusive) 1743 | // zoomLevelToStop: zoom level to stop running functions (inclusive) 1744 | // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level 1745 | // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level 1746 | _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) { 1747 | var childClusters = this._childClusters, 1748 | zoom = this._zoom, 1749 | i, c; 1750 | 1751 | if (zoomLevelToStart <= zoom) { 1752 | if (runAtEveryLevel) { 1753 | runAtEveryLevel(this); 1754 | } 1755 | if (runAtBottomLevel && zoom === zoomLevelToStop) { 1756 | runAtBottomLevel(this); 1757 | } 1758 | } 1759 | 1760 | if (zoom < zoomLevelToStart || zoom < zoomLevelToStop) { 1761 | for (i = childClusters.length - 1; i >= 0; i--) { 1762 | c = childClusters[i]; 1763 | if (boundsToApplyTo.intersects(c._bounds)) { 1764 | c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel); 1765 | } 1766 | } 1767 | } 1768 | }, 1769 | 1770 | //Returns true if we are the parent of only one cluster and that cluster is the same as us 1771 | _isSingleParent: function () { 1772 | //Don't need to check this._markers as the rest won't work if there are any 1773 | return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount; 1774 | } 1775 | }); 1776 | 1777 | 1778 | 1779 | /* 1780 | * Extends L.Marker to include two extra methods: clusterHide and clusterShow. 1781 | * 1782 | * They work as setOpacity(0) and setOpacity(1) respectively, but 1783 | * they will remember the marker's opacity when hiding and showing it again. 1784 | * 1785 | */ 1786 | 1787 | 1788 | L.Marker.include({ 1789 | 1790 | clusterHide: function () { 1791 | this.options.opacityWhenUnclustered = this.options.opacity || 1; 1792 | return this.setOpacity(0); 1793 | }, 1794 | 1795 | clusterShow: function () { 1796 | var ret = this.setOpacity(this.options.opacity || this.options.opacityWhenUnclustered); 1797 | delete this.options.opacityWhenUnclustered; 1798 | return ret; 1799 | } 1800 | 1801 | }); 1802 | 1803 | 1804 | 1805 | 1806 | 1807 | L.DistanceGrid = function (cellSize) { 1808 | this._cellSize = cellSize; 1809 | this._sqCellSize = cellSize * cellSize; 1810 | this._grid = {}; 1811 | this._objectPoint = { }; 1812 | }; 1813 | 1814 | L.DistanceGrid.prototype = { 1815 | 1816 | addObject: function (obj, point) { 1817 | var x = this._getCoord(point.x), 1818 | y = this._getCoord(point.y), 1819 | grid = this._grid, 1820 | row = grid[y] = grid[y] || {}, 1821 | cell = row[x] = row[x] || [], 1822 | stamp = L.Util.stamp(obj); 1823 | 1824 | this._objectPoint[stamp] = point; 1825 | 1826 | cell.push(obj); 1827 | }, 1828 | 1829 | updateObject: function (obj, point) { 1830 | this.removeObject(obj); 1831 | this.addObject(obj, point); 1832 | }, 1833 | 1834 | //Returns true if the object was found 1835 | removeObject: function (obj, point) { 1836 | var x = this._getCoord(point.x), 1837 | y = this._getCoord(point.y), 1838 | grid = this._grid, 1839 | row = grid[y] = grid[y] || {}, 1840 | cell = row[x] = row[x] || [], 1841 | i, len; 1842 | 1843 | delete this._objectPoint[L.Util.stamp(obj)]; 1844 | 1845 | for (i = 0, len = cell.length; i < len; i++) { 1846 | if (cell[i] === obj) { 1847 | 1848 | cell.splice(i, 1); 1849 | 1850 | if (len === 1) { 1851 | delete row[x]; 1852 | } 1853 | 1854 | return true; 1855 | } 1856 | } 1857 | 1858 | }, 1859 | 1860 | eachObject: function (fn, context) { 1861 | var i, j, k, len, row, cell, removed, 1862 | grid = this._grid; 1863 | 1864 | for (i in grid) { 1865 | row = grid[i]; 1866 | 1867 | for (j in row) { 1868 | cell = row[j]; 1869 | 1870 | for (k = 0, len = cell.length; k < len; k++) { 1871 | removed = fn.call(context, cell[k]); 1872 | if (removed) { 1873 | k--; 1874 | len--; 1875 | } 1876 | } 1877 | } 1878 | } 1879 | }, 1880 | 1881 | getNearObject: function (point) { 1882 | var x = this._getCoord(point.x), 1883 | y = this._getCoord(point.y), 1884 | i, j, k, row, cell, len, obj, dist, 1885 | objectPoint = this._objectPoint, 1886 | closestDistSq = this._sqCellSize, 1887 | closest = null; 1888 | 1889 | for (i = y - 1; i <= y + 1; i++) { 1890 | row = this._grid[i]; 1891 | if (row) { 1892 | 1893 | for (j = x - 1; j <= x + 1; j++) { 1894 | cell = row[j]; 1895 | if (cell) { 1896 | 1897 | for (k = 0, len = cell.length; k < len; k++) { 1898 | obj = cell[k]; 1899 | dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point); 1900 | if (dist < closestDistSq) { 1901 | closestDistSq = dist; 1902 | closest = obj; 1903 | } 1904 | } 1905 | } 1906 | } 1907 | } 1908 | } 1909 | return closest; 1910 | }, 1911 | 1912 | _getCoord: function (x) { 1913 | return Math.floor(x / this._cellSize); 1914 | }, 1915 | 1916 | _sqDist: function (p, p2) { 1917 | var dx = p2.x - p.x, 1918 | dy = p2.y - p.y; 1919 | return dx * dx + dy * dy; 1920 | } 1921 | }; 1922 | 1923 | 1924 | /* Copyright (c) 2012 the authors listed at the following URL, and/or 1925 | the authors of referenced articles or incorporated external code: 1926 | http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256 1927 | 1928 | Permission is hereby granted, free of charge, to any person obtaining 1929 | a copy of this software and associated documentation files (the 1930 | "Software"), to deal in the Software without restriction, including 1931 | without limitation the rights to use, copy, modify, merge, publish, 1932 | distribute, sublicense, and/or sell copies of the Software, and to 1933 | permit persons to whom the Software is furnished to do so, subject to 1934 | the following conditions: 1935 | 1936 | The above copyright notice and this permission notice shall be 1937 | included in all copies or substantial portions of the Software. 1938 | 1939 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 1940 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 1941 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 1942 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 1943 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 1944 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 1945 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 1946 | 1947 | Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434 1948 | */ 1949 | 1950 | (function () { 1951 | L.QuickHull = { 1952 | 1953 | /* 1954 | * @param {Object} cpt a point to be measured from the baseline 1955 | * @param {Array} bl the baseline, as represented by a two-element 1956 | * array of latlng objects. 1957 | * @returns {Number} an approximate distance measure 1958 | */ 1959 | getDistant: function (cpt, bl) { 1960 | var vY = bl[1].lat - bl[0].lat, 1961 | vX = bl[0].lng - bl[1].lng; 1962 | return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng)); 1963 | }, 1964 | 1965 | /* 1966 | * @param {Array} baseLine a two-element array of latlng objects 1967 | * representing the baseline to project from 1968 | * @param {Array} latLngs an array of latlng objects 1969 | * @returns {Object} the maximum point and all new points to stay 1970 | * in consideration for the hull. 1971 | */ 1972 | findMostDistantPointFromBaseLine: function (baseLine, latLngs) { 1973 | var maxD = 0, 1974 | maxPt = null, 1975 | newPoints = [], 1976 | i, pt, d; 1977 | 1978 | for (i = latLngs.length - 1; i >= 0; i--) { 1979 | pt = latLngs[i]; 1980 | d = this.getDistant(pt, baseLine); 1981 | 1982 | if (d > 0) { 1983 | newPoints.push(pt); 1984 | } else { 1985 | continue; 1986 | } 1987 | 1988 | if (d > maxD) { 1989 | maxD = d; 1990 | maxPt = pt; 1991 | } 1992 | } 1993 | 1994 | return { maxPoint: maxPt, newPoints: newPoints }; 1995 | }, 1996 | 1997 | 1998 | /* 1999 | * Given a baseline, compute the convex hull of latLngs as an array 2000 | * of latLngs. 2001 | * 2002 | * @param {Array} latLngs 2003 | * @returns {Array} 2004 | */ 2005 | buildConvexHull: function (baseLine, latLngs) { 2006 | var convexHullBaseLines = [], 2007 | t = this.findMostDistantPointFromBaseLine(baseLine, latLngs); 2008 | 2009 | if (t.maxPoint) { // if there is still a point "outside" the base line 2010 | convexHullBaseLines = 2011 | convexHullBaseLines.concat( 2012 | this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints) 2013 | ); 2014 | convexHullBaseLines = 2015 | convexHullBaseLines.concat( 2016 | this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints) 2017 | ); 2018 | return convexHullBaseLines; 2019 | } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull 2020 | return [baseLine[0]]; 2021 | } 2022 | }, 2023 | 2024 | /* 2025 | * Given an array of latlngs, compute a convex hull as an array 2026 | * of latlngs 2027 | * 2028 | * @param {Array} latLngs 2029 | * @returns {Array} 2030 | */ 2031 | getConvexHull: function (latLngs) { 2032 | // find first baseline 2033 | var maxLat = false, minLat = false, 2034 | maxLng = false, minLng = false, 2035 | maxLatPt = null, minLatPt = null, 2036 | maxLngPt = null, minLngPt = null, 2037 | maxPt = null, minPt = null, 2038 | i; 2039 | 2040 | for (i = latLngs.length - 1; i >= 0; i--) { 2041 | var pt = latLngs[i]; 2042 | if (maxLat === false || pt.lat > maxLat) { 2043 | maxLatPt = pt; 2044 | maxLat = pt.lat; 2045 | } 2046 | if (minLat === false || pt.lat < minLat) { 2047 | minLatPt = pt; 2048 | minLat = pt.lat; 2049 | } 2050 | if (maxLng === false || pt.lng > maxLng) { 2051 | maxLngPt = pt; 2052 | maxLng = pt.lng; 2053 | } 2054 | if (minLng === false || pt.lng < minLng) { 2055 | minLngPt = pt; 2056 | minLng = pt.lng; 2057 | } 2058 | } 2059 | 2060 | if (minLat !== maxLat) { 2061 | minPt = minLatPt; 2062 | maxPt = maxLatPt; 2063 | } else { 2064 | minPt = minLngPt; 2065 | maxPt = maxLngPt; 2066 | } 2067 | 2068 | var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs), 2069 | this.buildConvexHull([maxPt, minPt], latLngs)); 2070 | return ch; 2071 | } 2072 | }; 2073 | }()); 2074 | 2075 | L.MarkerCluster.include({ 2076 | getConvexHull: function () { 2077 | var childMarkers = this.getAllChildMarkers(), 2078 | points = [], 2079 | p, i; 2080 | 2081 | for (i = childMarkers.length - 1; i >= 0; i--) { 2082 | p = childMarkers[i].getLatLng(); 2083 | points.push(p); 2084 | } 2085 | 2086 | return L.QuickHull.getConvexHull(points); 2087 | } 2088 | }); 2089 | 2090 | 2091 | //This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet 2092 | //Huge thanks to jawj for implementing it first to make my job easy :-) 2093 | 2094 | L.MarkerCluster.include({ 2095 | 2096 | _2PI: Math.PI * 2, 2097 | _circleFootSeparation: 25, //related to circumference of circle 2098 | _circleStartAngle: Math.PI / 6, 2099 | 2100 | _spiralFootSeparation: 28, //related to size of spiral (experiment!) 2101 | _spiralLengthStart: 11, 2102 | _spiralLengthFactor: 5, 2103 | 2104 | _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards. 2105 | // 0 -> always spiral; Infinity -> always circle 2106 | 2107 | spiderfy: function () { 2108 | if (this._group._spiderfied === this || this._group._inZoomAnimation) { 2109 | return; 2110 | } 2111 | 2112 | var childMarkers = this.getAllChildMarkers(), 2113 | group = this._group, 2114 | map = group._map, 2115 | center = map.latLngToLayerPoint(this._latlng), 2116 | positions; 2117 | 2118 | this._group._unspiderfy(); 2119 | this._group._spiderfied = this; 2120 | 2121 | //TODO Maybe: childMarkers order by distance to center 2122 | 2123 | if (childMarkers.length >= this._circleSpiralSwitchover) { 2124 | positions = this._generatePointsSpiral(childMarkers.length, center); 2125 | } else { 2126 | center.y += 10; // Otherwise circles look wrong => hack for standard blue icon, renders differently for other icons. 2127 | positions = this._generatePointsCircle(childMarkers.length, center); 2128 | } 2129 | 2130 | this._animationSpiderfy(childMarkers, positions); 2131 | }, 2132 | 2133 | unspiderfy: function (zoomDetails) { 2134 | /// Argument from zoomanim if being called in a zoom animation or null otherwise 2135 | if (this._group._inZoomAnimation) { 2136 | return; 2137 | } 2138 | this._animationUnspiderfy(zoomDetails); 2139 | 2140 | this._group._spiderfied = null; 2141 | }, 2142 | 2143 | _generatePointsCircle: function (count, centerPt) { 2144 | var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count), 2145 | legLength = circumference / this._2PI, //radius from circumference 2146 | angleStep = this._2PI / count, 2147 | res = [], 2148 | i, angle; 2149 | 2150 | res.length = count; 2151 | 2152 | for (i = count - 1; i >= 0; i--) { 2153 | angle = this._circleStartAngle + i * angleStep; 2154 | res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); 2155 | } 2156 | 2157 | return res; 2158 | }, 2159 | 2160 | _generatePointsSpiral: function (count, centerPt) { 2161 | var spiderfyDistanceMultiplier = this._group.options.spiderfyDistanceMultiplier, 2162 | legLength = spiderfyDistanceMultiplier * this._spiralLengthStart, 2163 | separation = spiderfyDistanceMultiplier * this._spiralFootSeparation, 2164 | lengthFactor = spiderfyDistanceMultiplier * this._spiralLengthFactor * this._2PI, 2165 | angle = 0, 2166 | res = [], 2167 | i; 2168 | 2169 | res.length = count; 2170 | 2171 | // Higher index, closer position to cluster center. 2172 | for (i = count - 1; i >= 0; i--) { 2173 | angle += separation / legLength + i * 0.0005; 2174 | res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); 2175 | legLength += lengthFactor / angle; 2176 | } 2177 | return res; 2178 | }, 2179 | 2180 | _noanimationUnspiderfy: function () { 2181 | var group = this._group, 2182 | map = group._map, 2183 | fg = group._featureGroup, 2184 | childMarkers = this.getAllChildMarkers(), 2185 | m, i; 2186 | 2187 | group._ignoreMove = true; 2188 | 2189 | this.setOpacity(1); 2190 | for (i = childMarkers.length - 1; i >= 0; i--) { 2191 | m = childMarkers[i]; 2192 | 2193 | fg.removeLayer(m); 2194 | 2195 | if (m._preSpiderfyLatlng) { 2196 | m.setLatLng(m._preSpiderfyLatlng); 2197 | delete m._preSpiderfyLatlng; 2198 | } 2199 | if (m.setZIndexOffset) { 2200 | m.setZIndexOffset(0); 2201 | } 2202 | 2203 | if (m._spiderLeg) { 2204 | map.removeLayer(m._spiderLeg); 2205 | delete m._spiderLeg; 2206 | } 2207 | } 2208 | 2209 | group.fire('unspiderfied', { 2210 | cluster: this, 2211 | markers: childMarkers 2212 | }); 2213 | group._ignoreMove = false; 2214 | group._spiderfied = null; 2215 | } 2216 | }); 2217 | 2218 | //Non Animated versions of everything 2219 | L.MarkerClusterNonAnimated = L.MarkerCluster.extend({ 2220 | _animationSpiderfy: function (childMarkers, positions) { 2221 | var group = this._group, 2222 | map = group._map, 2223 | fg = group._featureGroup, 2224 | legOptions = this._group.options.spiderLegPolylineOptions, 2225 | i, m, leg, newPos; 2226 | 2227 | group._ignoreMove = true; 2228 | 2229 | // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition. 2230 | // The reverse order trick no longer improves performance on modern browsers. 2231 | for (i = 0; i < childMarkers.length; i++) { 2232 | newPos = map.layerPointToLatLng(positions[i]); 2233 | m = childMarkers[i]; 2234 | 2235 | // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it. 2236 | leg = new L.Polyline([this._latlng, newPos], legOptions); 2237 | map.addLayer(leg); 2238 | m._spiderLeg = leg; 2239 | 2240 | // Now add the marker. 2241 | m._preSpiderfyLatlng = m._latlng; 2242 | m.setLatLng(newPos); 2243 | if (m.setZIndexOffset) { 2244 | m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING 2245 | } 2246 | 2247 | fg.addLayer(m); 2248 | } 2249 | this.setOpacity(0.3); 2250 | 2251 | group._ignoreMove = false; 2252 | group.fire('spiderfied', { 2253 | cluster: this, 2254 | markers: childMarkers 2255 | }); 2256 | }, 2257 | 2258 | _animationUnspiderfy: function () { 2259 | this._noanimationUnspiderfy(); 2260 | } 2261 | }); 2262 | 2263 | //Animated versions here 2264 | L.MarkerCluster.include({ 2265 | 2266 | _animationSpiderfy: function (childMarkers, positions) { 2267 | var me = this, 2268 | group = this._group, 2269 | map = group._map, 2270 | fg = group._featureGroup, 2271 | thisLayerLatLng = this._latlng, 2272 | thisLayerPos = map.latLngToLayerPoint(thisLayerLatLng), 2273 | svg = L.Path.SVG, 2274 | legOptions = L.extend({}, this._group.options.spiderLegPolylineOptions), // Copy the options so that we can modify them for animation. 2275 | finalLegOpacity = legOptions.opacity, 2276 | i, m, leg, legPath, legLength, newPos; 2277 | 2278 | if (finalLegOpacity === undefined) { 2279 | finalLegOpacity = L.MarkerClusterGroup.prototype.options.spiderLegPolylineOptions.opacity; 2280 | } 2281 | 2282 | if (svg) { 2283 | // If the initial opacity of the spider leg is not 0 then it appears before the animation starts. 2284 | legOptions.opacity = 0; 2285 | 2286 | // Add the class for CSS transitions. 2287 | legOptions.className = (legOptions.className || '') + ' leaflet-cluster-spider-leg'; 2288 | } else { 2289 | // Make sure we have a defined opacity. 2290 | legOptions.opacity = finalLegOpacity; 2291 | } 2292 | 2293 | group._ignoreMove = true; 2294 | 2295 | // Add markers and spider legs to map, hidden at our center point. 2296 | // Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition. 2297 | // The reverse order trick no longer improves performance on modern browsers. 2298 | for (i = 0; i < childMarkers.length; i++) { 2299 | m = childMarkers[i]; 2300 | 2301 | newPos = map.layerPointToLatLng(positions[i]); 2302 | 2303 | // Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it. 2304 | leg = new L.Polyline([thisLayerLatLng, newPos], legOptions); 2305 | map.addLayer(leg); 2306 | m._spiderLeg = leg; 2307 | 2308 | // Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/ 2309 | // In our case the transition property is declared in the CSS file. 2310 | if (svg) { 2311 | legPath = leg._path; 2312 | legLength = legPath.getTotalLength() + 0.1; // Need a small extra length to avoid remaining dot in Firefox. 2313 | legPath.style.strokeDasharray = legLength; // Just 1 length is enough, it will be duplicated. 2314 | legPath.style.strokeDashoffset = legLength; 2315 | } 2316 | 2317 | // If it is a marker, add it now and we'll animate it out 2318 | if (m.setZIndexOffset) { 2319 | m.setZIndexOffset(1000000); // Make normal markers appear on top of EVERYTHING 2320 | } 2321 | if (m.clusterHide) { 2322 | m.clusterHide(); 2323 | } 2324 | 2325 | // Vectors just get immediately added 2326 | fg.addLayer(m); 2327 | 2328 | if (m._setPos) { 2329 | m._setPos(thisLayerPos); 2330 | } 2331 | } 2332 | 2333 | group._forceLayout(); 2334 | group._animationStart(); 2335 | 2336 | // Reveal markers and spider legs. 2337 | for (i = childMarkers.length - 1; i >= 0; i--) { 2338 | newPos = map.layerPointToLatLng(positions[i]); 2339 | m = childMarkers[i]; 2340 | 2341 | //Move marker to new position 2342 | m._preSpiderfyLatlng = m._latlng; 2343 | m.setLatLng(newPos); 2344 | 2345 | if (m.clusterShow) { 2346 | m.clusterShow(); 2347 | } 2348 | 2349 | // Animate leg (animation is actually delegated to CSS transition). 2350 | if (svg) { 2351 | leg = m._spiderLeg; 2352 | legPath = leg._path; 2353 | legPath.style.strokeDashoffset = 0; 2354 | //legPath.style.strokeOpacity = finalLegOpacity; 2355 | leg.setStyle({opacity: finalLegOpacity}); 2356 | } 2357 | } 2358 | this.setOpacity(0.3); 2359 | 2360 | group._ignoreMove = false; 2361 | 2362 | setTimeout(function () { 2363 | group._animationEnd(); 2364 | group.fire('spiderfied', { 2365 | cluster: me, 2366 | markers: childMarkers 2367 | }); 2368 | }, 200); 2369 | }, 2370 | 2371 | _animationUnspiderfy: function (zoomDetails) { 2372 | var me = this, 2373 | group = this._group, 2374 | map = group._map, 2375 | fg = group._featureGroup, 2376 | thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng), 2377 | childMarkers = this.getAllChildMarkers(), 2378 | svg = L.Path.SVG, 2379 | m, i, leg, legPath, legLength, nonAnimatable; 2380 | 2381 | group._ignoreMove = true; 2382 | group._animationStart(); 2383 | 2384 | //Make us visible and bring the child markers back in 2385 | this.setOpacity(1); 2386 | for (i = childMarkers.length - 1; i >= 0; i--) { 2387 | m = childMarkers[i]; 2388 | 2389 | //Marker was added to us after we were spiderfied 2390 | if (!m._preSpiderfyLatlng) { 2391 | continue; 2392 | } 2393 | 2394 | //Close any popup on the marker first, otherwise setting the location of the marker will make the map scroll 2395 | m.closePopup(); 2396 | 2397 | //Fix up the location to the real one 2398 | m.setLatLng(m._preSpiderfyLatlng); 2399 | delete m._preSpiderfyLatlng; 2400 | 2401 | //Hack override the location to be our center 2402 | nonAnimatable = true; 2403 | if (m._setPos) { 2404 | m._setPos(thisLayerPos); 2405 | nonAnimatable = false; 2406 | } 2407 | if (m.clusterHide) { 2408 | m.clusterHide(); 2409 | nonAnimatable = false; 2410 | } 2411 | if (nonAnimatable) { 2412 | fg.removeLayer(m); 2413 | } 2414 | 2415 | // Animate the spider leg back in (animation is actually delegated to CSS transition). 2416 | if (svg) { 2417 | leg = m._spiderLeg; 2418 | legPath = leg._path; 2419 | legLength = legPath.getTotalLength() + 0.1; 2420 | legPath.style.strokeDashoffset = legLength; 2421 | leg.setStyle({opacity: 0}); 2422 | } 2423 | } 2424 | 2425 | group._ignoreMove = false; 2426 | 2427 | setTimeout(function () { 2428 | //If we have only <= one child left then that marker will be shown on the map so don't remove it! 2429 | var stillThereChildCount = 0; 2430 | for (i = childMarkers.length - 1; i >= 0; i--) { 2431 | m = childMarkers[i]; 2432 | if (m._spiderLeg) { 2433 | stillThereChildCount++; 2434 | } 2435 | } 2436 | 2437 | 2438 | for (i = childMarkers.length - 1; i >= 0; i--) { 2439 | m = childMarkers[i]; 2440 | 2441 | if (!m._spiderLeg) { //Has already been unspiderfied 2442 | continue; 2443 | } 2444 | 2445 | if (m.clusterShow) { 2446 | m.clusterShow(); 2447 | } 2448 | if (m.setZIndexOffset) { 2449 | m.setZIndexOffset(0); 2450 | } 2451 | 2452 | if (stillThereChildCount > 1) { 2453 | fg.removeLayer(m); 2454 | } 2455 | 2456 | map.removeLayer(m._spiderLeg); 2457 | delete m._spiderLeg; 2458 | } 2459 | group._animationEnd(); 2460 | group.fire('unspiderfied', { 2461 | cluster: me, 2462 | markers: childMarkers 2463 | }); 2464 | }, 200); 2465 | } 2466 | }); 2467 | 2468 | 2469 | L.MarkerClusterGroup.include({ 2470 | //The MarkerCluster currently spiderfied (if any) 2471 | _spiderfied: null, 2472 | 2473 | unspiderfy: function () { 2474 | this._unspiderfy.apply(this, arguments); 2475 | }, 2476 | 2477 | _spiderfierOnAdd: function () { 2478 | this._map.on('click', this._unspiderfyWrapper, this); 2479 | 2480 | if (this._map.options.zoomAnimation) { 2481 | this._map.on('zoomstart', this._unspiderfyZoomStart, this); 2482 | } 2483 | //Browsers without zoomAnimation or a big zoom don't fire zoomstart 2484 | this._map.on('zoomend', this._noanimationUnspiderfy, this); 2485 | 2486 | if (!L.Browser.touch) { 2487 | this._map.getRenderer(this); 2488 | //Needs to happen in the pageload, not after, or animations don't work in webkit 2489 | // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements 2490 | //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable 2491 | } 2492 | }, 2493 | 2494 | _spiderfierOnRemove: function () { 2495 | this._map.off('click', this._unspiderfyWrapper, this); 2496 | this._map.off('zoomstart', this._unspiderfyZoomStart, this); 2497 | this._map.off('zoomanim', this._unspiderfyZoomAnim, this); 2498 | this._map.off('zoomend', this._noanimationUnspiderfy, this); 2499 | 2500 | //Ensure that markers are back where they should be 2501 | // Use no animation to avoid a sticky leaflet-cluster-anim class on mapPane 2502 | this._noanimationUnspiderfy(); 2503 | }, 2504 | 2505 | //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated) 2506 | //This means we can define the animation they do rather than Markers doing an animation to their actual location 2507 | _unspiderfyZoomStart: function () { 2508 | if (!this._map) { //May have been removed from the map by a zoomEnd handler 2509 | return; 2510 | } 2511 | 2512 | this._map.on('zoomanim', this._unspiderfyZoomAnim, this); 2513 | }, 2514 | 2515 | _unspiderfyZoomAnim: function (zoomDetails) { 2516 | //Wait until the first zoomanim after the user has finished touch-zooming before running the animation 2517 | if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) { 2518 | return; 2519 | } 2520 | 2521 | this._map.off('zoomanim', this._unspiderfyZoomAnim, this); 2522 | this._unspiderfy(zoomDetails); 2523 | }, 2524 | 2525 | _unspiderfyWrapper: function () { 2526 | /// _unspiderfy but passes no arguments 2527 | this._unspiderfy(); 2528 | }, 2529 | 2530 | _unspiderfy: function (zoomDetails) { 2531 | if (this._spiderfied) { 2532 | this._spiderfied.unspiderfy(zoomDetails); 2533 | } 2534 | }, 2535 | 2536 | _noanimationUnspiderfy: function () { 2537 | if (this._spiderfied) { 2538 | this._spiderfied._noanimationUnspiderfy(); 2539 | } 2540 | }, 2541 | 2542 | //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc 2543 | _unspiderfyLayer: function (layer) { 2544 | if (layer._spiderLeg) { 2545 | this._featureGroup.removeLayer(layer); 2546 | 2547 | if (layer.clusterShow) { 2548 | layer.clusterShow(); 2549 | } 2550 | //Position will be fixed up immediately in _animationUnspiderfy 2551 | if (layer.setZIndexOffset) { 2552 | layer.setZIndexOffset(0); 2553 | } 2554 | 2555 | this._map.removeLayer(layer._spiderLeg); 2556 | delete layer._spiderLeg; 2557 | } 2558 | } 2559 | }); 2560 | 2561 | 2562 | /** 2563 | * Adds 1 public method to MCG and 1 to L.Marker to facilitate changing 2564 | * markers' icon options and refreshing their icon and their parent clusters 2565 | * accordingly (case where their iconCreateFunction uses data of childMarkers 2566 | * to make up the cluster icon). 2567 | */ 2568 | 2569 | 2570 | L.MarkerClusterGroup.include({ 2571 | /** 2572 | * Updates the icon of all clusters which are parents of the given marker(s). 2573 | * In singleMarkerMode, also updates the given marker(s) icon. 2574 | * @param layers L.MarkerClusterGroup|L.LayerGroup|Array(L.Marker)|Map(L.Marker)| 2575 | * L.MarkerCluster|L.Marker (optional) list of markers (or single marker) whose parent 2576 | * clusters need to be updated. If not provided, retrieves all child markers of this. 2577 | * @returns {L.MarkerClusterGroup} 2578 | */ 2579 | refreshClusters: function (layers) { 2580 | if (!layers) { 2581 | layers = this._topClusterLevel.getAllChildMarkers(); 2582 | } else if (layers instanceof L.MarkerClusterGroup) { 2583 | layers = layers._topClusterLevel.getAllChildMarkers(); 2584 | } else if (layers instanceof L.LayerGroup) { 2585 | layers = layers._layers; 2586 | } else if (layers instanceof L.MarkerCluster) { 2587 | layers = layers.getAllChildMarkers(); 2588 | } else if (layers instanceof L.Marker) { 2589 | layers = [layers]; 2590 | } // else: must be an Array(L.Marker)|Map(L.Marker) 2591 | this._flagParentsIconsNeedUpdate(layers); 2592 | this._refreshClustersIcons(); 2593 | 2594 | // In case of singleMarkerMode, also re-draw the markers. 2595 | if (this.options.singleMarkerMode) { 2596 | this._refreshSingleMarkerModeMarkers(layers); 2597 | } 2598 | 2599 | return this; 2600 | }, 2601 | 2602 | /** 2603 | * Simply flags all parent clusters of the given markers as having a "dirty" icon. 2604 | * @param layers Array(L.Marker)|Map(L.Marker) list of markers. 2605 | * @private 2606 | */ 2607 | _flagParentsIconsNeedUpdate: function (layers) { 2608 | var id, parent; 2609 | 2610 | // Assumes layers is an Array or an Object whose prototype is non-enumerable. 2611 | for (id in layers) { 2612 | // Flag parent clusters' icon as "dirty", all the way up. 2613 | // Dumb process that flags multiple times upper parents, but still 2614 | // much more efficient than trying to be smart and make short lists, 2615 | // at least in the case of a hierarchy following a power law: 2616 | // http://jsperf.com/flag-nodes-in-power-hierarchy/2 2617 | parent = layers[id].__parent; 2618 | while (parent) { 2619 | parent._iconNeedsUpdate = true; 2620 | parent = parent.__parent; 2621 | } 2622 | } 2623 | }, 2624 | 2625 | /** 2626 | * Re-draws the icon of the supplied markers. 2627 | * To be used in singleMarkerMode only. 2628 | * @param layers Array(L.Marker)|Map(L.Marker) list of markers. 2629 | * @private 2630 | */ 2631 | _refreshSingleMarkerModeMarkers: function (layers) { 2632 | var id, layer; 2633 | 2634 | for (id in layers) { 2635 | layer = layers[id]; 2636 | 2637 | // Make sure we do not override markers that do not belong to THIS group. 2638 | if (this.hasLayer(layer)) { 2639 | // Need to re-create the icon first, then re-draw the marker. 2640 | layer.setIcon(this._overrideMarkerIcon(layer)); 2641 | } 2642 | } 2643 | } 2644 | }); 2645 | 2646 | L.Marker.include({ 2647 | /** 2648 | * Updates the given options in the marker's icon and refreshes the marker. 2649 | * @param options map object of icon options. 2650 | * @param directlyRefreshClusters boolean (optional) true to trigger 2651 | * MCG.refreshClustersOf() right away with this single marker. 2652 | * @returns {L.Marker} 2653 | */ 2654 | refreshIconOptions: function (options, directlyRefreshClusters) { 2655 | var icon = this.options.icon; 2656 | 2657 | L.setOptions(icon, options); 2658 | 2659 | this.setIcon(icon); 2660 | 2661 | // Shortcut to refresh the associated MCG clusters right away. 2662 | // To be used when refreshing a single marker. 2663 | // Otherwise, better use MCG.refreshClusters() once at the end with 2664 | // the list of modified markers. 2665 | if (directlyRefreshClusters && this.__parent) { 2666 | this.__parent._group.refreshClusters(this); 2667 | } 2668 | 2669 | return this; 2670 | } 2671 | }); 2672 | 2673 | 2674 | }(window, document)); --------------------------------------------------------------------------------