2017-02-04 16:46:44 +00:00
/ *
2020-04-06 03:03:19 +00:00
* Leaflet . markercluster 1.4 . 1 + master . 37 ab9a2 ,
* Provides Beautiful Animated Marker Clustering functionality for Leaflet , a JS library for interactive maps .
* https : //github.com/Leaflet/Leaflet.markercluster
* ( c ) 2012 - 2017 , Dave Leaver , smartrak
* /
( function ( global , factory ) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory ( exports ) :
typeof define === 'function' && define . amd ? define ( [ 'exports' ] , factory ) :
( factory ( ( global . Leaflet = global . Leaflet || { } , global . Leaflet . markercluster = global . Leaflet . markercluster || { } ) ) ) ;
} ( this , ( function ( exports ) { 'use strict' ;
/ *
2017-02-04 16:46:44 +00:00
* L . MarkerClusterGroup extends L . FeatureGroup by clustering the markers contained within
* /
2020-04-06 03:03:19 +00:00
var MarkerClusterGroup = L . MarkerClusterGroup = L . FeatureGroup . extend ( {
2017-02-04 16:46:44 +00:00
options : {
maxClusterRadius : 80 , //A cluster will cover at most this many pixels from its center
iconCreateFunction : null ,
2020-04-06 03:03:19 +00:00
clusterPane : L . Marker . prototype . options . pane ,
2017-02-04 16:46:44 +00:00
spiderfyOnMaxZoom : true ,
showCoverageOnHover : true ,
zoomToBoundsOnClick : true ,
singleMarkerMode : false ,
disableClusteringAtZoom : null ,
// Setting this to false prevents the removal of any clusters outside of the viewpoint, which
// is the default behaviour for performance reasons.
removeOutsideVisibleBounds : true ,
// Set to false to disable all animations (zoom and spiderfy).
// If false, option animateAddingMarkers below has no effect.
// If L.DomUtil.TRANSITION is falsy, this option has no effect.
animate : true ,
//Whether to animate adding markers after adding the MarkerClusterGroup to the map
// If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains.
animateAddingMarkers : false ,
//Increase to increase the distance away that spiderfied markers appear from the center
spiderfyDistanceMultiplier : 1 ,
// Make it possible to specify a polyline options on a spider leg
spiderLegPolylineOptions : { weight : 1.5 , color : '#222' , opacity : 0.5 } ,
// 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
chunkedLoading : false ,
chunkInterval : 200 , // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback)
chunkDelay : 50 , // at the end of each interval, give n milliseconds back to system/browser
chunkProgress : null , // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator)
//Options to pass to the L.Polygon constructor
polygonOptions : { }
} ,
initialize : function ( options ) {
L . Util . setOptions ( this , options ) ;
if ( ! 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 = [ ] ; //Markers removed while we aren't on the map need to be kept track of
//The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move
this . _currentShownBounds = null ;
this . _queue = [ ] ;
this . _childMarkerEventHandlers = {
'dragstart' : this . _childMarkerDragStart ,
'move' : this . _childMarkerMoved ,
'dragend' : this . _childMarkerDragEnd ,
} ;
// Hook the appropriate animation methods.
var animate = L . DomUtil . TRANSITION && this . options . animate ;
L . extend ( this , animate ? this . _withAnimation : this . _noAnimation ) ;
// Remember which MarkerCluster class to instantiate (animated or not).
this . _markerCluster = animate ? L . MarkerCluster : L . MarkerClusterNonAnimated ;
} ,
addLayer : function ( layer ) {
if ( layer instanceof L . LayerGroup ) {
return this . addLayers ( [ layer ] ) ;
}
//Don't cluster non point data
if ( ! layer . getLatLng ) {
this . _nonPointGroup . addLayer ( layer ) ;
this . fire ( 'layeradd' , { layer : layer } ) ;
return this ;
}
if ( ! this . _map ) {
this . _needsClustering . push ( layer ) ;
this . fire ( 'layeradd' , { layer : layer } ) ;
return this ;
}
if ( this . hasLayer ( layer ) ) {
return this ;
}
//If we have already clustered we'll need to add this one to a cluster
if ( this . _unspiderfy ) {
this . _unspiderfy ( ) ;
}
this . _addLayer ( layer , this . _maxZoom ) ;
this . fire ( 'layeradd' , { layer : layer } ) ;
// Refresh bounds and weighted positions.
this . _topClusterLevel . _recalculateBounds ( ) ;
this . _refreshClustersIcons ( ) ;
//Work out what is visible
var visibleLayer = layer ,
currentZoom = this . _zoom ;
if ( layer . _ _parent ) {
while ( visibleLayer . _ _parent . _zoom >= currentZoom ) {
visibleLayer = visibleLayer . _ _parent ;
}
}
if ( this . _currentShownBounds . contains ( visibleLayer . getLatLng ( ) ) ) {
if ( this . options . animateAddingMarkers ) {
this . _animationAddLayer ( layer , visibleLayer ) ;
} else {
this . _animationAddLayerNonAnimated ( layer , visibleLayer ) ;
}
}
return this ;
} ,
removeLayer : function ( layer ) {
if ( layer instanceof L . LayerGroup ) {
return this . removeLayers ( [ layer ] ) ;
}
//Non point layers
if ( ! layer . getLatLng ) {
this . _nonPointGroup . removeLayer ( layer ) ;
this . fire ( 'layerremove' , { layer : layer } ) ;
return this ;
}
if ( ! this . _map ) {
if ( ! this . _arraySplice ( this . _needsClustering , layer ) && this . hasLayer ( layer ) ) {
this . _needsRemoving . push ( { layer : layer , latlng : layer . _latlng } ) ;
}
this . fire ( 'layerremove' , { layer : layer } ) ;
return this ;
}
if ( ! layer . _ _parent ) {
return this ;
}
if ( this . _unspiderfy ) {
this . _unspiderfy ( ) ;
this . _unspiderfyLayer ( layer ) ;
}
//Remove the marker from clusters
this . _removeLayer ( layer , true ) ;
this . fire ( 'layerremove' , { layer : layer } ) ;
// Refresh bounds and weighted positions.
this . _topClusterLevel . _recalculateBounds ( ) ;
this . _refreshClustersIcons ( ) ;
layer . off ( this . _childMarkerEventHandlers , this ) ;
if ( this . _featureGroup . hasLayer ( layer ) ) {
this . _featureGroup . removeLayer ( layer ) ;
if ( layer . clusterShow ) {
layer . clusterShow ( ) ;
}
}
return this ;
} ,
//Takes an array of markers and adds them in bulk
addLayers : function ( layersArray , skipLayerAddEvent ) {
if ( ! L . Util . isArray ( layersArray ) ) {
return this . addLayer ( layersArray ) ;
}
var fg = this . _featureGroup ,
npg = this . _nonPointGroup ,
chunked = this . options . chunkedLoading ,
chunkInterval = this . options . chunkInterval ,
chunkProgress = this . options . chunkProgress ,
l = layersArray . length ,
offset = 0 ,
originalArray = true ,
m ;
if ( this . _map ) {
var started = ( new Date ( ) ) . getTime ( ) ;
var process = L . bind ( function ( ) {
var start = ( new Date ( ) ) . getTime ( ) ;
for ( ; offset < l ; offset ++ ) {
if ( chunked && offset % 200 === 0 ) {
// every couple hundred markers, instrument the time elapsed since processing started:
var elapsed = ( new Date ( ) ) . getTime ( ) - start ;
if ( elapsed > chunkInterval ) {
break ; // been working too hard, time to take a break :-)
}
}
m = layersArray [ offset ] ;
// Group of layers, append children to layersArray and skip.
// Side effects:
// - Total increases, so chunkProgress ratio jumps backward.
// - Groups are not included in this group, only their non-group child layers (hasLayer).
// Changing array length while looping does not affect performance in current browsers:
// http://jsperf.com/for-loop-changing-length/6
if ( m instanceof L . LayerGroup ) {
if ( originalArray ) {
layersArray = layersArray . slice ( ) ;
originalArray = false ;
}
this . _extractNonGroupLayers ( m , layersArray ) ;
l = layersArray . length ;
continue ;
}
//Not point data, can't be clustered
if ( ! m . getLatLng ) {
npg . addLayer ( m ) ;
if ( ! skipLayerAddEvent ) {
this . fire ( 'layeradd' , { layer : m } ) ;
}
continue ;
}
if ( this . hasLayer ( m ) ) {
continue ;
}
this . _addLayer ( m , this . _maxZoom ) ;
if ( ! skipLayerAddEvent ) {
this . fire ( 'layeradd' , { layer : m } ) ;
}
//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
if ( m . _ _parent ) {
if ( m . _ _parent . getChildCount ( ) === 2 ) {
var markers = m . _ _parent . getAllChildMarkers ( ) ,
otherMarker = markers [ 0 ] === m ? markers [ 1 ] : markers [ 0 ] ;
fg . removeLayer ( otherMarker ) ;
}
}
}
if ( chunkProgress ) {
// report progress and time elapsed:
chunkProgress ( offset , l , ( new Date ( ) ) . getTime ( ) - started ) ;
}
// Completed processing all markers.
if ( offset === l ) {
// Refresh bounds and weighted positions.
this . _topClusterLevel . _recalculateBounds ( ) ;
this . _refreshClustersIcons ( ) ;
this . _topClusterLevel . _recursivelyAddChildrenToMap ( null , this . _zoom , this . _currentShownBounds ) ;
} else {
setTimeout ( process , this . options . chunkDelay ) ;
}
} , this ) ;
process ( ) ;
} else {
var needsClustering = this . _needsClustering ;
for ( ; offset < l ; offset ++ ) {
m = layersArray [ offset ] ;
// Group of layers, append children to layersArray and skip.
if ( m instanceof L . LayerGroup ) {
if ( originalArray ) {
layersArray = layersArray . slice ( ) ;
originalArray = false ;
}
this . _extractNonGroupLayers ( m , layersArray ) ;
l = layersArray . length ;
continue ;
}
//Not point data, can't be clustered
if ( ! m . getLatLng ) {
npg . addLayer ( m ) ;
continue ;
}
if ( this . hasLayer ( m ) ) {
continue ;
}
needsClustering . push ( m ) ;
}
}
return this ;
} ,
//Takes an array of markers and removes them in bulk
removeLayers : function ( layersArray ) {
var i , m ,
l = layersArray . length ,
fg = this . _featureGroup ,
npg = this . _nonPointGroup ,
originalArray = true ;
if ( ! this . _map ) {
for ( i = 0 ; i < l ; i ++ ) {
m = layersArray [ i ] ;
// Group of layers, append children to layersArray and skip.
if ( m instanceof L . LayerGroup ) {
if ( originalArray ) {
layersArray = layersArray . slice ( ) ;
originalArray = false ;
}
this . _extractNonGroupLayers ( m , layersArray ) ;
l = layersArray . length ;
continue ;
}
this . _arraySplice ( this . _needsClustering , m ) ;
npg . removeLayer ( m ) ;
if ( this . hasLayer ( m ) ) {
this . _needsRemoving . push ( { layer : m , latlng : m . _latlng } ) ;
}
this . fire ( 'layerremove' , { layer : m } ) ;
}
return this ;
}
if ( this . _unspiderfy ) {
this . _unspiderfy ( ) ;
// Work on a copy of the array, so that next loop is not affected.
var layersArray2 = layersArray . slice ( ) ,
l2 = l ;
for ( i = 0 ; i < l2 ; i ++ ) {
m = layersArray2 [ i ] ;
// Group of layers, append children to layersArray and skip.
if ( m instanceof L . LayerGroup ) {
this . _extractNonGroupLayers ( m , layersArray2 ) ;
l2 = layersArray2 . length ;
continue ;
}
this . _unspiderfyLayer ( m ) ;
}
}
for ( i = 0 ; i < l ; i ++ ) {
m = layersArray [ i ] ;
// Group of layers, append children to layersArray and skip.
if ( m instanceof L . LayerGroup ) {
if ( originalArray ) {
layersArray = layersArray . slice ( ) ;
originalArray = false ;
}
this . _extractNonGroupLayers ( m , layersArray ) ;
l = layersArray . length ;
continue ;
}
if ( ! m . _ _parent ) {
npg . removeLayer ( m ) ;
this . fire ( 'layerremove' , { layer : m } ) ;
continue ;
}
this . _removeLayer ( m , true , true ) ;
this . fire ( 'layerremove' , { layer : m } ) ;
if ( fg . hasLayer ( m ) ) {
fg . removeLayer ( m ) ;
if ( m . clusterShow ) {
m . clusterShow ( ) ;
}
}
}
// Refresh bounds and weighted positions.
this . _topClusterLevel . _recalculateBounds ( ) ;
this . _refreshClustersIcons ( ) ;
//Fix up the clusters and markers on the map
this . _topClusterLevel . _recursivelyAddChildrenToMap ( null , this . _zoom , this . _currentShownBounds ) ;
return this ;
} ,
//Removes all layers from the MarkerClusterGroup
clearLayers : function ( ) {
//Need our own special implementation as the LayerGroup one doesn't work for us
//If we aren't on the map (yet), blow away the markers we know of
if ( ! this . _map ) {
this . _needsClustering = [ ] ;
2020-04-06 03:03:19 +00:00
this . _needsRemoving = [ ] ;
2017-02-04 16:46:44 +00:00
delete this . _gridClusters ;
delete this . _gridUnclustered ;
}
if ( this . _noanimationUnspiderfy ) {
this . _noanimationUnspiderfy ( ) ;
}
//Remove all the visible layers
this . _featureGroup . clearLayers ( ) ;
this . _nonPointGroup . clearLayers ( ) ;
this . eachLayer ( function ( marker ) {
marker . off ( this . _childMarkerEventHandlers , this ) ;
delete marker . _ _parent ;
} , this ) ;
if ( this . _map ) {
//Reset _topClusterLevel and the DistanceGrids
this . _generateInitialClusters ( ) ;
}
return this ;
} ,
//Override FeatureGroup.getBounds as it doesn't work
getBounds : function ( ) {
var bounds = new L . LatLngBounds ( ) ;
if ( this . _topClusterLevel ) {
bounds . extend ( this . _topClusterLevel . _bounds ) ;
}
for ( var i = this . _needsClustering . length - 1 ; i >= 0 ; i -- ) {
bounds . extend ( this . _needsClustering [ i ] . getLatLng ( ) ) ;
}
bounds . extend ( this . _nonPointGroup . getBounds ( ) ) ;
return bounds ;
} ,
//Overrides LayerGroup.eachLayer
eachLayer : function ( method , context ) {
var markers = this . _needsClustering . slice ( ) ,
needsRemoving = this . _needsRemoving ,
thisNeedsRemoving , i , j ;
if ( this . _topClusterLevel ) {
this . _topClusterLevel . getAllChildMarkers ( markers ) ;
}
for ( i = markers . length - 1 ; i >= 0 ; i -- ) {
thisNeedsRemoving = true ;
for ( j = needsRemoving . length - 1 ; j >= 0 ; j -- ) {
if ( needsRemoving [ j ] . layer === markers [ i ] ) {
thisNeedsRemoving = false ;
break ;
}
}
if ( thisNeedsRemoving ) {
method . call ( context , markers [ i ] ) ;
}
}
this . _nonPointGroup . eachLayer ( method , context ) ;
} ,
//Overrides LayerGroup.getLayers
getLayers : function ( ) {
var layers = [ ] ;
this . eachLayer ( function ( l ) {
layers . push ( l ) ;
} ) ;
return layers ;
} ,
//Overrides LayerGroup.getLayer, WARNING: Really bad performance
getLayer : function ( id ) {
var result = null ;
2020-04-06 03:03:19 +00:00
2017-02-04 16:46:44 +00:00
id = parseInt ( id , 10 ) ;
this . eachLayer ( function ( l ) {
if ( L . stamp ( l ) === id ) {
result = l ;
}
} ) ;
return result ;
} ,
//Returns true if the given layer is in this MarkerClusterGroup
hasLayer : function ( layer ) {
if ( ! layer ) {
return false ;
}
var i , anArray = this . _needsClustering ;
for ( i = anArray . length - 1 ; i >= 0 ; i -- ) {
if ( anArray [ i ] === layer ) {
return true ;
}
}
anArray = this . _needsRemoving ;
for ( i = anArray . length - 1 ; i >= 0 ; i -- ) {
if ( anArray [ i ] . layer === layer ) {
return false ;
}
}
return ! ! ( layer . _ _parent && layer . _ _parent . _group === this ) || this . _nonPointGroup . hasLayer ( layer ) ;
} ,
//Zoom down to show the given layer (spiderfying if necessary) then calls the callback
zoomToShowLayer : function ( layer , callback ) {
if ( typeof callback !== 'function' ) {
callback = function ( ) { } ;
}
var showMarker = function ( ) {
if ( ( layer . _icon || layer . _ _parent . _icon ) && ! this . _inZoomAnimation ) {
this . _map . off ( 'moveend' , showMarker , this ) ;
this . off ( 'animationend' , showMarker , this ) ;
if ( layer . _icon ) {
callback ( ) ;
} else if ( layer . _ _parent . _icon ) {
this . once ( 'spiderfied' , callback , this ) ;
layer . _ _parent . spiderfy ( ) ;
}
}
} ;
if ( layer . _icon && this . _map . getBounds ( ) . contains ( layer . getLatLng ( ) ) ) {
//Layer is visible ond on screen, immediate return
callback ( ) ;
} else if ( layer . _ _parent . _zoom < Math . round ( this . _map . _zoom ) ) {
//Layer should be visible at this zoom level. It must not be on screen so just pan over to it
this . _map . on ( 'moveend' , showMarker , this ) ;
this . _map . panTo ( layer . getLatLng ( ) ) ;
} else {
this . _map . on ( 'moveend' , showMarker , this ) ;
this . on ( 'animationend' , showMarker , this ) ;
layer . _ _parent . zoomToBounds ( ) ;
}
} ,
//Overrides FeatureGroup.onAdd
onAdd : function ( map ) {
this . _map = map ;
var i , l , layer ;
if ( ! isFinite ( this . _map . getMaxZoom ( ) ) ) {
throw "Map has no maxZoom specified" ;
}
this . _featureGroup . addTo ( map ) ;
this . _nonPointGroup . addTo ( map ) ;
if ( ! this . _gridClusters ) {
this . _generateInitialClusters ( ) ;
}
this . _maxLat = map . options . crs . projection . MAX _LATITUDE ;
//Restore all the positions as they are in the MCG before removing them
for ( i = 0 , l = this . _needsRemoving . length ; i < l ; i ++ ) {
layer = this . _needsRemoving [ i ] ;
layer . newlatlng = layer . layer . _latlng ;
layer . layer . _latlng = layer . latlng ;
}
//Remove them, then restore their new positions
for ( i = 0 , l = this . _needsRemoving . length ; i < l ; i ++ ) {
layer = this . _needsRemoving [ i ] ;
this . _removeLayer ( layer . layer , true ) ;
layer . layer . _latlng = layer . newlatlng ;
}
this . _needsRemoving = [ ] ;
//Remember the current zoom level and bounds
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 ) ;
if ( this . _spiderfierOnAdd ) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
this . _spiderfierOnAdd ( ) ;
}
this . _bindEvents ( ) ;
//Actually add our markers to the map:
l = this . _needsClustering ;
this . _needsClustering = [ ] ;
this . addLayers ( l , true ) ;
} ,
//Overrides FeatureGroup.onRemove
onRemove : function ( map ) {
map . off ( 'zoomend' , this . _zoomEnd , this ) ;
map . off ( 'moveend' , this . _moveEnd , this ) ;
this . _unbindEvents ( ) ;
//In case we are in a cluster animation
this . _map . _mapPane . className = this . _map . _mapPane . className . replace ( ' leaflet-cluster-anim' , '' ) ;
if ( this . _spiderfierOnRemove ) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely
this . _spiderfierOnRemove ( ) ;
}
delete this . _maxLat ;
//Clean up all the layers we added to the map
this . _hideCoverage ( ) ;
this . _featureGroup . remove ( ) ;
this . _nonPointGroup . remove ( ) ;
this . _featureGroup . clearLayers ( ) ;
this . _map = null ;
} ,
getVisibleParent : function ( marker ) {
var vMarker = marker ;
while ( vMarker && ! vMarker . _icon ) {
vMarker = vMarker . _ _parent ;
}
return vMarker || null ;
} ,
//Remove the given object from the given array
_arraySplice : function ( anArray , obj ) {
for ( var i = anArray . length - 1 ; i >= 0 ; i -- ) {
if ( anArray [ i ] === obj ) {
anArray . splice ( i , 1 ) ;
return true ;
}
}
} ,
/ * *
* Removes a marker from all _gridUnclustered zoom levels , starting at the supplied zoom .
* @ param marker to be removed from _gridUnclustered .
* @ param z integer bottom start zoom level ( included )
* @ private
* /
_removeFromGridUnclustered : function ( marker , z ) {
var map = this . _map ,
gridUnclustered = this . _gridUnclustered ,
2020-04-06 03:03:19 +00:00
minZoom = Math . floor ( this . _map . getMinZoom ( ) ) ;
2017-02-04 16:46:44 +00:00
for ( ; z >= minZoom ; z -- ) {
if ( ! gridUnclustered [ z ] . removeObject ( marker , map . project ( marker . getLatLng ( ) , z ) ) ) {
break ;
}
}
} ,
_childMarkerDragStart : function ( e ) {
e . target . _ _dragStart = e . target . _latlng ;
} ,
_childMarkerMoved : function ( e ) {
if ( ! this . _ignoreMove && ! e . target . _ _dragStart ) {
var isPopupOpen = e . target . _popup && e . target . _popup . isOpen ( ) ;
this . _moveChild ( e . target , e . oldLatLng , e . latlng ) ;
if ( isPopupOpen ) {
e . target . openPopup ( ) ;
}
}
} ,
_moveChild : function ( layer , from , to ) {
layer . _latlng = from ;
this . removeLayer ( layer ) ;
layer . _latlng = to ;
this . addLayer ( layer ) ;
} ,
_childMarkerDragEnd : function ( e ) {
2020-04-06 03:03:19 +00:00
var dragStart = e . target . _ _dragStart ;
2017-02-04 16:46:44 +00:00
delete e . target . _ _dragStart ;
2020-04-06 03:03:19 +00:00
if ( dragStart ) {
this . _moveChild ( e . target , dragStart , e . target . _latlng ) ;
}
2017-02-04 16:46:44 +00:00
} ,
2020-04-06 03:03:19 +00:00
2017-02-04 16:46:44 +00:00
//Internal function for removing a marker from everything.
//dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions)
_removeLayer : function ( marker , removeFromDistanceGrid , dontUpdateMap ) {
var gridClusters = this . _gridClusters ,
gridUnclustered = this . _gridUnclustered ,
fg = this . _featureGroup ,
map = this . _map ,
2020-04-06 03:03:19 +00:00
minZoom = Math . floor ( this . _map . getMinZoom ( ) ) ;
2017-02-04 16:46:44 +00:00
//Remove the marker from distance clusters it might be in
if ( removeFromDistanceGrid ) {
this . _removeFromGridUnclustered ( marker , this . _maxZoom ) ;
}
//Work our way up the clusters removing them as we go if required
var cluster = marker . _ _parent ,
markers = cluster . _markers ,
otherMarker ;
//Remove the marker from the immediate parents marker list
this . _arraySplice ( markers , marker ) ;
while ( cluster ) {
cluster . _childCount -- ;
cluster . _boundsNeedUpdate = true ;
if ( cluster . _zoom < minZoom ) {
//Top level, do nothing
break ;
} else if ( removeFromDistanceGrid && cluster . _childCount <= 1 ) { //Cluster no longer required
//We need to push the other marker up to the parent
otherMarker = cluster . _markers [ 0 ] === marker ? cluster . _markers [ 1 ] : cluster . _markers [ 0 ] ;
//Update distance grid
gridClusters [ cluster . _zoom ] . removeObject ( cluster , map . project ( cluster . _cLatLng , cluster . _zoom ) ) ;
gridUnclustered [ cluster . _zoom ] . addObject ( otherMarker , map . project ( otherMarker . getLatLng ( ) , cluster . _zoom ) ) ;
//Move otherMarker up to parent
this . _arraySplice ( cluster . _ _parent . _childClusters , cluster ) ;
cluster . _ _parent . _markers . push ( otherMarker ) ;
otherMarker . _ _parent = cluster . _ _parent ;
if ( cluster . _icon ) {
//Cluster is currently on the map, need to put the marker on the map instead
fg . removeLayer ( cluster ) ;
if ( ! dontUpdateMap ) {
fg . addLayer ( otherMarker ) ;
}
}
} else {
cluster . _iconNeedsUpdate = true ;
}
cluster = cluster . _ _parent ;
}
delete marker . _ _parent ;
} ,
_isOrIsParent : function ( el , oel ) {
while ( oel ) {
if ( el === oel ) {
return true ;
}
oel = oel . parentNode ;
}
return false ;
} ,
//Override L.Evented.fire
fire : function ( type , data , propagate ) {
if ( data && data . layer instanceof L . MarkerCluster ) {
//Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget)
if ( data . originalEvent && this . _isOrIsParent ( data . layer . _icon , data . originalEvent . relatedTarget ) ) {
return ;
}
type = 'cluster' + type ;
}
L . FeatureGroup . prototype . fire . call ( this , type , data , propagate ) ;
} ,
//Override L.Evented.listens
listens : function ( type , propagate ) {
return L . FeatureGroup . prototype . listens . call ( this , type , propagate ) || L . FeatureGroup . prototype . listens . call ( this , 'cluster' + type , propagate ) ;
} ,
//Default functionality
_defaultIconCreateFunction : function ( cluster ) {
var childCount = cluster . getChildCount ( ) ;
var c = ' marker-cluster-' ;
if ( childCount < 10 ) {
c += 'small' ;
} else if ( childCount < 100 ) {
c += 'medium' ;
} else {
c += 'large' ;
}
return new L . DivIcon ( { html : '<div><span>' + childCount + '</span></div>' , className : 'marker-cluster' + c , iconSize : new L . Point ( 40 , 40 ) } ) ;
} ,
_bindEvents : function ( ) {
var map = this . _map ,
spiderfyOnMaxZoom = this . options . spiderfyOnMaxZoom ,
showCoverageOnHover = this . options . showCoverageOnHover ,
zoomToBoundsOnClick = this . options . zoomToBoundsOnClick ;
//Zoom on cluster click or spiderfy if we are at the lowest level
if ( spiderfyOnMaxZoom || zoomToBoundsOnClick ) {
this . on ( 'clusterclick' , this . _zoomOrSpiderfy , this ) ;
}
//Show convex hull (boundary) polygon on mouse over
if ( showCoverageOnHover ) {
this . on ( 'clustermouseover' , this . _showCoverage , this ) ;
this . on ( 'clustermouseout' , this . _hideCoverage , this ) ;
map . on ( 'zoomend' , this . _hideCoverage , this ) ;
}
} ,
_zoomOrSpiderfy : function ( e ) {
var cluster = e . layer ,
bottomCluster = cluster ;
while ( bottomCluster . _childClusters . length === 1 ) {
bottomCluster = bottomCluster . _childClusters [ 0 ] ;
}
if ( bottomCluster . _zoom === this . _maxZoom &&
bottomCluster . _childCount === cluster . _childCount &&
this . options . spiderfyOnMaxZoom ) {
// All child markers are contained in a single cluster from this._maxZoom to this cluster.
cluster . spiderfy ( ) ;
} else if ( this . options . zoomToBoundsOnClick ) {
cluster . zoomToBounds ( ) ;
}
// Focus the map again for keyboard users.
if ( e . originalEvent && e . originalEvent . keyCode === 13 ) {
this . _map . _container . focus ( ) ;
}
} ,
_showCoverage : function ( e ) {
var map = this . _map ;
if ( this . _inZoomAnimation ) {
return ;
}
if ( this . _shownPolygon ) {
map . removeLayer ( this . _shownPolygon ) ;
}
if ( e . layer . getChildCount ( ) > 2 && e . layer !== this . _spiderfied ) {
this . _shownPolygon = new L . Polygon ( e . layer . getConvexHull ( ) , this . options . polygonOptions ) ;
map . addLayer ( this . _shownPolygon ) ;
}
} ,
_hideCoverage : function ( ) {
if ( this . _shownPolygon ) {
this . _map . removeLayer ( this . _shownPolygon ) ;
this . _shownPolygon = null ;
}
} ,
_unbindEvents : function ( ) {
var spiderfyOnMaxZoom = this . options . spiderfyOnMaxZoom ,
showCoverageOnHover = this . options . showCoverageOnHover ,
zoomToBoundsOnClick = this . options . zoomToBoundsOnClick ,
map = this . _map ;
if ( spiderfyOnMaxZoom || zoomToBoundsOnClick ) {
this . off ( 'clusterclick' , this . _zoomOrSpiderfy , this ) ;
}
if ( showCoverageOnHover ) {
this . off ( 'clustermouseover' , this . _showCoverage , this ) ;
this . off ( 'clustermouseout' , this . _hideCoverage , this ) ;
map . off ( 'zoomend' , this . _hideCoverage , this ) ;
}
} ,
_zoomEnd : function ( ) {
if ( ! this . _map ) { //May have been removed from the map by a zoomEnd handler
return ;
}
this . _mergeSplitClusters ( ) ;
this . _zoom = Math . round ( this . _map . _zoom ) ;
this . _currentShownBounds = this . _getExpandedVisibleBounds ( ) ;
} ,
_moveEnd : function ( ) {
if ( this . _inZoomAnimation ) {
return ;
}
var newBounds = this . _getExpandedVisibleBounds ( ) ;
2020-04-06 03:03:19 +00:00
this . _topClusterLevel . _recursivelyRemoveChildrenFromMap ( this . _currentShownBounds , Math . floor ( this . _map . getMinZoom ( ) ) , this . _zoom , newBounds ) ;
2017-02-04 16:46:44 +00:00
this . _topClusterLevel . _recursivelyAddChildrenToMap ( null , Math . round ( this . _map . _zoom ) , newBounds ) ;
this . _currentShownBounds = newBounds ;
return ;
} ,
_generateInitialClusters : function ( ) {
2020-04-06 03:03:19 +00:00
var maxZoom = Math . ceil ( this . _map . getMaxZoom ( ) ) ,
minZoom = Math . floor ( this . _map . getMinZoom ( ) ) ,
2017-02-04 16:46:44 +00:00
radius = this . options . maxClusterRadius ,
radiusFn = radius ;
2020-04-06 03:03:19 +00:00
2017-02-04 16:46:44 +00:00
//If we just set maxClusterRadius to a single number, we need to create
//a simple function to return that number. Otherwise, we just have to
//use the function we've passed in.
if ( typeof radius !== "function" ) {
radiusFn = function ( ) { return radius ; } ;
}
2020-04-06 03:03:19 +00:00
if ( this . options . disableClusteringAtZoom !== null ) {
2017-02-04 16:46:44 +00:00
maxZoom = this . options . disableClusteringAtZoom - 1 ;
}
this . _maxZoom = maxZoom ;
this . _gridClusters = { } ;
this . _gridUnclustered = { } ;
2020-04-06 03:03:19 +00:00
2017-02-04 16:46:44 +00:00
//Set up DistanceGrids for each zoom
for ( var zoom = maxZoom ; zoom >= minZoom ; zoom -- ) {
this . _gridClusters [ zoom ] = new L . DistanceGrid ( radiusFn ( zoom ) ) ;
this . _gridUnclustered [ zoom ] = new L . DistanceGrid ( radiusFn ( zoom ) ) ;
}
// Instantiate the appropriate L.MarkerCluster class (animated or not).
this . _topClusterLevel = new this . _markerCluster ( this , minZoom - 1 ) ;
} ,
//Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom)
_addLayer : function ( layer , zoom ) {
var gridClusters = this . _gridClusters ,
gridUnclustered = this . _gridUnclustered ,
2020-04-06 03:03:19 +00:00
minZoom = Math . floor ( this . _map . getMinZoom ( ) ) ,
2017-02-04 16:46:44 +00:00
markerPoint , z ;
if ( this . options . singleMarkerMode ) {
this . _overrideMarkerIcon ( layer ) ;
}
layer . on ( this . _childMarkerEventHandlers , this ) ;
//Find the lowest zoom level to slot this one in
for ( ; zoom >= minZoom ; zoom -- ) {
markerPoint = this . _map . project ( layer . getLatLng ( ) , zoom ) ; // calculate pixel position
//Try find a cluster close by
var closest = gridClusters [ zoom ] . getNearObject ( markerPoint ) ;
if ( closest ) {
closest . _addChild ( layer ) ;
layer . _ _parent = closest ;
return ;
}
//Try find a marker close by to form a new cluster with
closest = gridUnclustered [ zoom ] . getNearObject ( markerPoint ) ;
if ( closest ) {
var parent = closest . _ _parent ;
if ( parent ) {
this . _removeLayer ( closest , false ) ;
}
//Create new cluster with these 2 in it
var newCluster = new this . _markerCluster ( this , zoom , closest , layer ) ;
gridClusters [ zoom ] . addObject ( newCluster , this . _map . project ( newCluster . _cLatLng , zoom ) ) ;
closest . _ _parent = newCluster ;
layer . _ _parent = newCluster ;
//First create any new intermediate parent clusters that don't exist
var lastParent = newCluster ;
for ( z = zoom - 1 ; z > parent . _zoom ; z -- ) {
lastParent = new this . _markerCluster ( this , z , lastParent ) ;
gridClusters [ z ] . addObject ( lastParent , this . _map . project ( closest . getLatLng ( ) , z ) ) ;
}
parent . _addChild ( lastParent ) ;
//Remove closest from this zoom level and any above that it is in, replace with newCluster
this . _removeFromGridUnclustered ( closest , zoom ) ;
return ;
}
//Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards
gridUnclustered [ zoom ] . addObject ( layer , markerPoint ) ;
}
//Didn't get in anything, add us to the top
this . _topClusterLevel . _addChild ( layer ) ;
layer . _ _parent = this . _topClusterLevel ;
return ;
} ,
/ * *
* Refreshes the icon of all "dirty" visible clusters .
* Non - visible "dirty" clusters will be updated when they are added to the map .
* @ private
* /
_refreshClustersIcons : function ( ) {
this . _featureGroup . eachLayer ( function ( c ) {
if ( c instanceof L . MarkerCluster && c . _iconNeedsUpdate ) {
c . _updateIcon ( ) ;
}
} ) ;
} ,
//Enqueue code to fire after the marker expand/contract has happened
_enqueue : function ( fn ) {
this . _queue . push ( fn ) ;
if ( ! this . _queueTimeout ) {
this . _queueTimeout = setTimeout ( L . bind ( this . _processQueue , this ) , 300 ) ;
}
} ,
_processQueue : function ( ) {
for ( var i = 0 ; i < this . _queue . length ; i ++ ) {
this . _queue [ i ] . call ( this ) ;
}
this . _queue . length = 0 ;
clearTimeout ( this . _queueTimeout ) ;
this . _queueTimeout = null ;
} ,
//Merge and split any existing clusters that are too big or small
_mergeSplitClusters : function ( ) {
var mapZoom = Math . round ( this . _map . _zoom ) ;
//In case we are starting to split before the animation finished
this . _processQueue ( ) ;
if ( this . _zoom < mapZoom && this . _currentShownBounds . intersects ( this . _getExpandedVisibleBounds ( ) ) ) { //Zoom in, split
this . _animationStart ( ) ;
//Remove clusters now off screen
2020-04-06 03:03:19 +00:00
this . _topClusterLevel . _recursivelyRemoveChildrenFromMap ( this . _currentShownBounds , Math . floor ( this . _map . getMinZoom ( ) ) , this . _zoom , this . _getExpandedVisibleBounds ( ) ) ;
2017-02-04 16:46:44 +00:00
this . _animationZoomIn ( this . _zoom , mapZoom ) ;
} else if ( this . _zoom > mapZoom ) { //Zoom out, merge
this . _animationStart ( ) ;
this . _animationZoomOut ( this . _zoom , mapZoom ) ;
} else {
this . _moveEnd ( ) ;
}
} ,
//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)
_getExpandedVisibleBounds : function ( ) {
if ( ! this . options . removeOutsideVisibleBounds ) {
return this . _mapBoundsInfinite ;
} else if ( L . Browser . mobile ) {
return this . _checkBoundsMaxLat ( this . _map . getBounds ( ) ) ;
}
return this . _checkBoundsMaxLat ( this . _map . getBounds ( ) . pad ( 1 ) ) ; // Padding expands the bounds by its own dimensions but scaled with the given factor.
} ,
/ * *
* Expands the latitude to Infinity ( or - Infinity ) if the input bounds reach the map projection maximum defined latitude
* ( in the case of Web / Spherical Mercator , it is 85.0511287798 / see https : //en.wikipedia.org/wiki/Web_Mercator#Formulas).
* Otherwise , the removeOutsideVisibleBounds option will remove markers beyond that limit , whereas the same markers without
* this option ( or outside MCG ) will have their position floored ( ceiled ) by the projection and rendered at that limit ,
* making the user think that MCG "eats" them and never displays them again .
* @ param bounds L . LatLngBounds
* @ returns { L . LatLngBounds }
* @ private
* /
_checkBoundsMaxLat : function ( bounds ) {
var maxLat = this . _maxLat ;
if ( maxLat !== undefined ) {
if ( bounds . getNorth ( ) >= maxLat ) {
bounds . _northEast . lat = Infinity ;
}
if ( bounds . getSouth ( ) <= - maxLat ) {
bounds . _southWest . lat = - Infinity ;
}
}
return bounds ;
} ,
//Shared animation code
_animationAddLayerNonAnimated : function ( layer , newCluster ) {
if ( newCluster === layer ) {
this . _featureGroup . addLayer ( layer ) ;
} else if ( newCluster . _childCount === 2 ) {
newCluster . _addToMap ( ) ;
var markers = newCluster . getAllChildMarkers ( ) ;
this . _featureGroup . removeLayer ( markers [ 0 ] ) ;
this . _featureGroup . removeLayer ( markers [ 1 ] ) ;
} else {
newCluster . _updateIcon ( ) ;
}
} ,
/ * *
* Extracts individual ( i . e . non - group ) layers from a Layer Group .
* @ param group to extract layers from .
* @ param output { Array } in which to store the extracted layers .
* @ returns { * | Array }
* @ private
* /
_extractNonGroupLayers : function ( group , output ) {
var layers = group . getLayers ( ) ,
i = 0 ,
layer ;
output = output || [ ] ;
for ( ; i < layers . length ; i ++ ) {
layer = layers [ i ] ;
if ( layer instanceof L . LayerGroup ) {
this . _extractNonGroupLayers ( layer , output ) ;
continue ;
}
output . push ( layer ) ;
}
return output ;
} ,
/ * *
* Implements the singleMarkerMode option .
* @ param layer Marker to re - style using the Clusters iconCreateFunction .
* @ returns { L . Icon } The newly created icon .
* @ private
* /
_overrideMarkerIcon : function ( layer ) {
var icon = layer . options . icon = this . options . iconCreateFunction ( {
getChildCount : function ( ) {
return 1 ;
} ,
getAllChildMarkers : function ( ) {
return [ layer ] ;
}
} ) ;
return icon ;
}
} ) ;
// Constant bounds used in case option "removeOutsideVisibleBounds" is set to false.
L . MarkerClusterGroup . include ( {
_mapBoundsInfinite : new L . LatLngBounds ( new L . LatLng ( - Infinity , - Infinity ) , new L . LatLng ( Infinity , Infinity ) )
} ) ;
L . MarkerClusterGroup . include ( {
_noAnimation : {
//Non Animated versions of everything
_animationStart : function ( ) {
//Do nothing...
} ,
_animationZoomIn : function ( previousZoomLevel , newZoomLevel ) {
2020-04-06 03:03:19 +00:00
this . _topClusterLevel . _recursivelyRemoveChildrenFromMap ( this . _currentShownBounds , Math . floor ( this . _map . getMinZoom ( ) ) , previousZoomLevel ) ;
2017-02-04 16:46:44 +00:00
this . _topClusterLevel . _recursivelyAddChildrenToMap ( null , newZoomLevel , this . _getExpandedVisibleBounds ( ) ) ;
//We didn't actually animate, but we use this event to mean "clustering animations have finished"
this . fire ( 'animationend' ) ;
} ,
_animationZoomOut : function ( previousZoomLevel , newZoomLevel ) {
2020-04-06 03:03:19 +00:00
this . _topClusterLevel . _recursivelyRemoveChildrenFromMap ( this . _currentShownBounds , Math . floor ( this . _map . getMinZoom ( ) ) , previousZoomLevel ) ;
2017-02-04 16:46:44 +00:00
this . _topClusterLevel . _recursivelyAddChildrenToMap ( null , newZoomLevel , this . _getExpandedVisibleBounds ( ) ) ;
//We didn't actually animate, but we use this event to mean "clustering animations have finished"
this . fire ( 'animationend' ) ;
} ,
_animationAddLayer : function ( layer , newCluster ) {
this . _animationAddLayerNonAnimated ( layer , newCluster ) ;
}
} ,
_withAnimation : {
//Animated versions here
_animationStart : function ( ) {
this . _map . _mapPane . className += ' leaflet-cluster-anim' ;
this . _inZoomAnimation ++ ;
} ,
_animationZoomIn : function ( previousZoomLevel , newZoomLevel ) {
var bounds = this . _getExpandedVisibleBounds ( ) ,
fg = this . _featureGroup ,
2020-04-06 03:03:19 +00:00
minZoom = Math . floor ( this . _map . getMinZoom ( ) ) ,
2017-02-04 16:46:44 +00:00
i ;
this . _ignoreMove = true ;
//Add all children of current clusters to map and remove those clusters from map
this . _topClusterLevel . _recursively ( bounds , previousZoomLevel , minZoom , function ( c ) {
var startPos = c . _latlng ,
markers = c . _markers ,
m ;
if ( ! bounds . contains ( startPos ) ) {
startPos = null ;
}
if ( c . _isSingleParent ( ) && previousZoomLevel + 1 === newZoomLevel ) { //Immediately add the new child and remove us
fg . removeLayer ( c ) ;
c . _recursivelyAddChildrenToMap ( null , newZoomLevel , bounds ) ;
} else {
//Fade out old cluster
c . clusterHide ( ) ;
c . _recursivelyAddChildrenToMap ( startPos , newZoomLevel , bounds ) ;
}
//Remove all markers that aren't visible any more
//TODO: Do we actually need to do this on the higher levels too?
for ( i = markers . length - 1 ; i >= 0 ; i -- ) {
m = markers [ i ] ;
if ( ! bounds . contains ( m . _latlng ) ) {
fg . removeLayer ( m ) ;
}
}
} ) ;
this . _forceLayout ( ) ;
//Update opacities
this . _topClusterLevel . _recursivelyBecomeVisible ( bounds , newZoomLevel ) ;
//TODO Maybe? Update markers in _recursivelyBecomeVisible
fg . eachLayer ( function ( n ) {
if ( ! ( n instanceof L . MarkerCluster ) && n . _icon ) {
n . clusterShow ( ) ;
}
} ) ;
//update the positions of the just added clusters/markers
this . _topClusterLevel . _recursively ( bounds , previousZoomLevel , newZoomLevel , function ( c ) {
c . _recursivelyRestoreChildPositions ( newZoomLevel ) ;
} ) ;
this . _ignoreMove = false ;
//Remove the old clusters and close the zoom animation
this . _enqueue ( function ( ) {
//update the positions of the just added clusters/markers
this . _topClusterLevel . _recursively ( bounds , previousZoomLevel , minZoom , function ( c ) {
fg . removeLayer ( c ) ;
c . clusterShow ( ) ;
} ) ;
this . _animationEnd ( ) ;
} ) ;
} ,
_animationZoomOut : function ( previousZoomLevel , newZoomLevel ) {
this . _animationZoomOutSingle ( this . _topClusterLevel , previousZoomLevel - 1 , newZoomLevel ) ;
//Need to add markers for those that weren't on the map before but are now
this . _topClusterLevel . _recursivelyAddChildrenToMap ( null , newZoomLevel , this . _getExpandedVisibleBounds ( ) ) ;
//Remove markers that were on the map before but won't be now
2020-04-06 03:03:19 +00:00
this . _topClusterLevel . _recursivelyRemoveChildrenFromMap ( this . _currentShownBounds , Math . floor ( this . _map . getMinZoom ( ) ) , previousZoomLevel , this . _getExpandedVisibleBounds ( ) ) ;
2017-02-04 16:46:44 +00:00
} ,
_animationAddLayer : function ( layer , newCluster ) {
var me = this ,
fg = this . _featureGroup ;
fg . addLayer ( layer ) ;
if ( newCluster !== layer ) {
if ( newCluster . _childCount > 2 ) { //Was already a cluster
newCluster . _updateIcon ( ) ;
this . _forceLayout ( ) ;
this . _animationStart ( ) ;
layer . _setPos ( this . _map . latLngToLayerPoint ( newCluster . getLatLng ( ) ) ) ;
layer . clusterHide ( ) ;
this . _enqueue ( function ( ) {
fg . removeLayer ( layer ) ;
layer . clusterShow ( ) ;
me . _animationEnd ( ) ;
} ) ;
} else { //Just became a cluster
this . _forceLayout ( ) ;
me . _animationStart ( ) ;
me . _animationZoomOutSingle ( newCluster , this . _map . getMaxZoom ( ) , this . _zoom ) ;
}
}
}
} ,
// Private methods for animated versions.
_animationZoomOutSingle : function ( cluster , previousZoomLevel , newZoomLevel ) {
var bounds = this . _getExpandedVisibleBounds ( ) ,
2020-04-06 03:03:19 +00:00
minZoom = Math . floor ( this . _map . getMinZoom ( ) ) ;
2017-02-04 16:46:44 +00:00
//Animate all of the markers in the clusters to move to their cluster center point
2020-04-06 03:03:19 +00:00
cluster . _recursivelyAnimateChildrenInAndAddSelfToMap ( bounds , minZoom , previousZoomLevel + 1 , newZoomLevel ) ;
2017-02-04 16:46:44 +00:00
var me = this ;
//Update the opacity (If we immediately set it they won't animate)
this . _forceLayout ( ) ;
cluster . _recursivelyBecomeVisible ( bounds , newZoomLevel ) ;
//TODO: Maybe use the transition timing stuff to make this more reliable
//When the animations are done, tidy up
this . _enqueue ( function ( ) {
//This cluster stopped being a cluster before the timeout fired
if ( cluster . _childCount === 1 ) {
var m = cluster . _markers [ 0 ] ;
//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
this . _ignoreMove = true ;
m . setLatLng ( m . getLatLng ( ) ) ;
this . _ignoreMove = false ;
if ( m . clusterShow ) {
m . clusterShow ( ) ;
}
} else {
cluster . _recursively ( bounds , newZoomLevel , minZoom , function ( c ) {
2020-04-06 03:03:19 +00:00
c . _recursivelyRemoveChildrenFromMap ( bounds , minZoom , previousZoomLevel + 1 ) ;
2017-02-04 16:46:44 +00:00
} ) ;
}
me . _animationEnd ( ) ;
} ) ;
} ,
_animationEnd : function ( ) {
if ( this . _map ) {
this . _map . _mapPane . className = this . _map . _mapPane . className . replace ( ' leaflet-cluster-anim' , '' ) ;
}
this . _inZoomAnimation -- ;
this . fire ( 'animationend' ) ;
} ,
//Force a browser layout of stuff in the map
// Should apply the current opacity and location to all elements so we can update them again for an animation
_forceLayout : function ( ) {
//In my testing this works, infact offsetWidth of any element seems to work.
//Could loop all this._layers and do this for each _icon if it stops working
L . Util . falseFn ( document . body . offsetWidth ) ;
}
} ) ;
L . markerClusterGroup = function ( options ) {
return new L . MarkerClusterGroup ( options ) ;
} ;
2020-04-06 03:03:19 +00:00
var MarkerCluster = L . MarkerCluster = L . Marker . extend ( {
options : L . Icon . prototype . options ,
2017-02-04 16:46:44 +00:00
initialize : function ( group , zoom , a , b ) {
2020-04-06 03:03:19 +00:00
L . Marker . prototype . initialize . call ( this , a ? ( a . _cLatLng || a . getLatLng ( ) ) : new L . LatLng ( 0 , 0 ) ,
{ icon : this , pane : group . options . clusterPane } ) ;
2017-02-04 16:46:44 +00:00
this . _group = group ;
this . _zoom = zoom ;
this . _markers = [ ] ;
this . _childClusters = [ ] ;
this . _childCount = 0 ;
this . _iconNeedsUpdate = true ;
this . _boundsNeedUpdate = true ;
this . _bounds = new L . LatLngBounds ( ) ;
if ( a ) {
this . _addChild ( a ) ;
}
if ( b ) {
this . _addChild ( b ) ;
}
} ,
//Recursively retrieve all child markers of this cluster
2020-04-06 03:03:19 +00:00
getAllChildMarkers : function ( storageArray , ignoreDraggedMarker ) {
2017-02-04 16:46:44 +00:00
storageArray = storageArray || [ ] ;
for ( var i = this . _childClusters . length - 1 ; i >= 0 ; i -- ) {
this . _childClusters [ i ] . getAllChildMarkers ( storageArray ) ;
}
for ( var j = this . _markers . length - 1 ; j >= 0 ; j -- ) {
2020-04-06 03:03:19 +00:00
if ( ignoreDraggedMarker && this . _markers [ j ] . _ _dragStart ) {
continue ;
}
2017-02-04 16:46:44 +00:00
storageArray . push ( this . _markers [ j ] ) ;
}
return storageArray ;
} ,
//Returns the count of how many child markers we have
getChildCount : function ( ) {
return this . _childCount ;
} ,
//Zoom to the minimum of showing all of the child markers, or the extents of this cluster
2020-04-06 03:03:19 +00:00
zoomToBounds : function ( fitBoundsOptions ) {
2017-02-04 16:46:44 +00:00
var childClusters = this . _childClusters . slice ( ) ,
map = this . _group . _map ,
boundsZoom = map . getBoundsZoom ( this . _bounds ) ,
zoom = this . _zoom + 1 ,
mapZoom = map . getZoom ( ) ,
i ;
//calculate how far we need to zoom down to see all of the markers
while ( childClusters . length > 0 && boundsZoom > zoom ) {
zoom ++ ;
var newClusters = [ ] ;
for ( i = 0 ; i < childClusters . length ; i ++ ) {
newClusters = newClusters . concat ( childClusters [ i ] . _childClusters ) ;
}
childClusters = newClusters ;
}
if ( boundsZoom > zoom ) {
this . _group . _map . setView ( this . _latlng , zoom ) ;
} else if ( boundsZoom <= mapZoom ) { //If fitBounds wouldn't zoom us down, zoom us down instead
this . _group . _map . setView ( this . _latlng , mapZoom + 1 ) ;
} else {
2020-04-06 03:03:19 +00:00
this . _group . _map . fitBounds ( this . _bounds , fitBoundsOptions ) ;
2017-02-04 16:46:44 +00:00
}
} ,
getBounds : function ( ) {
var bounds = new L . LatLngBounds ( ) ;
bounds . extend ( this . _bounds ) ;
return bounds ;
} ,
_updateIcon : function ( ) {
this . _iconNeedsUpdate = true ;
if ( this . _icon ) {
this . setIcon ( this ) ;
}
} ,
//Cludge for Icon, we pretend to be an icon for performance
createIcon : function ( ) {
if ( this . _iconNeedsUpdate ) {
this . _iconObj = this . _group . options . iconCreateFunction ( this ) ;
this . _iconNeedsUpdate = false ;
}
return this . _iconObj . createIcon ( ) ;
} ,
createShadow : function ( ) {
return this . _iconObj . createShadow ( ) ;
} ,
_addChild : function ( new1 , isNotificationFromChild ) {
this . _iconNeedsUpdate = true ;
this . _boundsNeedUpdate = true ;
this . _setClusterCenter ( new1 ) ;
if ( new1 instanceof L . MarkerCluster ) {
if ( ! isNotificationFromChild ) {
this . _childClusters . push ( new1 ) ;
new1 . _ _parent = this ;
}
this . _childCount += new1 . _childCount ;
} else {
if ( ! isNotificationFromChild ) {
this . _markers . push ( new1 ) ;
}
this . _childCount ++ ;
}
if ( this . _ _parent ) {
this . _ _parent . _addChild ( new1 , true ) ;
}
} ,
/ * *
* Makes sure the cluster center is set . If not , uses the child center if it is a cluster , or the marker position .
* @ param child L . MarkerCluster | L . Marker that will be used as cluster center if not defined yet .
* @ private
* /
_setClusterCenter : function ( child ) {
if ( ! this . _cLatLng ) {
// when clustering, take position of the first point as the cluster center
this . _cLatLng = child . _cLatLng || child . _latlng ;
}
} ,
/ * *
* Assigns impossible bounding values so that the next extend entirely determines the new bounds .
* This method avoids having to trash the previous L . LatLngBounds object and to create a new one , which is much slower for this class .
* As long as the bounds are not extended , most other methods would probably fail , as they would with bounds initialized but not extended .
* @ private
* /
_resetBounds : function ( ) {
var bounds = this . _bounds ;
if ( bounds . _southWest ) {
bounds . _southWest . lat = Infinity ;
bounds . _southWest . lng = Infinity ;
}
if ( bounds . _northEast ) {
bounds . _northEast . lat = - Infinity ;
bounds . _northEast . lng = - Infinity ;
}
} ,
_recalculateBounds : function ( ) {
var markers = this . _markers ,
childClusters = this . _childClusters ,
latSum = 0 ,
lngSum = 0 ,
totalCount = this . _childCount ,
i , child , childLatLng , childCount ;
// Case where all markers are removed from the map and we are left with just an empty _topClusterLevel.
if ( totalCount === 0 ) {
return ;
}
// Reset rather than creating a new object, for performance.
this . _resetBounds ( ) ;
// Child markers.
for ( i = 0 ; i < markers . length ; i ++ ) {
childLatLng = markers [ i ] . _latlng ;
this . _bounds . extend ( childLatLng ) ;
latSum += childLatLng . lat ;
lngSum += childLatLng . lng ;
}
// Child clusters.
for ( i = 0 ; i < childClusters . length ; i ++ ) {
child = childClusters [ i ] ;
// Re-compute child bounds and weighted position first if necessary.
if ( child . _boundsNeedUpdate ) {
child . _recalculateBounds ( ) ;
}
this . _bounds . extend ( child . _bounds ) ;
childLatLng = child . _wLatLng ;
childCount = child . _childCount ;
latSum += childLatLng . lat * childCount ;
lngSum += childLatLng . lng * childCount ;
}
this . _latlng = this . _wLatLng = new L . LatLng ( latSum / totalCount , lngSum / totalCount ) ;
// Reset dirty flag.
this . _boundsNeedUpdate = false ;
} ,
//Set our markers position as given and add it to the map
_addToMap : function ( startPos ) {
if ( startPos ) {
this . _backupLatlng = this . _latlng ;
this . setLatLng ( startPos ) ;
}
this . _group . _featureGroup . addLayer ( this ) ;
} ,
_recursivelyAnimateChildrenIn : function ( bounds , center , maxZoom ) {
this . _recursively ( bounds , this . _group . _map . getMinZoom ( ) , maxZoom - 1 ,
function ( c ) {
var markers = c . _markers ,
i , m ;
for ( i = markers . length - 1 ; i >= 0 ; i -- ) {
m = markers [ i ] ;
//Only do it if the icon is still on the map
if ( m . _icon ) {
m . _setPos ( center ) ;
m . clusterHide ( ) ;
}
}
} ,
function ( c ) {
var childClusters = c . _childClusters ,
j , cm ;
for ( j = childClusters . length - 1 ; j >= 0 ; j -- ) {
cm = childClusters [ j ] ;
if ( cm . _icon ) {
cm . _setPos ( center ) ;
cm . clusterHide ( ) ;
}
}
}
) ;
} ,
2020-04-06 03:03:19 +00:00
_recursivelyAnimateChildrenInAndAddSelfToMap : function ( bounds , mapMinZoom , previousZoomLevel , newZoomLevel ) {
this . _recursively ( bounds , newZoomLevel , mapMinZoom ,
2017-02-04 16:46:44 +00:00
function ( c ) {
c . _recursivelyAnimateChildrenIn ( bounds , c . _group . _map . latLngToLayerPoint ( c . getLatLng ( ) ) . round ( ) , previousZoomLevel ) ;
//TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.
//As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate
if ( c . _isSingleParent ( ) && previousZoomLevel - 1 === newZoomLevel ) {
c . clusterShow ( ) ;
2020-04-06 03:03:19 +00:00
c . _recursivelyRemoveChildrenFromMap ( bounds , mapMinZoom , previousZoomLevel ) ; //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
2017-02-04 16:46:44 +00:00
} else {
c . clusterHide ( ) ;
}
c . _addToMap ( ) ;
}
) ;
} ,
_recursivelyBecomeVisible : function ( bounds , zoomLevel ) {
this . _recursively ( bounds , this . _group . _map . getMinZoom ( ) , zoomLevel , null , function ( c ) {
c . clusterShow ( ) ;
} ) ;
} ,
_recursivelyAddChildrenToMap : function ( startPos , zoomLevel , bounds ) {
this . _recursively ( bounds , this . _group . _map . getMinZoom ( ) - 1 , zoomLevel ,
function ( c ) {
if ( zoomLevel === c . _zoom ) {
return ;
}
//Add our child markers at startPos (so they can be animated out)
for ( var i = c . _markers . length - 1 ; i >= 0 ; i -- ) {
var nm = c . _markers [ i ] ;
if ( ! bounds . contains ( nm . _latlng ) ) {
continue ;
}
if ( startPos ) {
nm . _backupLatlng = nm . getLatLng ( ) ;
nm . setLatLng ( startPos ) ;
if ( nm . clusterHide ) {
nm . clusterHide ( ) ;
}
}
c . _group . _featureGroup . addLayer ( nm ) ;
}
} ,
function ( c ) {
c . _addToMap ( startPos ) ;
}
) ;
} ,
_recursivelyRestoreChildPositions : function ( zoomLevel ) {
//Fix positions of child markers
for ( var i = this . _markers . length - 1 ; i >= 0 ; i -- ) {
var nm = this . _markers [ i ] ;
if ( nm . _backupLatlng ) {
nm . setLatLng ( nm . _backupLatlng ) ;
delete nm . _backupLatlng ;
}
}
if ( zoomLevel - 1 === this . _zoom ) {
//Reposition child clusters
for ( var j = this . _childClusters . length - 1 ; j >= 0 ; j -- ) {
this . _childClusters [ j ] . _restorePosition ( ) ;
}
} else {
for ( var k = this . _childClusters . length - 1 ; k >= 0 ; k -- ) {
this . _childClusters [ k ] . _recursivelyRestoreChildPositions ( zoomLevel ) ;
}
}
} ,
_restorePosition : function ( ) {
if ( this . _backupLatlng ) {
this . setLatLng ( this . _backupLatlng ) ;
delete this . _backupLatlng ;
}
} ,
//exceptBounds: If set, don't remove any markers/clusters in it
2020-04-06 03:03:19 +00:00
_recursivelyRemoveChildrenFromMap : function ( previousBounds , mapMinZoom , zoomLevel , exceptBounds ) {
2017-02-04 16:46:44 +00:00
var m , i ;
2020-04-06 03:03:19 +00:00
this . _recursively ( previousBounds , mapMinZoom - 1 , zoomLevel - 1 ,
2017-02-04 16:46:44 +00:00
function ( c ) {
//Remove markers at every level
for ( i = c . _markers . length - 1 ; i >= 0 ; i -- ) {
m = c . _markers [ i ] ;
if ( ! exceptBounds || ! exceptBounds . contains ( m . _latlng ) ) {
c . _group . _featureGroup . removeLayer ( m ) ;
if ( m . clusterShow ) {
m . clusterShow ( ) ;
}
}
}
} ,
function ( c ) {
//Remove child clusters at just the bottom level
for ( i = c . _childClusters . length - 1 ; i >= 0 ; i -- ) {
m = c . _childClusters [ i ] ;
if ( ! exceptBounds || ! exceptBounds . contains ( m . _latlng ) ) {
c . _group . _featureGroup . removeLayer ( m ) ;
if ( m . clusterShow ) {
m . clusterShow ( ) ;
}
}
}
}
) ;
} ,
//Run the given functions recursively to this and child clusters
// boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to
// zoomLevelToStart: zoom level to start running functions (inclusive)
// zoomLevelToStop: zoom level to stop running functions (inclusive)
// runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level
// runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level
_recursively : function ( boundsToApplyTo , zoomLevelToStart , zoomLevelToStop , runAtEveryLevel , runAtBottomLevel ) {
var childClusters = this . _childClusters ,
zoom = this . _zoom ,
i , c ;
if ( zoomLevelToStart <= zoom ) {
if ( runAtEveryLevel ) {
runAtEveryLevel ( this ) ;
}
if ( runAtBottomLevel && zoom === zoomLevelToStop ) {
runAtBottomLevel ( this ) ;
}
}
if ( zoom < zoomLevelToStart || zoom < zoomLevelToStop ) {
for ( i = childClusters . length - 1 ; i >= 0 ; i -- ) {
c = childClusters [ i ] ;
2020-04-06 03:03:19 +00:00
if ( c . _boundsNeedUpdate ) {
c . _recalculateBounds ( ) ;
}
2017-02-04 16:46:44 +00:00
if ( boundsToApplyTo . intersects ( c . _bounds ) ) {
c . _recursively ( boundsToApplyTo , zoomLevelToStart , zoomLevelToStop , runAtEveryLevel , runAtBottomLevel ) ;
}
}
}
} ,
//Returns true if we are the parent of only one cluster and that cluster is the same as us
_isSingleParent : function ( ) {
//Don't need to check this._markers as the rest won't work if there are any
return this . _childClusters . length > 0 && this . _childClusters [ 0 ] . _childCount === this . _childCount ;
}
} ) ;
/ *
* Extends L . Marker to include two extra methods : clusterHide and clusterShow .
*
* They work as setOpacity ( 0 ) and setOpacity ( 1 ) respectively , but
2020-04-06 03:03:19 +00:00
* don ' t overwrite the options . opacity
2017-02-04 16:46:44 +00:00
*
* /
L . Marker . include ( {
clusterHide : function ( ) {
2020-04-06 03:03:19 +00:00
var backup = this . options . opacity ;
this . setOpacity ( 0 ) ;
this . options . opacity = backup ;
return this ;
2017-02-04 16:46:44 +00:00
} ,
clusterShow : function ( ) {
2020-04-06 03:03:19 +00:00
return this . setOpacity ( this . options . opacity ) ;
2017-02-04 16:46:44 +00:00
}
} ) ;
L . DistanceGrid = function ( cellSize ) {
this . _cellSize = cellSize ;
this . _sqCellSize = cellSize * cellSize ;
this . _grid = { } ;
this . _objectPoint = { } ;
} ;
L . DistanceGrid . prototype = {
addObject : function ( obj , point ) {
var x = this . _getCoord ( point . x ) ,
y = this . _getCoord ( point . y ) ,
grid = this . _grid ,
row = grid [ y ] = grid [ y ] || { } ,
cell = row [ x ] = row [ x ] || [ ] ,
stamp = L . Util . stamp ( obj ) ;
this . _objectPoint [ stamp ] = point ;
cell . push ( obj ) ;
} ,
updateObject : function ( obj , point ) {
this . removeObject ( obj ) ;
this . addObject ( obj , point ) ;
} ,
//Returns true if the object was found
removeObject : function ( obj , point ) {
var x = this . _getCoord ( point . x ) ,
y = this . _getCoord ( point . y ) ,
grid = this . _grid ,
row = grid [ y ] = grid [ y ] || { } ,
cell = row [ x ] = row [ x ] || [ ] ,
i , len ;
delete this . _objectPoint [ L . Util . stamp ( obj ) ] ;
for ( i = 0 , len = cell . length ; i < len ; i ++ ) {
if ( cell [ i ] === obj ) {
cell . splice ( i , 1 ) ;
if ( len === 1 ) {
delete row [ x ] ;
}
return true ;
}
}
} ,
eachObject : function ( fn , context ) {
var i , j , k , len , row , cell , removed ,
grid = this . _grid ;
for ( i in grid ) {
row = grid [ i ] ;
for ( j in row ) {
cell = row [ j ] ;
for ( k = 0 , len = cell . length ; k < len ; k ++ ) {
removed = fn . call ( context , cell [ k ] ) ;
if ( removed ) {
k -- ;
len -- ;
}
}
}
}
} ,
getNearObject : function ( point ) {
var x = this . _getCoord ( point . x ) ,
y = this . _getCoord ( point . y ) ,
i , j , k , row , cell , len , obj , dist ,
objectPoint = this . _objectPoint ,
closestDistSq = this . _sqCellSize ,
closest = null ;
for ( i = y - 1 ; i <= y + 1 ; i ++ ) {
row = this . _grid [ i ] ;
if ( row ) {
for ( j = x - 1 ; j <= x + 1 ; j ++ ) {
cell = row [ j ] ;
if ( cell ) {
for ( k = 0 , len = cell . length ; k < len ; k ++ ) {
obj = cell [ k ] ;
dist = this . _sqDist ( objectPoint [ L . Util . stamp ( obj ) ] , point ) ;
2020-04-06 03:03:19 +00:00
if ( dist < closestDistSq ||
dist <= closestDistSq && closest === null ) {
2017-02-04 16:46:44 +00:00
closestDistSq = dist ;
closest = obj ;
}
}
}
}
}
}
return closest ;
} ,
_getCoord : function ( x ) {
2020-04-06 03:03:19 +00:00
var coord = Math . floor ( x / this . _cellSize ) ;
return isFinite ( coord ) ? coord : x ;
2017-02-04 16:46:44 +00:00
} ,
_sqDist : function ( p , p2 ) {
var dx = p2 . x - p . x ,
dy = p2 . y - p . y ;
return dx * dx + dy * dy ;
}
} ;
/ * C o p y r i g h t ( c ) 2 0 1 2 t h e a u t h o r s l i s t e d a t t h e f o l l o w i n g U R L , a n d / o r
the authors of referenced articles or incorporated external code :
http : //en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256
Permission is hereby granted , free of charge , to any person obtaining
a copy of this software and associated documentation files ( the
"Software" ) , to deal in the Software without restriction , including
without limitation the rights to use , copy , modify , merge , publish ,
distribute , sublicense , and / or sell copies of the Software , and to
permit persons to whom the Software is furnished to do so , subject to
the following conditions :
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software .
THE SOFTWARE IS PROVIDED "AS IS" , WITHOUT WARRANTY OF ANY KIND ,
EXPRESS OR IMPLIED , INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY , FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT .
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM , DAMAGES OR OTHER LIABILITY , WHETHER IN AN ACTION OF CONTRACT ,
TORT OR OTHERWISE , ARISING FROM , OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE .
Retrieved from : http : //en.literateprograms.org/Quickhull_(Javascript)?oldid=18434
* /
( function ( ) {
L . QuickHull = {
/ *
* @ param { Object } cpt a point to be measured from the baseline
* @ param { Array } bl the baseline , as represented by a two - element
* array of latlng objects .
* @ returns { Number } an approximate distance measure
* /
getDistant : function ( cpt , bl ) {
var vY = bl [ 1 ] . lat - bl [ 0 ] . lat ,
vX = bl [ 0 ] . lng - bl [ 1 ] . lng ;
return ( vX * ( cpt . lat - bl [ 0 ] . lat ) + vY * ( cpt . lng - bl [ 0 ] . lng ) ) ;
} ,
/ *
* @ param { Array } baseLine a two - element array of latlng objects
* representing the baseline to project from
* @ param { Array } latLngs an array of latlng objects
* @ returns { Object } the maximum point and all new points to stay
* in consideration for the hull .
* /
findMostDistantPointFromBaseLine : function ( baseLine , latLngs ) {
var maxD = 0 ,
maxPt = null ,
newPoints = [ ] ,
i , pt , d ;
for ( i = latLngs . length - 1 ; i >= 0 ; i -- ) {
pt = latLngs [ i ] ;
d = this . getDistant ( pt , baseLine ) ;
if ( d > 0 ) {
newPoints . push ( pt ) ;
} else {
continue ;
}
if ( d > maxD ) {
maxD = d ;
maxPt = pt ;
}
}
return { maxPoint : maxPt , newPoints : newPoints } ;
} ,
/ *
* Given a baseline , compute the convex hull of latLngs as an array
* of latLngs .
*
* @ param { Array } latLngs
* @ returns { Array }
* /
buildConvexHull : function ( baseLine , latLngs ) {
var convexHullBaseLines = [ ] ,
t = this . findMostDistantPointFromBaseLine ( baseLine , latLngs ) ;
if ( t . maxPoint ) { // if there is still a point "outside" the base line
convexHullBaseLines =
convexHullBaseLines . concat (
this . buildConvexHull ( [ baseLine [ 0 ] , t . maxPoint ] , t . newPoints )
) ;
convexHullBaseLines =
convexHullBaseLines . concat (
this . buildConvexHull ( [ t . maxPoint , baseLine [ 1 ] ] , t . newPoints )
) ;
return convexHullBaseLines ;
} else { // if there is no more point "outside" the base line, the current base line is part of the convex hull
return [ baseLine [ 0 ] ] ;
}
} ,
/ *
* Given an array of latlngs , compute a convex hull as an array
* of latlngs
*
* @ param { Array } latLngs
* @ returns { Array }
* /
getConvexHull : function ( latLngs ) {
// find first baseline
var maxLat = false , minLat = false ,
maxLng = false , minLng = false ,
maxLatPt = null , minLatPt = null ,
maxLngPt = null , minLngPt = null ,
maxPt = null , minPt = null ,
i ;
for ( i = latLngs . length - 1 ; i >= 0 ; i -- ) {
var pt = latLngs [ i ] ;
if ( maxLat === false || pt . lat > maxLat ) {
maxLatPt = pt ;
maxLat = pt . lat ;
}
if ( minLat === false || pt . lat < minLat ) {
minLatPt = pt ;
minLat = pt . lat ;
}
if ( maxLng === false || pt . lng > maxLng ) {
maxLngPt = pt ;
maxLng = pt . lng ;
}
if ( minLng === false || pt . lng < minLng ) {
minLngPt = pt ;
minLng = pt . lng ;
}
}
if ( minLat !== maxLat ) {
minPt = minLatPt ;
maxPt = maxLatPt ;
} else {
minPt = minLngPt ;
maxPt = maxLngPt ;
}
var ch = [ ] . concat ( this . buildConvexHull ( [ minPt , maxPt ] , latLngs ) ,
this . buildConvexHull ( [ maxPt , minPt ] , latLngs ) ) ;
return ch ;
}
} ;
} ( ) ) ;
L . MarkerCluster . include ( {
getConvexHull : function ( ) {
var childMarkers = this . getAllChildMarkers ( ) ,
points = [ ] ,
p , i ;
for ( i = childMarkers . length - 1 ; i >= 0 ; i -- ) {
p = childMarkers [ i ] . getLatLng ( ) ;
points . push ( p ) ;
}
return L . QuickHull . getConvexHull ( points ) ;
}
} ) ;
//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet
//Huge thanks to jawj for implementing it first to make my job easy :-)
L . MarkerCluster . include ( {
_2PI : Math . PI * 2 ,
_circleFootSeparation : 25 , //related to circumference of circle
2020-04-06 03:03:19 +00:00
_circleStartAngle : 0 ,
2017-02-04 16:46:44 +00:00
_spiralFootSeparation : 28 , //related to size of spiral (experiment!)
_spiralLengthStart : 11 ,
_spiralLengthFactor : 5 ,
_circleSpiralSwitchover : 9 , //show spiral instead of circle from this marker count upwards.
// 0 -> always spiral; Infinity -> always circle
spiderfy : function ( ) {
if ( this . _group . _spiderfied === this || this . _group . _inZoomAnimation ) {
return ;
}
2020-04-06 03:03:19 +00:00
var childMarkers = this . getAllChildMarkers ( null , true ) ,
2017-02-04 16:46:44 +00:00
group = this . _group ,
map = group . _map ,
center = map . latLngToLayerPoint ( this . _latlng ) ,
positions ;
this . _group . _unspiderfy ( ) ;
this . _group . _spiderfied = this ;
//TODO Maybe: childMarkers order by distance to center
if ( childMarkers . length >= this . _circleSpiralSwitchover ) {
positions = this . _generatePointsSpiral ( childMarkers . length , center ) ;
} else {
center . y += 10 ; // Otherwise circles look wrong => hack for standard blue icon, renders differently for other icons.
positions = this . _generatePointsCircle ( childMarkers . length , center ) ;
}
this . _animationSpiderfy ( childMarkers , positions ) ;
} ,
unspiderfy : function ( zoomDetails ) {
/// <param Name="zoomDetails">Argument from zoomanim if being called in a zoom animation or null otherwise</param>
if ( this . _group . _inZoomAnimation ) {
return ;
}
this . _animationUnspiderfy ( zoomDetails ) ;
this . _group . _spiderfied = null ;
} ,
_generatePointsCircle : function ( count , centerPt ) {
var circumference = this . _group . options . spiderfyDistanceMultiplier * this . _circleFootSeparation * ( 2 + count ) ,
legLength = circumference / this . _2PI , //radius from circumference
angleStep = this . _2PI / count ,
res = [ ] ,
i , angle ;
2020-04-06 03:03:19 +00:00
legLength = Math . max ( legLength , 35 ) ; // Minimum distance to get outside the cluster icon.
2017-02-04 16:46:44 +00:00
res . length = count ;
2020-04-06 03:03:19 +00:00
for ( i = 0 ; i < count ; i ++ ) { // Clockwise, like spiral.
2017-02-04 16:46:44 +00:00
angle = this . _circleStartAngle + i * angleStep ;
res [ i ] = new L . Point ( centerPt . x + legLength * Math . cos ( angle ) , centerPt . y + legLength * Math . sin ( angle ) ) . _round ( ) ;
}
return res ;
} ,
_generatePointsSpiral : function ( count , centerPt ) {
var spiderfyDistanceMultiplier = this . _group . options . spiderfyDistanceMultiplier ,
legLength = spiderfyDistanceMultiplier * this . _spiralLengthStart ,
separation = spiderfyDistanceMultiplier * this . _spiralFootSeparation ,
lengthFactor = spiderfyDistanceMultiplier * this . _spiralLengthFactor * this . _2PI ,
angle = 0 ,
res = [ ] ,
i ;
res . length = count ;
// Higher index, closer position to cluster center.
2020-04-06 03:03:19 +00:00
for ( i = count ; i >= 0 ; i -- ) {
// Skip the first position, so that we are already farther from center and we avoid
// being under the default cluster icon (especially important for Circle Markers).
if ( i < count ) {
res [ i ] = new L . Point ( centerPt . x + legLength * Math . cos ( angle ) , centerPt . y + legLength * Math . sin ( angle ) ) . _round ( ) ;
}
2017-02-04 16:46:44 +00:00
angle += separation / legLength + i * 0.0005 ;
legLength += lengthFactor / angle ;
}
return res ;
} ,
_noanimationUnspiderfy : function ( ) {
var group = this . _group ,
map = group . _map ,
fg = group . _featureGroup ,
2020-04-06 03:03:19 +00:00
childMarkers = this . getAllChildMarkers ( null , true ) ,
2017-02-04 16:46:44 +00:00
m , i ;
group . _ignoreMove = true ;
this . setOpacity ( 1 ) ;
for ( i = childMarkers . length - 1 ; i >= 0 ; i -- ) {
m = childMarkers [ i ] ;
fg . removeLayer ( m ) ;
if ( m . _preSpiderfyLatlng ) {
m . setLatLng ( m . _preSpiderfyLatlng ) ;
delete m . _preSpiderfyLatlng ;
}
if ( m . setZIndexOffset ) {
m . setZIndexOffset ( 0 ) ;
}
if ( m . _spiderLeg ) {
map . removeLayer ( m . _spiderLeg ) ;
delete m . _spiderLeg ;
}
}
group . fire ( 'unspiderfied' , {
cluster : this ,
markers : childMarkers
} ) ;
group . _ignoreMove = false ;
group . _spiderfied = null ;
}
} ) ;
//Non Animated versions of everything
L . MarkerClusterNonAnimated = L . MarkerCluster . extend ( {
_animationSpiderfy : function ( childMarkers , positions ) {
var group = this . _group ,
map = group . _map ,
fg = group . _featureGroup ,
legOptions = this . _group . options . spiderLegPolylineOptions ,
i , m , leg , newPos ;
group . _ignoreMove = true ;
// Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.
// The reverse order trick no longer improves performance on modern browsers.
for ( i = 0 ; i < childMarkers . length ; i ++ ) {
newPos = map . layerPointToLatLng ( positions [ i ] ) ;
m = childMarkers [ i ] ;
// Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.
leg = new L . Polyline ( [ this . _latlng , newPos ] , legOptions ) ;
map . addLayer ( leg ) ;
m . _spiderLeg = leg ;
// Now add the marker.
m . _preSpiderfyLatlng = m . _latlng ;
m . setLatLng ( newPos ) ;
if ( m . setZIndexOffset ) {
m . setZIndexOffset ( 1000000 ) ; //Make these appear on top of EVERYTHING
}
fg . addLayer ( m ) ;
}
this . setOpacity ( 0.3 ) ;
group . _ignoreMove = false ;
group . fire ( 'spiderfied' , {
cluster : this ,
markers : childMarkers
} ) ;
} ,
_animationUnspiderfy : function ( ) {
this . _noanimationUnspiderfy ( ) ;
}
} ) ;
//Animated versions here
L . MarkerCluster . include ( {
_animationSpiderfy : function ( childMarkers , positions ) {
var me = this ,
group = this . _group ,
map = group . _map ,
fg = group . _featureGroup ,
thisLayerLatLng = this . _latlng ,
thisLayerPos = map . latLngToLayerPoint ( thisLayerLatLng ) ,
svg = L . Path . SVG ,
legOptions = L . extend ( { } , this . _group . options . spiderLegPolylineOptions ) , // Copy the options so that we can modify them for animation.
finalLegOpacity = legOptions . opacity ,
i , m , leg , legPath , legLength , newPos ;
if ( finalLegOpacity === undefined ) {
finalLegOpacity = L . MarkerClusterGroup . prototype . options . spiderLegPolylineOptions . opacity ;
}
if ( svg ) {
// If the initial opacity of the spider leg is not 0 then it appears before the animation starts.
legOptions . opacity = 0 ;
// Add the class for CSS transitions.
legOptions . className = ( legOptions . className || '' ) + ' leaflet-cluster-spider-leg' ;
} else {
// Make sure we have a defined opacity.
legOptions . opacity = finalLegOpacity ;
}
group . _ignoreMove = true ;
// Add markers and spider legs to map, hidden at our center point.
// Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.
// The reverse order trick no longer improves performance on modern browsers.
for ( i = 0 ; i < childMarkers . length ; i ++ ) {
m = childMarkers [ i ] ;
newPos = map . layerPointToLatLng ( positions [ i ] ) ;
// Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.
leg = new L . Polyline ( [ thisLayerLatLng , newPos ] , legOptions ) ;
map . addLayer ( leg ) ;
m . _spiderLeg = leg ;
// Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/
// In our case the transition property is declared in the CSS file.
if ( svg ) {
legPath = leg . _path ;
legLength = legPath . getTotalLength ( ) + 0.1 ; // Need a small extra length to avoid remaining dot in Firefox.
legPath . style . strokeDasharray = legLength ; // Just 1 length is enough, it will be duplicated.
legPath . style . strokeDashoffset = legLength ;
}
// If it is a marker, add it now and we'll animate it out
if ( m . setZIndexOffset ) {
m . setZIndexOffset ( 1000000 ) ; // Make normal markers appear on top of EVERYTHING
}
if ( m . clusterHide ) {
m . clusterHide ( ) ;
}
// Vectors just get immediately added
fg . addLayer ( m ) ;
if ( m . _setPos ) {
m . _setPos ( thisLayerPos ) ;
}
}
group . _forceLayout ( ) ;
group . _animationStart ( ) ;
// Reveal markers and spider legs.
for ( i = childMarkers . length - 1 ; i >= 0 ; i -- ) {
newPos = map . layerPointToLatLng ( positions [ i ] ) ;
m = childMarkers [ i ] ;
//Move marker to new position
m . _preSpiderfyLatlng = m . _latlng ;
m . setLatLng ( newPos ) ;
if ( m . clusterShow ) {
m . clusterShow ( ) ;
}
// Animate leg (animation is actually delegated to CSS transition).
if ( svg ) {
leg = m . _spiderLeg ;
legPath = leg . _path ;
legPath . style . strokeDashoffset = 0 ;
//legPath.style.strokeOpacity = finalLegOpacity;
leg . setStyle ( { opacity : finalLegOpacity } ) ;
}
}
this . setOpacity ( 0.3 ) ;
group . _ignoreMove = false ;
setTimeout ( function ( ) {
group . _animationEnd ( ) ;
group . fire ( 'spiderfied' , {
cluster : me ,
markers : childMarkers
} ) ;
} , 200 ) ;
} ,
_animationUnspiderfy : function ( zoomDetails ) {
var me = this ,
group = this . _group ,
map = group . _map ,
fg = group . _featureGroup ,
thisLayerPos = zoomDetails ? map . _latLngToNewLayerPoint ( this . _latlng , zoomDetails . zoom , zoomDetails . center ) : map . latLngToLayerPoint ( this . _latlng ) ,
2020-04-06 03:03:19 +00:00
childMarkers = this . getAllChildMarkers ( null , true ) ,
2017-02-04 16:46:44 +00:00
svg = L . Path . SVG ,
m , i , leg , legPath , legLength , nonAnimatable ;
group . _ignoreMove = true ;
group . _animationStart ( ) ;
//Make us visible and bring the child markers back in
this . setOpacity ( 1 ) ;
for ( i = childMarkers . length - 1 ; i >= 0 ; i -- ) {
m = childMarkers [ i ] ;
//Marker was added to us after we were spiderfied
if ( ! m . _preSpiderfyLatlng ) {
continue ;
}
//Close any popup on the marker first, otherwise setting the location of the marker will make the map scroll
m . closePopup ( ) ;
//Fix up the location to the real one
m . setLatLng ( m . _preSpiderfyLatlng ) ;
delete m . _preSpiderfyLatlng ;
//Hack override the location to be our center
nonAnimatable = true ;
if ( m . _setPos ) {
m . _setPos ( thisLayerPos ) ;
nonAnimatable = false ;
}
if ( m . clusterHide ) {
m . clusterHide ( ) ;
nonAnimatable = false ;
}
if ( nonAnimatable ) {
fg . removeLayer ( m ) ;
}
// Animate the spider leg back in (animation is actually delegated to CSS transition).
if ( svg ) {
leg = m . _spiderLeg ;
legPath = leg . _path ;
legLength = legPath . getTotalLength ( ) + 0.1 ;
legPath . style . strokeDashoffset = legLength ;
leg . setStyle ( { opacity : 0 } ) ;
}
}
group . _ignoreMove = false ;
setTimeout ( function ( ) {
//If we have only <= one child left then that marker will be shown on the map so don't remove it!
var stillThereChildCount = 0 ;
for ( i = childMarkers . length - 1 ; i >= 0 ; i -- ) {
m = childMarkers [ i ] ;
if ( m . _spiderLeg ) {
stillThereChildCount ++ ;
}
}
for ( i = childMarkers . length - 1 ; i >= 0 ; i -- ) {
m = childMarkers [ i ] ;
if ( ! m . _spiderLeg ) { //Has already been unspiderfied
continue ;
}
if ( m . clusterShow ) {
m . clusterShow ( ) ;
}
if ( m . setZIndexOffset ) {
m . setZIndexOffset ( 0 ) ;
}
if ( stillThereChildCount > 1 ) {
fg . removeLayer ( m ) ;
}
map . removeLayer ( m . _spiderLeg ) ;
delete m . _spiderLeg ;
}
group . _animationEnd ( ) ;
group . fire ( 'unspiderfied' , {
cluster : me ,
markers : childMarkers
} ) ;
} , 200 ) ;
}
} ) ;
L . MarkerClusterGroup . include ( {
//The MarkerCluster currently spiderfied (if any)
_spiderfied : null ,
unspiderfy : function ( ) {
this . _unspiderfy . apply ( this , arguments ) ;
} ,
_spiderfierOnAdd : function ( ) {
this . _map . on ( 'click' , this . _unspiderfyWrapper , this ) ;
if ( this . _map . options . zoomAnimation ) {
this . _map . on ( 'zoomstart' , this . _unspiderfyZoomStart , this ) ;
}
//Browsers without zoomAnimation or a big zoom don't fire zoomstart
this . _map . on ( 'zoomend' , this . _noanimationUnspiderfy , this ) ;
if ( ! L . Browser . touch ) {
this . _map . getRenderer ( this ) ;
//Needs to happen in the pageload, not after, or animations don't work in webkit
// http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements
//Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable
}
} ,
_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 ) ;
//Ensure that markers are back where they should be
// Use no animation to avoid a sticky leaflet-cluster-anim class on mapPane
this . _noanimationUnspiderfy ( ) ;
} ,
//On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated)
//This means we can define the animation they do rather than Markers doing an animation to their actual location
_unspiderfyZoomStart : function ( ) {
if ( ! this . _map ) { //May have been removed from the map by a zoomEnd handler
return ;
}
this . _map . on ( 'zoomanim' , this . _unspiderfyZoomAnim , this ) ;
} ,
_unspiderfyZoomAnim : function ( zoomDetails ) {
//Wait until the first zoomanim after the user has finished touch-zooming before running the animation
if ( L . DomUtil . hasClass ( this . _map . _mapPane , 'leaflet-touching' ) ) {
return ;
}
this . _map . off ( 'zoomanim' , this . _unspiderfyZoomAnim , this ) ;
this . _unspiderfy ( zoomDetails ) ;
} ,
_unspiderfyWrapper : function ( ) {
/// <summary>_unspiderfy but passes no arguments</summary>
this . _unspiderfy ( ) ;
} ,
_unspiderfy : function ( zoomDetails ) {
if ( this . _spiderfied ) {
this . _spiderfied . unspiderfy ( zoomDetails ) ;
}
} ,
_noanimationUnspiderfy : function ( ) {
if ( this . _spiderfied ) {
this . _spiderfied . _noanimationUnspiderfy ( ) ;
}
} ,
//If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc
_unspiderfyLayer : function ( layer ) {
if ( layer . _spiderLeg ) {
this . _featureGroup . removeLayer ( layer ) ;
if ( layer . clusterShow ) {
layer . clusterShow ( ) ;
}
//Position will be fixed up immediately in _animationUnspiderfy
if ( layer . setZIndexOffset ) {
layer . setZIndexOffset ( 0 ) ;
}
this . _map . removeLayer ( layer . _spiderLeg ) ;
delete layer . _spiderLeg ;
}
}
} ) ;
/ * *
* Adds 1 public method to MCG and 1 to L . Marker to facilitate changing
* markers ' icon options and refreshing their icon and their parent clusters
* accordingly ( case where their iconCreateFunction uses data of childMarkers
* to make up the cluster icon ) .
* /
L . MarkerClusterGroup . include ( {
/ * *
* Updates the icon of all clusters which are parents of the given marker ( s ) .
* In singleMarkerMode , also updates the given marker ( s ) icon .
* @ param layers L . MarkerClusterGroup | L . LayerGroup | Array ( L . Marker ) | Map ( L . Marker ) |
* L . MarkerCluster | L . Marker ( optional ) list of markers ( or single marker ) whose parent
* clusters need to be updated . If not provided , retrieves all child markers of this .
* @ returns { L . MarkerClusterGroup }
* /
refreshClusters : function ( layers ) {
if ( ! layers ) {
layers = this . _topClusterLevel . getAllChildMarkers ( ) ;
} else if ( layers instanceof L . MarkerClusterGroup ) {
layers = layers . _topClusterLevel . getAllChildMarkers ( ) ;
} else if ( layers instanceof L . LayerGroup ) {
layers = layers . _layers ;
} else if ( layers instanceof L . MarkerCluster ) {
layers = layers . getAllChildMarkers ( ) ;
} else if ( layers instanceof L . Marker ) {
layers = [ layers ] ;
} // else: must be an Array(L.Marker)|Map(L.Marker)
this . _flagParentsIconsNeedUpdate ( layers ) ;
this . _refreshClustersIcons ( ) ;
// In case of singleMarkerMode, also re-draw the markers.
if ( this . options . singleMarkerMode ) {
this . _refreshSingleMarkerModeMarkers ( layers ) ;
}
return this ;
} ,
/ * *
* Simply flags all parent clusters of the given markers as having a "dirty" icon .
* @ param layers Array ( L . Marker ) | Map ( L . Marker ) list of markers .
* @ private
* /
_flagParentsIconsNeedUpdate : function ( layers ) {
var id , parent ;
// Assumes layers is an Array or an Object whose prototype is non-enumerable.
for ( id in layers ) {
// Flag parent clusters' icon as "dirty", all the way up.
// Dumb process that flags multiple times upper parents, but still
// much more efficient than trying to be smart and make short lists,
// at least in the case of a hierarchy following a power law:
// http://jsperf.com/flag-nodes-in-power-hierarchy/2
parent = layers [ id ] . _ _parent ;
while ( parent ) {
parent . _iconNeedsUpdate = true ;
parent = parent . _ _parent ;
}
}
} ,
/ * *
* Re - draws the icon of the supplied markers .
* To be used in singleMarkerMode only .
* @ param layers Array ( L . Marker ) | Map ( L . Marker ) list of markers .
* @ private
* /
_refreshSingleMarkerModeMarkers : function ( layers ) {
var id , layer ;
for ( id in layers ) {
layer = layers [ id ] ;
// Make sure we do not override markers that do not belong to THIS group.
if ( this . hasLayer ( layer ) ) {
// Need to re-create the icon first, then re-draw the marker.
layer . setIcon ( this . _overrideMarkerIcon ( layer ) ) ;
}
}
}
} ) ;
L . Marker . include ( {
/ * *
* Updates the given options in the marker ' s icon and refreshes the marker .
* @ param options map object of icon options .
* @ param directlyRefreshClusters boolean ( optional ) true to trigger
* MCG . refreshClustersOf ( ) right away with this single marker .
* @ returns { L . Marker }
* /
refreshIconOptions : function ( options , directlyRefreshClusters ) {
var icon = this . options . icon ;
L . setOptions ( icon , options ) ;
this . setIcon ( icon ) ;
// Shortcut to refresh the associated MCG clusters right away.
// To be used when refreshing a single marker.
// Otherwise, better use MCG.refreshClusters() once at the end with
// the list of modified markers.
if ( directlyRefreshClusters && this . _ _parent ) {
this . _ _parent . _group . refreshClusters ( this ) ;
}
return this ;
}
} ) ;
2020-04-06 03:03:19 +00:00
exports . MarkerClusterGroup = MarkerClusterGroup ;
exports . MarkerCluster = MarkerCluster ;
2017-02-04 16:46:44 +00:00
2020-04-06 03:03:19 +00:00
} ) ) ) ;
//# sourceMappingURL=leaflet.markercluster-src.js.map