UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

376 lines (295 loc) 9.8 kB
/* * @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(); };