leaflet-canvas-marker
Version:
Leaflet plugin to display markers on canvas instead of DOM
406 lines (282 loc) • 12.4 kB
JavaScript
'use strict';
function layerFactory(L) {
var CanvasIconLayer = (L.Layer ? L.Layer : L.Class).extend({
//Add event listeners to initialized section.
initialize: function (options) {
L.setOptions(this, options);
this._onClickListeners = [];
this._onHoverListeners = [];
},
setOptions: function (options) {
L.setOptions(this, options);
return this.redraw();
},
redraw: function () {
this._redraw(true);
},
//Multiple layers at a time for rBush performance
addMarkers: function (markers) {
var self = this;
var tmpMark = [];
var tmpLatLng = [];
markers.forEach(function (marker) {
if (!((marker.options.pane == 'markerPane') && marker.options.icon))
{
console.error('Layer isn\'t a marker');
return;
}
var latlng = marker.getLatLng();
var isDisplaying = self._map.getBounds().contains(latlng);
var s = self._addMarker(marker,latlng,isDisplaying);
//Only add to Point Lookup if we are on map
if (isDisplaying ===true) tmpMark.push(s[0]);
tmpLatLng.push(s[1]);
});
self._markers.load(tmpMark);
self._latlngMarkers.load(tmpLatLng);
},
//Adds single layer at a time. Less efficient for rBush
addMarker: function (marker) {
var self = this;
var latlng = marker.getLatLng();
var isDisplaying = self._map.getBounds().contains(latlng);
var dat = self._addMarker(marker,latlng,isDisplaying);
//Only add to Point Lookup if we are on map
if(isDisplaying ===true) self._markers.insert(dat[0]);
self._latlngMarkers.insert(dat[1]);
},
addLayer: function (layer) {
if ((layer.options.pane == 'markerPane') && layer.options.icon) this.addMarker(layer);
else console.error('Layer isn\'t a marker');
},
addLayers: function (layers) {
this.addMarkers(layers);
},
removeLayer: function (layer) {
this.removeMarker(layer,true);
},
removeMarker: function (marker,redraw) {
var self = this;
//If we are removed point
if(marker["minX"]) marker = marker.data;
var latlng = marker.getLatLng();
var isDisplaying = self._map.getBounds().contains(latlng);
var markerData = {
minX: latlng.lng,
minY: latlng.lat,
maxX: latlng.lng,
maxY: latlng.lat,
data: marker
};
self._latlngMarkers.remove(markerData, function (a,b) {
return a.data._leaflet_id ===b.data._leaflet_id;
});
self._latlngMarkers.total--;
self._latlngMarkers.dirty++;
if(isDisplaying ===true && redraw ===true) {
self._redraw(true);
}
},
onAdd: function (map) {
this._map = map;
if (!this._canvas) this._initCanvas();
if (this.options.pane) this.getPane().appendChild(this._canvas);
else map._panes.overlayPane.appendChild(this._canvas);
map.on('moveend', this._reset, this);
map.on('resize',this._reset,this);
map.on('click', this._executeListeners, this);
map.on('mousemove', this._executeListeners, this);
},
onRemove: function (map) {
if (this.options.pane) this.getPane().removeChild(this._canvas);
else map.getPanes().overlayPane.removeChild(this._canvas);
},
addTo: function (map) {
map.addLayer(this);
return this;
},
_addMarker: function(marker,latlng,isDisplaying) {
var self = this;
//Needed for pop-up & tooltip to work.
marker._map = self._map;
//_markers contains Points of markers currently displaying on map
if (!self._markers) self._markers = new rbush();
//_latlngMarkers contains Lat\Long coordinates of all markers in layer.
if (!self._latlngMarkers) {
self._latlngMarkers = new rbush();
self._latlngMarkers.dirty=0;
self._latlngMarkers.total=0;
}
L.Util.stamp(marker);
var pointPos = self._map.latLngToContainerPoint(latlng);
var iconSize = marker.options.icon.options.iconSize;
var adj_x = iconSize[0]/2;
var adj_y = iconSize[1]/2;
var ret = [({
minX: (pointPos.x - adj_x),
minY: (pointPos.y - adj_y),
maxX: (pointPos.x + adj_x),
maxY: (pointPos.y + adj_y),
data: marker
}),({
minX: latlng.lng,
minY: latlng.lat,
maxX: latlng.lng,
maxY: latlng.lat,
data: marker
})];
self._latlngMarkers.dirty++;
self._latlngMarkers.total++;
//Only draw if we are on map
if(isDisplaying===true) self._drawMarker(marker, pointPos);
return ret;
},
_drawMarker: function (marker, pointPos) {
var self = this;
if (!this._imageLookup) this._imageLookup = {};
if (!pointPos) {
pointPos = self._map.latLngToContainerPoint(marker.getLatLng());
}
var iconUrl = marker.options.icon.options.iconUrl;
if (marker.canvas_img) {
self._drawImage(marker, pointPos);
}
else {
if(self._imageLookup[iconUrl]) {
marker.canvas_img = self._imageLookup[iconUrl][0];
if (self._imageLookup[iconUrl][1] ===false) {
self._imageLookup[iconUrl][2].push([marker,pointPos]);
}
else {
self._drawImage(marker,pointPos);
}
}
else {
var i = new Image();
i.src = iconUrl;
marker.canvas_img = i;
//Image,isLoaded,marker\pointPos ref
self._imageLookup[iconUrl] = [i, false, [[marker, pointPos]]];
i.onload = function() {
self._imageLookup[iconUrl][1] = true;
self._imageLookup[iconUrl][2].forEach(function (e) {
self._drawImage(e[0],e[1]);
});
}
}
}
},
_drawImage: function (marker, pointPos) {
var options = marker.options.icon.options;
this._context.drawImage(
marker.canvas_img,
pointPos.x - options.iconAnchor[0],
pointPos.y - options.iconAnchor[1],
options.iconSize[0],
options.iconSize[1]
);
},
_reset: function () {
var topLeft = this._map.containerPointToLayerPoint([0, 0]);
L.DomUtil.setPosition(this._canvas, topLeft);
var size = this._map.getSize();
this._canvas.width = size.x;
this._canvas.height = size.y;
this._redraw();
},
_redraw: function (clear) {
var self = this;
if (!this._map) return;
if (clear) this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
var tmp = [];
//If we are 10% individual inserts\removals, reconstruct lookup for efficiency
if (self._latlngMarkers.dirty/self._latlngMarkers.total >= .1) {
self._latlngMarkers.all().forEach(function(e) {
tmp.push(e);
});
self._latlngMarkers.clear();
self._latlngMarkers.load(tmp);
self._latlngMarkers.dirty=0;
tmp = [];
}
var mapBounds = self._map.getBounds();
//Only re-draw what we are showing on the map.
var mapBoxCoords = {
minX: mapBounds.getWest(),
minY: mapBounds.getSouth(),
maxX: mapBounds.getEast(),
maxY: mapBounds.getNorth(),
};
self._latlngMarkers.search(mapBoxCoords).forEach(function (e) {
//Readjust Point Map
var pointPos = self._map.latLngToContainerPoint(e.data.getLatLng());
var iconSize = e.data.options.icon.options.iconSize;
var adj_x = iconSize[0]/2;
var adj_y = iconSize[1]/2;
var newCoords = {
minX: (pointPos.x - adj_x),
minY: (pointPos.y - adj_y),
maxX: (pointPos.x + adj_x),
maxY: (pointPos.y + adj_y),
data: e.data
}
tmp.push(newCoords);
//Redraw points
self._drawMarker(e.data, pointPos);
});
//Clear rBush & Bulk Load for performance
this._markers.clear();
this._markers.load(tmp);
},
_initCanvas: function () {
this._canvas = L.DomUtil.create('canvas', 'leaflet-canvas-icon-layer leaflet-layer');
var originProp = L.DomUtil.testProp(['transformOrigin', 'WebkitTransformOrigin', 'msTransformOrigin']);
this._canvas.style[originProp] = '50% 50%';
var size = this._map.getSize();
this._canvas.width = size.x;
this._canvas.height = size.y;
this._context = this._canvas.getContext('2d');
var animated = this._map.options.zoomAnimation && L.Browser.any3d;
L.DomUtil.addClass(this._canvas, 'leaflet-zoom-' + (animated ? 'animated' : 'hide'));
},
addOnClickListener: function (listener) {
this._onClickListeners.push(listener);
},
addOnHoverListener: function (listener) {
this._onHoverListeners.push(listener);
},
_executeListeners: function (event) {
var me = this;
var x = event.containerPoint.x;
var y = event.containerPoint.y;
if(me._openToolTip) {
me._openToolTip.closeTooltip();
delete me._openToolTip;
}
var ret = this._markers.search({ minX: x, minY: y, maxX: x, maxY: y });
if (ret && ret.length > 0) {
me._map._container.style.cursor="pointer";
if (event.type==="click") {
var hasPopup = ret[0].data.getPopup();
if(hasPopup) ret[0].data.openPopup();
me._onClickListeners.forEach(function (listener) { listener(event, ret); });
}
if (event.type==="mousemove") {
var hasTooltip = ret[0].data.getTooltip();
if(hasTooltip) {
me._openToolTip = ret[0].data;
ret[0].data.openTooltip();
}
me._onHoverListeners.forEach(function (listener) { listener(event, ret); });
}
}
else {
me._map._container.style.cursor="";
}
}
});
L.canvasIconLayer = function (options) {
return new CanvasIconLayer(options);
};
};
module.exports = layerFactory;