this._objectsOnMap[i];
if (o.data._leafletMarker !== m) {
if (o.bounds.minLat >= cbounds.minLat &&
o.bounds.maxLat <= cbounds.maxLat &&
o.bounds.minLng >= cbounds.minLng &&
o.bounds.maxLng <= cbounds.maxLng) {
filteredBounds.push(o.bounds);
}
}
}
// Filter the markers
if (filteredBounds.length > 0) {
var newMarkersArea = [];
var ll = filteredBounds.length;
for (i = 0, l = markersArea.length; i < l; ++i) {
var markerPos = markersArea[i].position;
var isFiltered = false;
for (var j = 0; j < ll; ++j) {
var currentFilteredBounds = filteredBounds[j];
if (markerPos.lat >= currentFilteredBounds.minLat &&
markerPos.lat <= currentFilteredBounds.maxLat &&
markerPos.lng >= currentFilteredBounds.minLng &&
markerPos.lng <= currentFilteredBounds.maxLng) {
isFiltered = true;
break;
}
}
if (!isFiltered) {
newMarkersArea.push(markersArea[i]);
}
}
markersArea = newMarkersArea;
}
// TODO use an option registered somewhere
if (markersArea.length < 200 || zoomLevelAfter >= this._map.getMaxZoom()) {
// Send an event for the LeafletSpiderfier
this._map.fire('overlappingmarkers', {
cluster: this,
markers: markersArea,
center: m.getLatLng(),
marker: m
});
} else {
zoomLevelAfter++;
}
this._map.setView(m.getLatLng(), zoomLevelAfter);
} else {
this._map.fitBounds(bounds);
}
}
});
return m;
},
BuildLeafletClusterIcon: function(cluster: PruneCluster.Cluster): L.Icon {
var c = 'prunecluster prunecluster-';
var iconSize = 38;
var maxPopulation = this.Cluster.GetPopulation();
if (cluster.population < Math.max(10, maxPopulation*0.01)) {
c += 'small';
} else if (cluster.population < Math.max(100, maxPopulation * 0.05)) {
c += 'medium';
iconSize = 40;
} else {
c += 'large';
iconSize = 44;
}
return new L.DivIcon({
html: "" + cluster.population + "
",
className: c,
iconSize: L.point(iconSize, iconSize)
});
},
BuildLeafletMarker: function (marker: PruneCluster.Marker, position: L.LatLng): L.Marker {
var m = new L.Marker(position);
this.PrepareLeafletMarker(m, marker.data, marker.category);
return m;
},
PrepareLeafletMarker: (marker: L.Marker, data: any, category: number) => {
if (data.icon) {
if (typeof data.icon === 'function') {
marker.setIcon(data.icon(data, category));
} else {
marker.setIcon(data.icon);
}
}
if (data.popup) {
var content = typeof data.popup === 'function' ? data.popup(data, category) : data.popup;
if (marker.getPopup()) {
marker.setPopupContent(content, data.popupOptions);
} else {
marker.bindPopup(content, data.popupOptions);
}
}
},
onAdd: function(map: L.Map) {
this._map = map;
map.on('movestart', this._moveStart, this);
map.on('moveend', this._moveEnd, this);
map.on('zoomend', this._zoomStart, this);
map.on('zoomend', this._zoomEnd, this);
this.ProcessView();
map.addLayer(this.spiderfier);
},
onRemove: function(map: L.Map) {
map.off('movestart', this._moveStart, this);
map.off('moveend', this._moveEnd, this);
map.off('zoomend', this._zoomStart, this);
map.off('zoomend', this._zoomEnd, this);
for (var i = 0, l = this._objectsOnMap.length; i < l; ++i) {
map.removeLayer(this._objectsOnMap[i].data._leafletMarker);
}
this._objectsOnMap = [];
this.Cluster.ResetClusters();
map.removeLayer(this.spiderfier);
this._map = null;
},
_moveStart: function() {
this._moveInProgress = true;
},
_moveEnd: function(e) {
this._moveInProgress = false;
this._hardMove = e.hard;
this.ProcessView();
},
_zoomStart: function() {
this._zoomInProgress = true;
},
_zoomEnd: function() {
this._zoomInProgress = false;
this.ProcessView();
},
ProcessView: function () {
// Don't do anything during the map manipulation
if (!this._map || this._zoomInProgress || this._moveInProgress) {
return;
}
var map = this._map,
bounds = map.getBounds(),
zoom = Math.floor(map.getZoom()),
marginRatio = this.clusterMargin / this.Cluster.Size,
resetIcons = this._resetIcons;
var southWest = bounds.getSouthWest(),
northEast = bounds.getNorthEast();
// First step : Compute the clusters
var clusters: PruneCluster.Cluster[] = this.Cluster.ProcessView({
minLat: southWest.lat,
minLng: southWest.lng,
maxLat: northEast.lat,
maxLng: northEast.lng
});
var objectsOnMap: PruneCluster.Cluster[] = this._objectsOnMap,
newObjectsOnMap: PruneCluster.Cluster[] = [],
markersOnMap: PruneCluster.LeafletMarker[] = new Array(objectsOnMap.length);
// Second step : By default, all the leaflet markers should be removed
for (var i = 0, l = objectsOnMap.length; i < l; ++i) {
var marker = (objectsOnMap[i].data)._leafletMarker;
markersOnMap[i] = marker;
marker._removeFromMap = true;
}
var clusterCreationList: PruneCluster.Cluster[] = [];
var clusterCreationListPopOne: PruneCluster.Cluster[] = [];
var opacityUpdateList = [];
// Third step : anti collapsing system
// => merge collapsing cluster using a sweep and prune algorithm
var workingList: PruneCluster.Cluster[] = [];
for (i = 0, l = clusters.length; i < l; ++i) {
var icluster = clusters[i],
iclusterData = icluster.data;
var latMargin = (icluster.bounds.maxLat - icluster.bounds.minLat) * marginRatio,
lngMargin = (icluster.bounds.maxLng - icluster.bounds.minLng) * marginRatio;
for (var j = 0, ll = workingList.length; j < ll; ++j) {
var c = workingList[j];
if (c.bounds.maxLng < icluster.bounds.minLng) {
workingList.splice(j, 1);
--j;
--ll;
continue;
}
var oldMaxLng = c.averagePosition.lng + lngMargin,
oldMinLat = c.averagePosition.lat - latMargin,
oldMaxLat = c.averagePosition.lat + latMargin,
newMinLng = icluster.averagePosition.lng - lngMargin,
newMinLat = icluster.averagePosition.lat - latMargin,
newMaxLat = icluster.averagePosition.lat + latMargin;
// Collapsing detected
if (oldMaxLng > newMinLng && oldMaxLat > newMinLat && oldMinLat < newMaxLat) {
iclusterData._leafletCollision = true;
c.ApplyCluster(icluster);
break;
}
}
// If the object is not in collision, we keep it in the process
if (!iclusterData._leafletCollision) {
workingList.push(icluster);
}
}
// Fourth step : update the already existing leaflet markers and create
// a list of required new leaflet markers
clusters.forEach((cluster: PruneCluster.Cluster) => {
var m = undefined;
var data = cluster.data;
// Ignore collapsing clusters detected by the previous step
if (data._leafletCollision) {
// Reset these clusters
data._leafletCollision = false;
data._leafletOldPopulation = 0;
data._leafletOldHashCode = 0;
return;
}
var position = new L.LatLng(cluster.averagePosition.lat, cluster.averagePosition.lng);
// If the cluster is already attached to a leaflet marker
var oldMarker = data._leafletMarker;
if (oldMarker) {
// If it's a single marker and it doesn't have changed
if (cluster.population === 1 && data._leafletOldPopulation === 1 && cluster.hashCode === oldMarker._hashCode) {
// Update if the zoom level has changed or if we need to reset the icon
if (resetIcons || oldMarker._zoomLevel !== zoom || cluster.lastMarker.data.forceIconRedraw) {
this.PrepareLeafletMarker(
oldMarker,
cluster.lastMarker.data,
cluster.lastMarker.category);
if (cluster.lastMarker.data.forceIconRedraw) {
cluster.lastMarker.data.forceIconRedraw = false;
}
}
// Update the position
oldMarker.setLatLng(position);
m = oldMarker;
// If it's a cluster marker on the same position
} else if (cluster.population > 1 && data._leafletOldPopulation > 1 && (oldMarker._zoomLevel === zoom ||
data._leafletPosition.equals(position))) {
// Update the position
oldMarker.setLatLng(position);
// Update the icon if the population of his content has changed or if we need to reset the icon
if (resetIcons || cluster.population != data._leafletOldPopulation ||
cluster.hashCode !== data._leafletOldHashCode) {
var boundsCopy = {};
L.Util.extend(boundsCopy, cluster.bounds);
(oldMarker)._leafletClusterBounds = boundsCopy;
oldMarker.setIcon(this.BuildLeafletClusterIcon(cluster));
}
data._leafletOldPopulation = cluster.population;
data._leafletOldHashCode = cluster.hashCode;
m = oldMarker;
}
}
// If a leaflet marker is unfound,
// register it in the creation waiting list
if (!m) {
// Clusters with a single marker are placed at the beginning
// of the cluster creation list, to recycle them in priority
if (cluster.population === 1) {
clusterCreationListPopOne.push(cluster);
} else {
clusterCreationList.push(cluster);
}
data._leafletPosition = position;
data._leafletOldPopulation = cluster.population;
data._leafletOldHashCode = cluster.hashCode;
} else {
// The leafet marker is used, we don't need to remove it anymore
m._removeFromMap = false;
newObjectsOnMap.push(cluster);
// Update the properties
m._zoomLevel = zoom;
m._hashCode = cluster.hashCode;
m._population = cluster.population;
data._leafletMarker = m;
data._leafletPosition = position;
}
});
// Fifth step : recycle leaflet markers using a sweep and prune algorithm
// The purpose of this step is to make smooth transition when a cluster or a marker
// is moving on the map and its grid cell changes
clusterCreationList = clusterCreationListPopOne.concat(clusterCreationList);
for (i = 0, l = objectsOnMap.length; i < l; ++i) {
icluster = objectsOnMap[i];
var idata = icluster.data;
marker = idata._leafletMarker;
// We do not recycle markers already in use
if (idata._leafletMarker._removeFromMap) {
// If the sweep and prune algorithm doesn't find anything,
// the leaflet marker can't be recycled and it will be removed
var remove = true;
// Recycle marker only with the same zoom level
if (marker._zoomLevel === zoom) {
var pa = icluster.averagePosition;
latMargin = (icluster.bounds.maxLat - icluster.bounds.minLat) * marginRatio,
lngMargin = (icluster.bounds.maxLng - icluster.bounds.minLng) * marginRatio;
for (j = 0, ll = clusterCreationList.length; j < ll; ++j) {
var jcluster = clusterCreationList[j],
jdata = jcluster.data;
// If luckily it's the same single marker
if (marker._population === 1 && jcluster.population === 1 &&
marker._hashCode === jcluster.hashCode) {
// I we need to reset the icon
if (resetIcons || jcluster.lastMarker.data.forceIconRedraw) {
this.PrepareLeafletMarker(
marker,
jcluster.lastMarker.data,
jcluster.lastMarker.category);
if (jcluster.lastMarker.data.forceIconRedraw) {
jcluster.lastMarker.data.forceIconRedraw = false;
}
}
// Update the position
marker.setLatLng(jdata._leafletPosition);
remove = false;
} else {
var pb = jcluster.averagePosition;
var oldMinLng = pa.lng - lngMargin,
newMaxLng = pb.lng + lngMargin;
oldMaxLng = pa.lng + lngMargin;
oldMinLat = pa.lat - latMargin;
oldMaxLat = pa.lat + latMargin;
newMinLng = pb.lng - lngMargin;
newMinLat = pb.lat - latMargin;
newMaxLat = pb.lat + latMargin;
// If it's a cluster marker
// and if a collapsing leaflet marker is found, it may be recycled
if ((marker._population > 1 && jcluster.population > 1) &&
(oldMaxLng > newMinLng && oldMinLng < newMaxLng && oldMaxLat > newMinLat && oldMinLat < newMaxLat)) {
// Update everything
marker.setLatLng(jdata._leafletPosition);
marker.setIcon(this.BuildLeafletClusterIcon(jcluster));
var poisson = {};
L.Util.extend(poisson, jcluster.bounds);
(marker)._leafletClusterBounds = poisson;
jdata._leafletOldPopulation = jcluster.population;
jdata._leafletOldHashCode = jcluster.hashCode;
marker._population = jcluster.population;
remove = false;
}
}
// If the leaflet marker is recycled
if (!remove) {
// Register the new marker
jdata._leafletMarker = marker;
marker._removeFromMap = false;
newObjectsOnMap.push(jcluster);
// Remove it from the sweep and prune working list
clusterCreationList.splice(j, 1);
--j;
--ll;
break;
}
}
}
// If sadly the leaflet marker can't be recycled
if (remove) {
if (!marker._removeFromMap) console.error("wtf");
}
}
}
// Sixth step : Create the new leaflet markers
for (i = 0, l = clusterCreationList.length; i < l; ++i) {
icluster = clusterCreationList[i],
idata = icluster.data;
var iposition = idata._leafletPosition;
var creationMarker: any;
if (icluster.population === 1) {
creationMarker = this.BuildLeafletMarker(icluster.lastMarker, iposition);
} else {
creationMarker = this.BuildLeafletCluster(icluster, iposition);
}
creationMarker.addTo(map);
// Fading in transition
// (disabled by default with no-anim)
// if(creationMarker._icon) L.DomUtil.addClass(creationMarker._icon, "no-anim");
creationMarker.setOpacity(0);
opacityUpdateList.push(creationMarker);
idata._leafletMarker = creationMarker;
creationMarker._zoomLevel = zoom;
creationMarker._hashCode = icluster.hashCode;
creationMarker._population = icluster.population;
newObjectsOnMap.push(icluster);
}
// Start the fading in transition
window.setTimeout(() => {
for (i = 0, l = opacityUpdateList.length; i < l; ++i) {
var m = opacityUpdateList[i];
if(m._icon) L.DomUtil.addClass(m._icon, "prunecluster-anim");
if(m._shadow) L.DomUtil.addClass(m._shadow, "prunecluster-anim");
m.setOpacity(1);
}
}, 1);
// Remove the remaining unused markers
if (this._hardMove) {
for (i = 0, l = markersOnMap.length; i < l; ++i) {
marker = markersOnMap[i];
if (marker._removeFromMap) {
map.removeLayer(marker);
}
}
} else {
if (this._removeTimeoutId !== 0) {
window.clearTimeout(this._removeTimeoutId);
for (i = 0, l = this._markersRemoveListTimeout.length; i < l; ++i) {
map.removeLayer(this._markersRemoveListTimeout[i]);
}
}
var toRemove = [];
for (i = 0, l = markersOnMap.length; i < l; ++i) {
marker = markersOnMap[i];
if (marker._removeFromMap) {
marker.setOpacity(0);
toRemove.push(marker);
}
}
if (toRemove.length > 0) {
this._removeTimeoutId = window.setTimeout(() => {
for (i = 0, l = toRemove.length; i < l; ++i) {
map.removeLayer(toRemove[i]);
}
this._removeTimeoutId = 0;
}, 300);
}
this._markersRemoveListTimeout = toRemove;
}
this._objectsOnMap = newObjectsOnMap;
this._hardMove = false;
this._resetIcons = false;
},
FitBounds: function(withFiltered: boolean = true) {
var bounds: PruneCluster.Bounds = this.Cluster.ComputeGlobalBounds(withFiltered);
if (bounds) {
this._map.fitBounds(new L.LatLngBounds(
new L.LatLng(bounds.minLat, bounds.maxLng),
new L.LatLng(bounds.maxLat, bounds.minLng)));
}
},
GetMarkers: function() {
return this.Cluster.GetMarkers();
},
RedrawIcons: function (processView: boolean = true) {
this._resetIcons = true;
if (processView) {
this.ProcessView();
}
}
});