/* * L.Canvas handles Canvas vector layers rendering and mouse events handling. All Canvas-specific code goes here. */ 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 this._draw(); }, _initContainer: function () { var container = this._container = document.createElement('canvas'); L.DomEvent .on(container, 'mousemove', this._onMouseMove, this) .on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this); this._ctx = container.getContext('2d'); }, _update: function () { if (this._map._animatingZoom) { return; } 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); }, _initPath: function (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(); this._draw(); this._redrawBounds = null; }, _updateStyle: function (layer) { this._requestRedraw(layer); }, _requestRedraw: function (layer) { if (!this._map) { return; } this._redrawBounds = this._redrawBounds || new L.Bounds(); this._redrawBounds.extend(layer._pxBounds.min).extend(layer._pxBounds.max); 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; for (var id in this._layers) { layer = this._layers[id]; if (!this._redrawBounds || layer._pxBounds.intersects(this._redrawBounds)) { layer._updatePath(); } if (clear && layer._removed) { delete layer._removed; delete this._layers[id]; } } }, _updatePoly: function (layer, closed) { var i, j, len2, p, parts = layer._parts, len = parts.length, ctx = this._ctx; if (!len) { return; } ctx.beginPath(); 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; 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('evenodd'); } if (options.stroke) { 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); for (var id in this._layers) { if (this._layers[id]._containsPoint(point)) { this._layers[id]._fireMouseEvent(e); } } }, _onMouseMove: function (e) { if (!this._map || this._map._animatingZoom) { return; } var point = this._map.mouseEventToLayerPoint(e); // TODO don't do on each move event, throttle since it's expensive for (var id in this._layers) { this._handleHover(this._layers[id], e, point); } }, _handleHover: function (layer, e, point) { if (!layer.options.clickable) { return; } if (layer._containsPoint(point)) { // if we just got inside the layer, fire mouseover if (!layer._mouseInside) { L.DomUtil.addClass(this._container, 'leaflet-clickable'); // change cursor layer._fireMouseEvent(e, 'mouseover'); layer._mouseInside = true; } // fire mousemove layer._fireMouseEvent(e); } else if (layer._mouseInside) { // if we're leaving the layer, fire mouseout L.DomUtil.removeClass(this._container, 'leaflet-clickable'); layer._fireMouseEvent(e, 'mouseout'); layer._mouseInside = false; } }, // TODO _bringToFront & _bringToBack, pretty tricky _bringToFront: L.Util.falseFn, _bringToBack: L.Util.falseFn }); L.Browser.canvas = (function () { return !!document.createElement('canvas').getContext; }()); L.canvas = function (options) { return L.Browser.canvas ? new L.Canvas(options) : null; }; L.Canvas.instance = L.canvas(); 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.Circle.prototype._containsPoint = function (p) { return p.distanceTo(this._point) <= this._radius + this._clickTolerance(); };