UNPKG

leaflet.layergroup.collision

Version:

Leaflet plugin for uncluttering L.Markers using basic collision detection.

238 lines (189 loc) 6.33 kB
var isMSIE8 = !('getComputedStyle' in window && typeof window.getComputedStyle === 'function') function extensions(parentClass) { return { initialize: function (options) { parentClass.prototype.initialize.call(this, options); this._originalLayers = []; this._visibleLayers = []; this._staticLayers = []; this._rbush = []; this._cachedRelativeBoxes = []; this._margin = options.margin || 0; this._rbush = null; }, addLayer: function(layer) { if ( !('options' in layer) || !('icon' in layer.options)) { this._staticLayers.push(layer); parentClass.prototype.addLayer.call(this, layer); return; } this._originalLayers.push(layer); if (this._map) { this._maybeAddLayerToRBush( layer ); } }, removeLayer: function(layer) { this._rbush.remove(this._cachedRelativeBoxes[layer._leaflet_id]); delete this._cachedRelativeBoxes[layer._leaflet_id]; parentClass.prototype.removeLayer.call(this,layer); var i; i = this._originalLayers.indexOf(layer); if (i !== -1) { this._originalLayers.splice(i,1); } i = this._visibleLayers.indexOf(layer); if (i !== -1) { this._visibleLayers.splice(i,1); } i = this._staticLayers.indexOf(layer); if (i !== -1) { this._staticLayers.splice(i,1); } }, clearLayers: function() { this._rbush = rbush(); this._originalLayers = []; this._visibleLayers = []; this._staticLayers = []; this._cachedRelativeBoxes = []; parentClass.prototype.clearLayers.call(this); }, onAdd: function (map) { this._map = map; for (var i in this._staticLayers) { map.addLayer(this._staticLayers[i]); } this._onZoomEnd(); map.on('zoomend', this._onZoomEnd, this); }, onRemove: function(map) { for (var i in this._staticLayers) { map.removeLayer(this._staticLayers[i]); } map.off('zoomend', this._onZoomEnd, this); parentClass.prototype.onRemove.call(this, map); }, _maybeAddLayerToRBush: function(layer) { var z = this._map.getZoom(); var bush = this._rbush; var boxes = this._cachedRelativeBoxes[layer._leaflet_id]; var visible = false; if (!boxes) { // Add the layer to the map so it's instantiated on the DOM, // in order to fetch its position and size. parentClass.prototype.addLayer.call(this, layer); var visible = true; // var htmlElement = layer._icon; var box = this._getIconBox(layer._icon); boxes = this._getRelativeBoxes(layer._icon.children, box); boxes.push(box); this._cachedRelativeBoxes[layer._leaflet_id] = boxes; } boxes = this._positionBoxes(this._map.latLngToLayerPoint(layer.getLatLng()),boxes); var collision = false; for (var i=0; i<boxes.length && !collision; i++) { collision = bush.search(boxes[i]).length > 0; } if (!collision) { if (!visible) { parentClass.prototype.addLayer.call(this, layer); } this._visibleLayers.push(layer); bush.load(boxes); } else { parentClass.prototype.removeLayer.call(this, layer); } }, // Returns a plain array with the relative dimensions of a L.Icon, based // on the computed values from iconSize and iconAnchor. _getIconBox: function (el) { if (isMSIE8) { // Fallback for MSIE8, will most probably fail on edge cases return [ 0, 0, el.offsetWidth, el.offsetHeight]; } var styles = window.getComputedStyle(el); // getComputedStyle() should return values already in pixels, so using parseInt() // is not as much as a hack as it seems to be. return [ parseInt(styles.marginLeft), parseInt(styles.marginTop), parseInt(styles.marginLeft) + parseInt(styles.width), parseInt(styles.marginTop) + parseInt(styles.height) ]; }, // Much like _getIconBox, but works for positioned HTML elements, based on offsetWidth/offsetHeight. _getRelativeBoxes: function(els,baseBox) { var boxes = []; for (var i=0; i<els.length; i++) { var el = els[i]; var box = [ el.offsetLeft, el.offsetTop, el.offsetLeft + el.offsetWidth, el.offsetTop + el.offsetHeight ]; box = this._offsetBoxes(box, baseBox); boxes.push( box ); if (el.children.length) { var parentBox = baseBox; if (!isMSIE8) { var positionStyle = window.getComputedStyle(el).position; if (positionStyle === 'absolute' || positionStyle === 'relative') { parentBox = box; } } boxes = boxes.concat( this._getRelativeBoxes(el.children, parentBox) ); } } return boxes; }, _offsetBoxes: function(a,b){ return [ a[0] + b[0], a[1] + b[1], a[2] + b[0], a[3] + b[1] ]; }, // Adds the coordinate of the layer (in pixels / map canvas units) to each box coordinate. _positionBoxes: function(offset, boxes) { var newBoxes = []; // Must be careful to not overwrite references to the original ones. for (var i=0; i<boxes.length; i++) { newBoxes.push( this._positionBox( offset, boxes[i] ) ); } return newBoxes; }, _positionBox: function(offset, box) { return [ box[0] + offset.x - this._margin, box[1] + offset.y - this._margin, box[2] + offset.x + this._margin, box[3] + offset.y + this._margin, ] }, _onZoomEnd: function() { for (var i=0; i<this._visibleLayers.length; i++) { parentClass.prototype.removeLayer.call(this, this._visibleLayers[i]); } this._rbush = rbush(); for (var i=0; i < this._originalLayers.length; i++) { this._maybeAddLayerToRBush(this._originalLayers[i]); } } }}; L.LayerGroup.Collision = L.LayerGroup.extend(extensions( L.LayerGroup )); L.FeatureGroup.Collision = L.FeatureGroup.extend(extensions( L.FeatureGroup )); L.GeoJSON.Collision = L.GeoJSON.extend(extensions( L.GeoJSON )); // Uppercase factories only for backwards compatibility: L.LayerGroup.collision = function (options) { return new L.LayerGroup.Collision(options || {}); }; L.FeatureGroup.collision = function (options) { return new L.FeatureGroup.Collision(options || {}); }; L.GeoJSON.collision = function (options) { return new L.GeoJSON.Collision(options || {}); }; // Factories should always be lowercase, like this: L.layerGroup.collision = function (options) { return new L.LayerGroup.Collision(options || {}); }; L.featureGroup.collision = function (options) { return new L.FeatureGroup.Collision(options || {}); }; L.geoJson.collision = function (options) { return new L.GeoJSON.Collision(options || {}); };