/ *
Leaflet . markercluster , Provides Beautiful Animated Marker Clustering functionality for Leaflet , a JS library for interactive maps .
https : //github.com/Leaflet/Leaflet.markercluster
( c ) 2012 - 2013 , Dave Leaver , smartrak
* /
( function ( window , document , undefined ) {
/ *
* L . MarkerClusterGroup extends L . FeatureGroup by clustering the markers contained within
* /
L . MarkerClusterGroup = L . FeatureGroup . extend ( {
options : {
maxClusterRadius : 80 , //A cluster will cover at most this many pixels from its center
iconCreateFunction : null ,
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 ,
//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 ,
//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 . on ( L . FeatureGroup . EVENTS , this . _ propagateEvent , this ) ;
this . _ nonPointGroup = L . featureGroup ( ) ;
this . _ nonPointGroup . on ( L . FeatureGroup . EVENTS , this . _ propagateEvent , 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 ;
} ,
addLayer : function ( layer ) {
if ( layer instanceof L . LayerGroup ) {
var array = [ ] ;
for ( var i in layer . _ layers ) {
array . push ( layer . _ layers [ i ] ) ;
}
return this . addLayers ( array ) ;
}
//Don't cluster non point data
if ( ! layer . getLatLng ) {
this . _ nonPointGroup . addLayer ( layer ) ;
return this ;
}
if ( ! this . _ map ) {
this . _ needsClustering . push ( 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 ) ;
//Work out what is visible
var visibleLayer = layer ,
currentZoom = this . _ map . getZoom ( ) ;
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 )
{
var array = [ ] ;
for ( var i in layer . _ layers ) {
array . push ( layer . _ layers [ i ] ) ;
}
return this . removeLayers ( array ) ;
}
//Non point layers
if ( ! layer . getLatLng ) {
this . _ nonPointGroup . removeLayer ( layer ) ;
return this ;
}
if ( ! this . _ map ) {
if ( ! this . _ arraySplice ( this . _ needsClustering , layer ) && this . hasLayer ( layer ) ) {
this . _ needsRemoving . push ( 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 ) ;
if ( this . _ featureGroup . hasLayer ( layer ) ) {
this . _ featureGroup . removeLayer ( layer ) ;
if ( layer . setOpacity ) {
layer . setOpacity ( 1 ) ;
}
}
return this ;
} ,
//Takes an array of markers and adds them in bulk
addLayers : function ( layersArray ) {
var i , l , m ,
onMap = this . _ map ,
fg = this . _ featureGroup ,
npg = this . _ nonPointGroup ;
for ( i = 0 , l = layersArray . length ; i < l ; i ++ ) {
m = layersArray [ i ] ;
//Not point data, can't be clustered
if ( ! m . getLatLng ) {
npg . addLayer ( m ) ;
continue ;
}
if ( this . hasLayer ( m ) ) {
continue ;
}
if ( ! onMap ) {
this . _ needsClustering . push ( m ) ;
continue ;
}
this . _ addLayer ( m , this . _ maxZoom ) ;
//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 ( onMap ) {
//Update the icons of all those visible clusters that were affected
fg . eachLayer ( function ( c ) {
if ( c instanceof L . MarkerCluster && c . _ iconNeedsUpdate ) {
c . _ updateIcon ( ) ;
}
} ) ;
this . _ topClusterLevel . _ recursivelyAddChildrenToMap ( null , this . _ zoom , this . _ currentShownBounds ) ;
}
return this ;
} ,
//Takes an array of markers and removes them in bulk
removeLayers : function ( layersArray ) {
var i , l , m ,
fg = this . _ featureGroup ,
npg = this . _ nonPointGroup ;
if ( ! this . _ map ) {
for ( i = 0 , l = layersArray . length ; i < l ; i ++ ) {
m = layersArray [ i ] ;
this . _ arraySplice ( this . _ needsClustering , m ) ;
npg . removeLayer ( m ) ;
}
return this ;
}
for ( i = 0 , l = layersArray . length ; i < l ; i ++ ) {
m = layersArray [ i ] ;
if ( ! m . __ parent ) {
npg . removeLayer ( m ) ;
continue ;
}
this . _ removeLayer ( m , true , true ) ;
if ( fg . hasLayer ( m ) ) {
fg . removeLayer ( m ) ;
if ( m . setOpacity ) {
m . setOpacity ( 1 ) ;
}
}
}
//Fix up the clusters and markers on the map
this . _ topClusterLevel . _ recursivelyAddChildrenToMap ( null , this . _ zoom , this . _ currentShownBounds ) ;
fg . eachLayer ( function ( c ) {
if ( c instanceof L . MarkerCluster ) {
c . _ updateIcon ( ) ;
}
} ) ;
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 = [ ] ;
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 ) {
delete marker . __ parent ;
} ) ;
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 ) ;
} else {
for ( var i = this . _ needsClustering . length - 1 ; i >= 0 ; i -- ) {
bounds . extend ( this . _ needsClustering [ i ] . getLatLng ( ) ) ;
}
}
//TODO: Can remove this isValid test when leaflet 0.6 is released
var nonPointBounds = this . _ nonPointGroup . getBounds ( ) ;
if ( nonPointBounds . isValid ( ) ) {
bounds . extend ( nonPointBounds ) ;
}
return bounds ;
} ,
//Overrides LayerGroup.eachLayer
eachLayer : function ( method , context ) {
var markers = this . _ needsClustering . slice ( ) ,
i ;
if ( this . _ topClusterLevel ) {
this . _ topClusterLevel . getAllChildMarkers ( markers ) ;
}
for ( i = markers . length - 1 ; i >= 0 ; i -- ) {
method . call ( context , markers [ i ] ) ;
}
this . _ nonPointGroup . eachLayer ( method , context ) ;
} ,
//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 ) {
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 ) {
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 ) {
var afterSpiderfy = function ( ) {
this . off ( 'spiderfied' , afterSpiderfy , this ) ;
callback ( ) ;
} ;
this . on ( 'spiderfied' , afterSpiderfy , this ) ;
layer . __ parent . spiderfy ( ) ;
}
}
} ;
if ( layer . _ icon ) {
callback ( ) ;
} else if ( layer . __ parent . _ zoom < this . _ map . getZoom ( ) ) {
//Layer should be visible now but isn't on screen, just pan over to it
this . _ map . on ( 'moveend' , showMarker , this ) ;
if ( ! layer . _ icon ) {
this . _ map . panTo ( layer . getLatLng ( ) ) ;
}
} else {
this . _ map . on ( 'moveend' , showMarker , this ) ;
this . on ( 'animationend' , showMarker , this ) ;
this . _ map . setView ( layer . getLatLng ( ) , layer . __ parent . _ zoom + 1 ) ;
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 . onAdd ( map ) ;
this . _ nonPointGroup . onAdd ( map ) ;
if ( ! this . _ gridClusters ) {
this . _ generateInitialClusters ( ) ;
}
for ( i = 0 , l = this . _ needsRemoving . length ; i < l ; i ++ ) {
layer = this . _ needsRemoving [ i ] ;
this . _ removeLayer ( layer , true ) ;
}
this . _ needsRemoving = [ ] ;
for ( i = 0 , l = this . _ needsClustering . length ; i < l ; i ++ ) {
layer = this . _ needsClustering [ i ] ;
//If the layer doesn't have a getLatLng then we can't cluster it, so add it to our child featureGroup
if ( ! layer . getLatLng ) {
this . _ featureGroup . addLayer ( layer ) ;
continue ;
}
if ( layer . __ parent ) {
continue ;
}
this . _ addLayer ( layer , this . _ maxZoom ) ;
}
this . _ needsClustering = [ ] ;
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:
//Remember the current zoom level and bounds
this . _ zoom = this . _ map . getZoom ( ) ;
this . _ currentShownBounds = this . _ getExpandedVisibleBounds ( ) ;
//Make things appear on the map
this . _ topClusterLevel . _ recursivelyAddChildrenToMap ( null , this . _ zoom , this . _ currentShownBounds ) ;
} ,
//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 ( ) ;
}
//Clean up all the layers we added to the map
this . _ featureGroup . onRemove ( map ) ;
this . _ nonPointGroup . onRemove ( map ) ;
this . _ featureGroup . clearLayers ( ) ;
this . _ map = null ;
} ,
getVisibleParent : function ( marker ) {
var vMarker = marker ;
while ( vMarker !== null && ! vMarker . _ icon ) {
vMarker = vMarker . __ parent ;
}
return vMarker ;
} ,
//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 ;
}
}
} ,
//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 ;
//Remove the marker from distance clusters it might be in
if ( removeFromDistanceGrid ) {
for ( var z = this . _ maxZoom ; z >= 0 ; z -- ) {
if ( ! gridUnclustered [ z ] . removeObject ( marker , map . project ( marker . getLatLng ( ) , z ) ) ) {
break ;
}
}
}
//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 -- ;
if ( cluster . _ zoom < 0 ) {
//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 . _ recalculateBounds ( ) ;
if ( ! dontUpdateMap || ! cluster . _ icon ) {
cluster . _ updateIcon ( ) ;
}
}
cluster = cluster . __ parent ;
}
delete marker . __ parent ;
} ,
_ propagateEvent : function ( e ) {
if ( e . layer instanceof L . MarkerCluster ) {
e . type = 'cluster' + e . type ;
}
this . fire ( e . type , e ) ;
} ,
//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 . _ hideCoverage , this ) ;
this . on ( 'clustermouseout' , this . _ hideCoverage , this ) ;
map . on ( 'zoomend' , this . _ hideCoverage , this ) ;
map . on ( 'layerremove' , this . _ hideCoverageOnRemove , this ) ;
}
} ,
_ zoomOrSpiderfy : function ( e ) {
var map = this . _ map ;
if ( map . getMaxZoom ( ) === map . getZoom ( ) ) {
if ( this . options . spiderfyOnMaxZoom ) {
e . layer . spiderfy ( ) ;
}
} else if ( this . options . zoomToBoundsOnClick ) {
e . layer . zoomToBounds ( ) ;
}
} ,
_ 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 ;
}
} ,
_ hideCoverageOnRemove : function ( e ) {
if ( e . layer === this ) {
this . _ hideCoverage ( ) ;
}
} ,
_ 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 ) ;
map . off ( 'layerremove' , this . _ hideCoverageOnRemove , this ) ;
}
} ,
_ zoomEnd : function ( ) {
if ( ! this . _ map ) { //May have been removed from the map by a zoomEnd handler
return ;
}
this . _ mergeSplitClusters ( ) ;
this . _ zoom = this . _ map . _ zoom ;
this . _ currentShownBounds = this . _ getExpandedVisibleBounds ( ) ;
} ,
_ moveEnd : function ( ) {
if ( this . _ inZoomAnimation ) {
return ;
}
var newBounds = this . _ getExpandedVisibleBounds ( ) ;
this . _ topClusterLevel . _ recursivelyRemoveChildrenFromMap ( this . _ currentShownBounds , this . _ zoom , newBounds ) ;
this . _ topClusterLevel . _ recursivelyAddChildrenToMap ( null , this . _ zoom , newBounds ) ;
this . _ currentShownBounds = newBounds ;
return ;
} ,
_ generateInitialClusters : function ( ) {
var maxZoom = this . _ map . getMaxZoom ( ) ,
radius = this . options . maxClusterRadius ;
if ( this . options . disableClusteringAtZoom ) {
maxZoom = this . options . disableClusteringAtZoom - 1 ;
}
this . _ maxZoom = maxZoom ;
this . _ gridClusters = { } ;
this . _ gridUnclustered = { } ;
//Set up DistanceGrids for each zoom
for ( var zoom = maxZoom ; zoom >= 0 ; zoom -- ) {
this . _ gridClusters [ zoom ] = new L . DistanceGrid ( radius ) ;
this . _ gridUnclustered [ zoom ] = new L . DistanceGrid ( radius ) ;
}
this . _ topClusterLevel = new L . MarkerCluster ( this , - 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 ,
markerPoint , z ;
if ( this . options . singleMarkerMode ) {
layer . options . icon = this . options . iconCreateFunction ( {
getChildCount : function ( ) {
return 1 ;
} ,
getAllChildMarkers : function ( ) {
return [ layer ] ;
}
} ) ;
}
//Find the lowest zoom level to slot this one in
for ( ; zoom >= 0 ; 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 L . 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 L . 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
for ( z = zoom ; z >= 0 ; z -- ) {
if ( ! gridUnclustered [ z ] . removeObject ( closest , this . _ map . project ( closest . getLatLng ( ) , z ) ) ) {
break ;
}
}
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 ;
} ,
//Merge and split any existing clusters that are too big or small
_ mergeSplitClusters : function ( ) {
if ( this . _ zoom < this . _ map . _ zoom ) { //Zoom in, split
this . _ animationStart ( ) ;
//Remove clusters now off screen
this . _ topClusterLevel . _ recursivelyRemoveChildrenFromMap ( this . _ currentShownBounds , this . _ zoom , this . _ getExpandedVisibleBounds ( ) ) ;
this . _ animationZoomIn ( this . _ zoom , this . _ map . _ zoom ) ;
} else if ( this . _ zoom > this . _ map . _ zoom ) { //Zoom out, merge
this . _ animationStart ( ) ;
this . _ animationZoomOut ( this . _ zoom , this . _ map . _ zoom ) ;
} 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 . getBounds ( ) ;
}
var map = this . _ map ,
bounds = map . getBounds ( ) ,
sw = bounds . _ southWest ,
ne = bounds . _ northEast ,
latDiff = L . Browser . mobile ? 0 : Math . abs ( sw . lat - ne . lat ) ,
lngDiff = L . Browser . mobile ? 0 : Math . abs ( sw . lng - ne . lng ) ;
return new L . LatLngBounds (
new L . LatLng ( sw . lat - latDiff , sw . lng - lngDiff , true ) ,
new L . LatLng ( ne . lat + latDiff , ne . lng + lngDiff , true ) ) ;
} ,
//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 ( ) ;
}
}
} ) ;
L . MarkerClusterGroup . include ( ! L . DomUtil . TRANSITION ? {
//Non Animated versions of everything
_ animationStart : function ( ) {
//Do nothing...
} ,
_ animationZoomIn : function ( previousZoomLevel , newZoomLevel ) {
this . _ topClusterLevel . _ recursivelyRemoveChildrenFromMap ( this . _ currentShownBounds , previousZoomLevel ) ;
this . _ topClusterLevel . _ recursivelyAddChildrenToMap ( null , newZoomLevel , this . _ getExpandedVisibleBounds ( ) ) ;
} ,
_ animationZoomOut : function ( previousZoomLevel , newZoomLevel ) {
this . _ topClusterLevel . _ recursivelyRemoveChildrenFromMap ( this . _ currentShownBounds , previousZoomLevel ) ;
this . _ topClusterLevel . _ recursivelyAddChildrenToMap ( null , newZoomLevel , this . _ getExpandedVisibleBounds ( ) ) ;
} ,
_ animationAddLayer : function ( layer , newCluster ) {
this . _ animationAddLayerNonAnimated ( layer , newCluster ) ;
}
} : {
//Animated versions here
_ animationStart : function ( ) {
this . _ map . _ mapPane . className += ' leaflet-cluster-anim' ;
this . _ inZoomAnimation ++ ;
} ,
_ animationEnd : function ( ) {
if ( this . _ map ) {
this . _ map . _ mapPane . className = this . _ map . _ mapPane . className . replace ( ' leaflet-cluster-anim' , '' ) ;
}
this . _ inZoomAnimation -- ;
this . fire ( 'animationend' ) ;
} ,
_ animationZoomIn : function ( previousZoomLevel , newZoomLevel ) {
var me = this ,
bounds = this . _ getExpandedVisibleBounds ( ) ,
fg = this . _ featureGroup ,
i ;
//Add all children of current clusters to map and remove those clusters from map
this . _ topClusterLevel . _ recursively ( bounds , previousZoomLevel , 0 , 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 . setOpacity ( 0 ) ;
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
me . _ topClusterLevel . _ recursivelyBecomeVisible ( bounds , newZoomLevel ) ;
//TODO Maybe? Update markers in _recursivelyBecomeVisible
fg . eachLayer ( function ( n ) {
if ( ! ( n instanceof L . MarkerCluster ) && n . _ icon ) {
n . setOpacity ( 1 ) ;
}
} ) ;
//update the positions of the just added clusters/markers
me . _ topClusterLevel . _ recursively ( bounds , previousZoomLevel , newZoomLevel , function ( c ) {
c . _ recursivelyRestoreChildPositions ( newZoomLevel ) ;
} ) ;
//Remove the old clusters and close the zoom animation
setTimeout ( function ( ) {
//update the positions of the just added clusters/markers
me . _ topClusterLevel . _ recursively ( bounds , previousZoomLevel , 0 , function ( c ) {
fg . removeLayer ( c ) ;
c . setOpacity ( 1 ) ;
} ) ;
me . _ animationEnd ( ) ;
} , 200 ) ;
} ,
_ 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
this . _ topClusterLevel . _ recursivelyRemoveChildrenFromMap ( this . _ currentShownBounds , previousZoomLevel , this . _ getExpandedVisibleBounds ( ) ) ;
} ,
_ animationZoomOutSingle : function ( cluster , previousZoomLevel , newZoomLevel ) {
var bounds = this . _ getExpandedVisibleBounds ( ) ;
//Animate all of the markers in the clusters to move to their cluster center point
cluster . _ recursivelyAnimateChildrenInAndAddSelfToMap ( bounds , previousZoomLevel + 1 , newZoomLevel ) ;
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
setTimeout ( 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
m . setLatLng ( m . getLatLng ( ) ) ;
m . setOpacity ( 1 ) ;
} else {
cluster . _ recursively ( bounds , newZoomLevel , 0 , function ( c ) {
c . _ recursivelyRemoveChildrenFromMap ( bounds , previousZoomLevel + 1 ) ;
} ) ;
}
me . _ animationEnd ( ) ;
} , 200 ) ;
} ,
_ 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 . setOpacity ( 0 ) ;
setTimeout ( function ( ) {
fg . removeLayer ( layer ) ;
layer . setOpacity ( 1 ) ;
me . _ animationEnd ( ) ;
} , 200 ) ;
} else { //Just became a cluster
this . _ forceLayout ( ) ;
me . _ animationStart ( ) ;
me . _ animationZoomOutSingle ( newCluster , this . _ map . getMaxZoom ( ) , this . _ map . getZoom ( ) ) ;
}
}
} ,
//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 ) ;
} ;
L . MarkerCluster = L . Marker . extend ( {
initialize : function ( group , zoom , a , b ) {
L . Marker . prototype . initialize . call ( this , a ? ( a . _ cLatLng || a . getLatLng ( ) ) : new L . LatLng ( 0 , 0 ) , { icon : this } ) ;
this . _ group = group ;
this . _ zoom = zoom ;
this . _ markers = [ ] ;
this . _ childClusters = [ ] ;
this . _ childCount = 0 ;
this . _ iconNeedsUpdate = true ;
this . _ bounds = new L . LatLngBounds ( ) ;
if ( a ) {
this . _ addChild ( a ) ;
}
if ( b ) {
this . _ addChild ( b ) ;
}
} ,
//Recursively retrieve all child markers of this cluster
getAllChildMarkers : function ( storageArray ) {
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 -- ) {
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 extents of this cluster
zoomToBounds : function ( ) {
this . _ group . _ map . fitBounds ( this . _ bounds ) ;
} ,
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 . _ expandBounds ( 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 ) ;
}
} ,
//Expand our bounds and tell our parent to
_ expandBounds : function ( marker ) {
var addedCount ,
addedLatLng = marker . _ wLatLng || marker . _ latlng ;
if ( marker instanceof L . MarkerCluster ) {
this . _ bounds . extend ( marker . _ bounds ) ;
addedCount = marker . _ childCount ;
} else {
this . _ bounds . extend ( addedLatLng ) ;
addedCount = 1 ;
}
if ( ! this . _ cLatLng ) {
// when clustering, take position of the first point as the cluster center
this . _ cLatLng = marker . _ cLatLng || addedLatLng ;
}
// when showing clusters, take weighted average of all points as cluster center
var totalCount = this . _ childCount + addedCount ;
//Calculate weighted latlng for display
if ( ! this . _ wLatLng ) {
this . _ latlng = this . _ wLatLng = new L . LatLng ( addedLatLng . lat , addedLatLng . lng ) ;
} else {
this . _ wLatLng . lat = ( addedLatLng . lat * addedCount + this . _ wLatLng . lat * this . _ childCount ) / totalCount ;
this . _ wLatLng . lng = ( addedLatLng . lng * addedCount + this . _ wLatLng . lng * this . _ childCount ) / totalCount ;
}
} ,
//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 , 0 , 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 . setOpacity ( 0 ) ;
}
}
} ,
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 . setOpacity ( 0 ) ;
}
}
}
) ;
} ,
_ recursivelyAnimateChildrenInAndAddSelfToMap : function ( bounds , previousZoomLevel , newZoomLevel ) {
this . _ recursively ( bounds , newZoomLevel , 0 ,
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 . setOpacity ( 1 ) ;
c . _ recursivelyRemoveChildrenFromMap ( bounds , previousZoomLevel ) ; //Immediately remove our children as we are replacing them. TODO previousBounds not bounds
} else {
c . setOpacity ( 0 ) ;
}
c . _ addToMap ( ) ;
}
) ;
} ,
_ recursivelyBecomeVisible : function ( bounds , zoomLevel ) {
this . _ recursively ( bounds , 0 , zoomLevel , null , function ( c ) {
c . setOpacity ( 1 ) ;
} ) ;
} ,
_ recursivelyAddChildrenToMap : function ( startPos , zoomLevel , bounds ) {
this . _ recursively ( bounds , - 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 . setOpacity ) {
nm . setOpacity ( 0 ) ;
}
}
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
_ recursivelyRemoveChildrenFromMap : function ( previousBounds , zoomLevel , exceptBounds ) {
var m , i ;
this . _ recursively ( previousBounds , - 1 , zoomLevel - 1 ,
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 . setOpacity ) {
m . setOpacity ( 1 ) ;
}
}
}
} ,
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 . setOpacity ) {
m . setOpacity ( 1 ) ;
}
}
}
}
) ;
} ,
//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 ) { //Still going down to required depth, just recurse to child clusters
for ( i = childClusters . length - 1 ; i >= 0 ; i -- ) {
c = childClusters [ i ] ;
if ( boundsToApplyTo . intersects ( c . _ bounds ) ) {
c . _ recursively ( boundsToApplyTo , zoomLevelToStart , zoomLevelToStop , runAtEveryLevel , runAtBottomLevel ) ;
}
}
} else { //In required depth
if ( runAtEveryLevel ) {
runAtEveryLevel ( this ) ;
}
if ( runAtBottomLevel && this . _ zoom === zoomLevelToStop ) {
runAtBottomLevel ( this ) ;
}
//TODO: This loop is almost the same as above
if ( zoomLevelToStop > zoom ) {
for ( i = childClusters . length - 1 ; i >= 0 ; i -- ) {
c = childClusters [ i ] ;
if ( boundsToApplyTo . intersects ( c . _ bounds ) ) {
c . _ recursively ( boundsToApplyTo , zoomLevelToStart , zoomLevelToStop , runAtEveryLevel , runAtBottomLevel ) ;
}
}
}
}
} ,
_ recalculateBounds : function ( ) {
var markers = this . _ markers ,
childClusters = this . _ childClusters ,
i ;
this . _ bounds = new L . LatLngBounds ( ) ;
delete this . _ wLatLng ;
for ( i = markers . length - 1 ; i >= 0 ; i -- ) {
this . _ expandBounds ( markers [ i ] ) ;
}
for ( i = childClusters . length - 1 ; i >= 0 ; i -- ) {
this . _ expandBounds ( childClusters [ i ] ) ;
}
} ,
//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 ;
}
} ) ;
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 ) ;
if ( dist < closestDistSq ) {
closestDistSq = dist ;
closest = obj ;
}
}
}
}
}
}
return closest ;
} ,
_ getCoord : function ( x ) {
return Math . floor ( x / this . _ cellSize ) ;
} ,
_ 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 = {
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 ) ) ;
} ,
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 } ;
} ,
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 ] ;
}
} ,
getConvexHull : function ( latLngs ) {
//find first baseline
var maxLat = false , minLat = false ,
maxPt = null , minPt = null ,
i ;
for ( i = latLngs . length - 1 ; i >= 0 ; i -- ) {
var pt = latLngs [ i ] ;
if ( maxLat === false || pt . lat > maxLat ) {
maxPt = pt ;
maxLat = pt . lat ;
}
if ( minLat === false || pt . lat < minLat ) {
minPt = pt ;
minLat = pt . lat ;
}
}
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 = [ ] ,
hullLatLng = [ ] ,
hull , p , i ;
for ( i = childMarkers . length - 1 ; i >= 0 ; i -- ) {
p = childMarkers [ i ] . getLatLng ( ) ;
points . push ( p ) ;
}
hull = L . QuickHull . getConvexHull ( points ) ;
for ( i = hull . length - 1 ; i >= 0 ; i -- ) {
hullLatLng . push ( hull [ i ] [ 0 ] ) ;
}
return hullLatLng ;
}
} ) ;
//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 ( {
_2 PI : Math . PI * 2 ,
_ circleFootSeparation : 25 , //related to circumference of circle
_ circleStartAngle : Math . PI / 6 ,
_ 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 ;
}
var childMarkers = this . getAllChildMarkers ( ) ,
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
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 . _2 PI , //radius from circumference
angleStep = this . _2 PI / count ,
res = [ ] ,
i , angle ;
res . length = count ;
for ( i = count - 1 ; i >= 0 ; i -- ) {
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 legLength = this . _ group . options . spiderfyDistanceMultiplier * this . _ spiralLengthStart ,
separation = this . _ group . options . spiderfyDistanceMultiplier * this . _ spiralFootSeparation ,
lengthFactor = this . _ group . options . spiderfyDistanceMultiplier * this . _ spiralLengthFactor ,
angle = 0 ,
res = [ ] ,
i ;
res . length = count ;
for ( i = count - 1 ; i >= 0 ; i -- ) {
angle += separation / legLength + i * 0.0005 ;
res [ i ] = new L . Point ( centerPt . x + legLength * Math . cos ( angle ) , centerPt . y + legLength * Math . sin ( angle ) ) . _ round ( ) ;
legLength += this . _2 PI * lengthFactor / angle ;
}
return res ;
} ,
_ noanimationUnspiderfy : function ( ) {
var group = this . _ group ,
map = group . _ map ,
fg = group . _ featureGroup ,
childMarkers = this . getAllChildMarkers ( ) ,
m , i ;
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 ;
}
}
}
} ) ;
L . MarkerCluster . include ( ! L . DomUtil . TRANSITION ? {
//Non Animated versions of everything
_ animationSpiderfy : function ( childMarkers , positions ) {
var group = this . _ group ,
map = group . _ map ,
fg = group . _ featureGroup ,
i , m , leg , newPos ;
for ( i = childMarkers . length - 1 ; i >= 0 ; i -- ) {
newPos = map . layerPointToLatLng ( positions [ i ] ) ;
m = childMarkers [ i ] ;
m . _ preSpiderfyLatlng = m . _ latlng ;
m . setLatLng ( newPos ) ;
if ( m . setZIndexOffset ) {
m . setZIndexOffset ( 1000000 ) ; //Make these appear on top of EVERYTHING
}
fg . addLayer ( m ) ;
leg = new L . Polyline ( [ this . _ latlng , newPos ] , { weight : 1.5 , color : '#222' } ) ;
map . addLayer ( leg ) ;
m . _ spiderLeg = leg ;
}
this . setOpacity ( 0 ) ;
group . fire ( 'spiderfied' ) ;
} ,
_ animationUnspiderfy : function ( ) {
this . _ noanimationUnspiderfy ( ) ;
}
} : {
//Animated versions here
SVG_ANIMATION : ( function ( ) {
return document . createElementNS ( 'http://www.w3.org/2000/svg' , 'animate' ) . toString ( ) . indexOf ( 'SVGAnimate' ) > - 1 ;
} ( ) ) ,
_ animationSpiderfy : function ( childMarkers , positions ) {
var me = this ,
group = this . _ group ,
map = group . _ map ,
fg = group . _ featureGroup ,
thisLayerPos = map . latLngToLayerPoint ( this . _ latlng ) ,
i , m , leg , newPos ;
//Add markers to map hidden at our center point
for ( i = childMarkers . length - 1 ; i >= 0 ; i -- ) {
m = childMarkers [ i ] ;
//If it is a marker, add it now and we'll animate it out
if ( m . setOpacity ) {
m . setZIndexOffset ( 1000000 ) ; //Make these appear on top of EVERYTHING
m . setOpacity ( 0 ) ;
fg . addLayer ( m ) ;
m . _ setPos ( thisLayerPos ) ;
} else {
//Vectors just get immediately added
fg . addLayer ( m ) ;
}
}
group . _ forceLayout ( ) ;
group . _ animationStart ( ) ;
var initialLegOpacity = L . Path . SVG ? 0 : 0 ,
xmlns = L . Path . SVG_NS ;
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 . setOpacity ) {
m . setOpacity ( 1 ) ;
}
//Add Legs.
leg = new L . Polyline ( [ me . _ latlng , newPos ] , { weight : 1.5 , color : '#222' , opacity : 0 } ) ;
map . addLayer ( leg ) ;
m . _ spiderLeg = leg ;
//Following animations don't work for canvas
if ( ! L . Path . SVG || ! this . SVG_ANIMATION ) {
continue ;
}
//How this works:
//http://stackoverflow.com/questions/5924238/how-do-you-animate-an-svg-path-in-ios
//http://dev.opera.com/articles/view/advanced-svg-animation-techniques/
//Animate length
var length = leg . _ path . getTotalLength ( ) ;
leg . _ path . setAttribute ( "stroke-dasharray" , length + "," + length ) ;
var anim = document . createElementNS ( xmlns , "animate" ) ;
anim . setAttribute ( "attributeName" , "stroke-dashoffset" ) ;
anim . setAttribute ( "begin" , "indefinite" ) ;
anim . setAttribute ( "from" , length ) ;
anim . setAttribute ( "to" , 0 ) ;
anim . setAttribute ( "dur" , 0.25 ) ;
leg . _ path . appendChild ( anim ) ;
anim . beginElement ( ) ;
//Animate opacity
anim = document . createElementNS ( xmlns , "animate" ) ;
anim . setAttribute ( "attributeName" , "stroke-opacity" ) ;
anim . setAttribute ( "attributeName" , "stroke-opacity" ) ;
anim . setAttribute ( "begin" , "indefinite" ) ;
anim . setAttribute ( "from" , 0 ) ;
anim . setAttribute ( "to" , 0 ) ;
anim . setAttribute ( "dur" , 0.25 ) ;
leg . _ path . appendChild ( anim ) ;
anim . beginElement ( ) ;
}
me . setOpacity ( 0 ) ;
//Set the opacity of the spiderLegs back to their correct value
// The animations above override this until they complete.
// If the initial opacity of the spiderlegs isn't 0 then they appear before the animation starts.
if ( L . Path . SVG ) {
this . _ group . _ forceLayout ( ) ;
for ( i = childMarkers . length - 1 ; i >= 0 ; i -- ) {
m = childMarkers [ i ] . _ spiderLeg ;
m . options . opacity = 0 ;
m . _ path . setAttribute ( 'stroke-opacity' , 0 ) ;
}
}
setTimeout ( function ( ) {
group . _ animationEnd ( ) ;
group . fire ( 'spiderfied' ) ;
} , 200 ) ;
} ,
_ animationUnspiderfy : function ( zoomDetails ) {
var group = this . _ group ,
map = group . _ map ,
fg = group . _ featureGroup ,
thisLayerPos = zoomDetails ? map . _ latLngToNewLayerPoint ( this . _ latlng , zoomDetails . zoom , zoomDetails . center ) : map . latLngToLayerPoint ( this . _ latlng ) ,
childMarkers = this . getAllChildMarkers ( ) ,
svg = L . Path . SVG && this . SVG_ANIMATION ,
m , i , a ;
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 spidified
if ( ! m . _ preSpiderfyLatlng ) {
continue ;
}
//Fix up the location to the real one
m . setLatLng ( m . _ preSpiderfyLatlng ) ;
delete m . _ preSpiderfyLatlng ;
//Hack override the location to be our center
if ( m . setOpacity ) {
m . _ setPos ( thisLayerPos ) ;
m . setOpacity ( 0 ) ;
} else {
fg . removeLayer ( m ) ;
}
//Animate the spider legs back in
if ( svg ) {
a = m . _ spiderLeg . _ path . childNodes [ 0 ] ;
a . setAttribute ( 'to' , a . getAttribute ( 'from' ) ) ;
a . setAttribute ( 'from' , 0 ) ;
a . beginElement ( ) ;
a = m . _ spiderLeg . _ path . childNodes [ 1 ] ;
a . setAttribute ( 'from' , 0 ) ;
a . setAttribute ( 'to' , 0 ) ;
a . setAttribute ( 'stroke-opacity' , 0 ) ;
a . beginElement ( ) ;
m . _ spiderLeg . _ path . setAttribute ( 'stroke-opacity' , 0 ) ;
}
}
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 . setOpacity ) {
m . setOpacity ( 1 ) ;
m . setZIndexOffset ( 0 ) ;
}
if ( stillThereChildCount > 1 ) {
fg . removeLayer ( m ) ;
}
map . removeLayer ( m . _ spiderLeg ) ;
delete m . _ spiderLeg ;
}
group . _ animationEnd ( ) ;
} , 200 ) ;
}
} ) ;
L . MarkerClusterGroup . include ( {
//The MarkerCluster currently spiderfied (if any)
_ spiderfied : null ,
_ spiderfierOnAdd : function ( ) {
this . _ map . on ( 'click' , this . _ unspiderfyWrapper , this ) ;
if ( this . _ map . options . zoomAnimation ) {
this . _ map . on ( 'zoomstart' , this . _ unspiderfyZoomStart , this ) ;
} else {
//Browsers without zoomAnimation don't fire zoomstart
this . _ map . on ( 'zoomend' , this . _ unspiderfyWrapper , this ) ;
}
if ( L . Path . SVG && ! L . Browser . touch ) {
this . _ map . _ initPathRoot ( ) ;
//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 . _ unspiderfy ( ) ; //Ensure that markers are back where they should be
} ,
//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 ) ;
layer . setOpacity ( 1 ) ;
//Position will be fixed up immediately in _animationUnspiderfy
layer . setZIndexOffset ( 0 ) ;
this . _ map . removeLayer ( layer . _ spiderLeg ) ;
delete layer . _ spiderLeg ;
}
}
} ) ;
} ( window , document ) ) ;