leaflet
Version:
JavaScript library for mobile-friendly interactive maps
376 lines (295 loc) • 9.8 kB
JavaScript
/*
* @class Canvas
* @inherits Renderer
* @aka L.Canvas
*
* Allows vector layers to be displayed with [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API).
* Inherits `Renderer`.
*
* Due to [technical limitations](http://caniuse.com/#search=canvas), Canvas is not
* available in all web browsers, notably IE8, and overlapping geometries might
* not display properly in some edge cases.
*
* @example
*
* Use Canvas by default for all paths in the map:
*
* ```js
* var map = L.map('map', {
* renderer: L.canvas()
* });
* ```
*
* Use a Canvas renderer with extra padding for specific vector geometries:
*
* ```js
* var map = L.map('map');
* var myRenderer = L.canvas({ padding: 0.5 });
* var line = L.polyline( coordinates, { renderer: myRenderer } );
* var circle = L.circle( center, { renderer: myRenderer } );
* ```
*/
L.Canvas = L.Renderer.extend({
onAdd: function () {
L.Renderer.prototype.onAdd.call(this);
this._layers = this._layers || {};
// Redraw vectors since canvas is cleared upon removal,
// in case of removing the renderer itself from the map.
this._draw();
},
_initContainer: function () {
var container = this._container = document.createElement('canvas');
L.DomEvent
.on(container, 'mousemove', L.Util.throttle(this._onMouseMove, 32, this), this)
.on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this)
.on(container, 'mouseout', this._handleMouseOut, this);
this._ctx = container.getContext('2d');
},
_update: function () {
if (this._map._animatingZoom && this._bounds) { return; }
this._drawnLayers = {};
L.Renderer.prototype._update.call(this);
var b = this._bounds,
container = this._container,
size = b.getSize(),
m = L.Browser.retina ? 2 : 1;
L.DomUtil.setPosition(container, b.min);
// set canvas size (also clearing it); use double size on retina
container.width = m * size.x;
container.height = m * size.y;
container.style.width = size.x + 'px';
container.style.height = size.y + 'px';
if (L.Browser.retina) {
this._ctx.scale(2, 2);
}
// translate so we use the same path coordinates after canvas element moves
this._ctx.translate(-b.min.x, -b.min.y);
// Tell paths to redraw themselves
this.fire('update');
},
_initPath: function (layer) {
this._updateDashArray(layer);
this._layers[L.stamp(layer)] = layer;
},
_addPath: L.Util.falseFn,
_removePath: function (layer) {
layer._removed = true;
this._requestRedraw(layer);
},
_updatePath: function (layer) {
this._redrawBounds = layer._pxBounds;
this._draw(true);
layer._project();
layer._update();
this._draw();
this._redrawBounds = null;
},
_updateStyle: function (layer) {
this._updateDashArray(layer);
this._requestRedraw(layer);
},
_updateDashArray: function (layer) {
if (layer.options.dashArray) {
var parts = layer.options.dashArray.split(','),
dashArray = [],
i;
for (i = 0; i < parts.length; i++) {
dashArray.push(Number(parts[i]));
}
layer.options._dashArray = dashArray;
}
},
_requestRedraw: function (layer) {
if (!this._map) { return; }
var padding = (layer.options.weight || 0) + 1;
this._redrawBounds = this._redrawBounds || new L.Bounds();
this._redrawBounds.extend(layer._pxBounds.min.subtract([padding, padding]));
this._redrawBounds.extend(layer._pxBounds.max.add([padding, padding]));
this._redrawRequest = this._redrawRequest || L.Util.requestAnimFrame(this._redraw, this);
},
_redraw: function () {
this._redrawRequest = null;
this._draw(true); // clear layers in redraw bounds
this._draw(); // draw layers
this._redrawBounds = null;
},
_draw: function (clear) {
this._clear = clear;
var layer, bounds = this._redrawBounds;
this._ctx.save();
if (bounds) {
this._ctx.beginPath();
this._ctx.rect(bounds.min.x, bounds.min.y, bounds.max.x - bounds.min.x, bounds.max.y - bounds.min.y);
this._ctx.clip();
}
for (var id in this._layers) {
layer = this._layers[id];
if (!bounds || (layer._pxBounds && layer._pxBounds.intersects(bounds))) {
layer._updatePath();
}
if (clear && layer._removed) {
delete layer._removed;
delete this._layers[id];
}
}
this._ctx.restore(); // Restore state before clipping.
},
_updatePoly: function (layer, closed) {
var i, j, len2, p,
parts = layer._parts,
len = parts.length,
ctx = this._ctx;
if (!len) { return; }
this._drawnLayers[layer._leaflet_id] = layer;
ctx.beginPath();
if (ctx.setLineDash) {
ctx.setLineDash(layer.options && layer.options._dashArray || []);
}
for (i = 0; i < len; i++) {
for (j = 0, len2 = parts[i].length; j < len2; j++) {
p = parts[i][j];
ctx[j ? 'lineTo' : 'moveTo'](p.x, p.y);
}
if (closed) {
ctx.closePath();
}
}
this._fillStroke(ctx, layer);
// TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature
},
_updateCircle: function (layer) {
if (layer._empty()) { return; }
var p = layer._point,
ctx = this._ctx,
r = layer._radius,
s = (layer._radiusY || r) / r;
this._drawnLayers[layer._leaflet_id] = layer;
if (s !== 1) {
ctx.save();
ctx.scale(1, s);
}
ctx.beginPath();
ctx.arc(p.x, p.y / s, r, 0, Math.PI * 2, false);
if (s !== 1) {
ctx.restore();
}
this._fillStroke(ctx, layer);
},
_fillStroke: function (ctx, layer) {
var clear = this._clear,
options = layer.options;
ctx.globalCompositeOperation = clear ? 'destination-out' : 'source-over';
if (options.fill) {
ctx.globalAlpha = clear ? 1 : options.fillOpacity;
ctx.fillStyle = options.fillColor || options.color;
ctx.fill(options.fillRule || 'evenodd');
}
if (options.stroke && options.weight !== 0) {
ctx.globalAlpha = clear ? 1 : options.opacity;
// if clearing shape, do it with the previously drawn line width
layer._prevWeight = ctx.lineWidth = clear ? layer._prevWeight + 1 : options.weight;
ctx.strokeStyle = options.color;
ctx.lineCap = options.lineCap;
ctx.lineJoin = options.lineJoin;
ctx.stroke();
}
},
// Canvas obviously doesn't have mouse events for individual drawn objects,
// so we emulate that by calculating what's under the mouse on mousemove/click manually
_onClick: function (e) {
var point = this._map.mouseEventToLayerPoint(e), layers = [], layer;
for (var id in this._layers) {
layer = this._layers[id];
if (layer.options.interactive && layer._containsPoint(point) && !this._map._draggableMoved(layer)) {
L.DomEvent._fakeStop(e);
layers.push(layer);
}
}
if (layers.length) {
this._fireEvent(layers, e);
}
},
_onMouseMove: function (e) {
if (!this._map || this._map.dragging.moving() || this._map._animatingZoom) { return; }
var point = this._map.mouseEventToLayerPoint(e);
this._handleMouseOut(e, point);
this._handleMouseHover(e, point);
},
_handleMouseOut: function (e, point) {
var layer = this._hoveredLayer;
if (layer && (e.type === 'mouseout' || !layer._containsPoint(point))) {
// if we're leaving the layer, fire mouseout
L.DomUtil.removeClass(this._container, 'leaflet-interactive');
this._fireEvent([layer], e, 'mouseout');
this._hoveredLayer = null;
}
},
_handleMouseHover: function (e, point) {
var id, layer;
for (id in this._drawnLayers) {
layer = this._drawnLayers[id];
if (layer.options.interactive && layer._containsPoint(point)) {
L.DomUtil.addClass(this._container, 'leaflet-interactive'); // change cursor
this._fireEvent([layer], e, 'mouseover');
this._hoveredLayer = layer;
}
}
if (this._hoveredLayer) {
this._fireEvent([this._hoveredLayer], e);
}
},
_fireEvent: function (layers, e, type) {
this._map._fireDOMEvent(e, type || e.type, layers);
},
// TODO _bringToFront & _bringToBack, pretty tricky
_bringToFront: L.Util.falseFn,
_bringToBack: L.Util.falseFn
});
// @namespace Browser; @property canvas: Boolean
// `true` when the browser supports [`<canvas>`](https://developer.mozilla.org/docs/Web/API/Canvas_API).
L.Browser.canvas = (function () {
return !!document.createElement('canvas').getContext;
}());
// @namespace Canvas
// @factory L.canvas(options?: Renderer options)
// Creates a Canvas renderer with the given options.
L.canvas = function (options) {
return L.Browser.canvas ? new L.Canvas(options) : null;
};
L.Polyline.prototype._containsPoint = function (p, closed) {
var i, j, k, len, len2, part,
w = this._clickTolerance();
if (!this._pxBounds.contains(p)) { return false; }
// hit detection for polylines
for (i = 0, len = this._parts.length; i < len; i++) {
part = this._parts[i];
for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
if (!closed && (j === 0)) { continue; }
if (L.LineUtil.pointToSegmentDistance(p, part[k], part[j]) <= w) {
return true;
}
}
}
return false;
};
L.Polygon.prototype._containsPoint = function (p) {
var inside = false,
part, p1, p2, i, j, k, len, len2;
if (!this._pxBounds.contains(p)) { return false; }
// ray casting algorithm for detecting if point is in polygon
for (i = 0, len = this._parts.length; i < len; i++) {
part = this._parts[i];
for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
p1 = part[j];
p2 = part[k];
if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) {
inside = !inside;
}
}
}
// also check if it's on polygon stroke
return inside || L.Polyline.prototype._containsPoint.call(this, p, true);
};
L.CircleMarker.prototype._containsPoint = function (p) {
return p.distanceTo(this._point) <= this._radius + this._clickTolerance();
};