UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

508 lines (406 loc) 12.4 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({ getEvents: function () { var events = L.Renderer.prototype.getEvents.call(this); events.viewprereset = this._onViewPreReset; return events; }, _onViewPreReset: function () { // Set a flag so that a viewprereset+moveend+viewreset only updates&redraws once this._postponeUpdatePaths = true; }, onAdd: function () { L.Renderer.prototype.onAdd.call(this); // 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'); }, _updatePaths: function () { if (this._postponeUpdatePaths) { return; } var layer; this._redrawBounds = null; for (var id in this._layers) { layer = this._layers[id]; layer._update(); } this._redraw(); }, _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'); }, _reset: function () { L.Renderer.prototype._reset.call(this); if (this._postponeUpdatePaths) { this._postponeUpdatePaths = false; this._updatePaths(); } }, _initPath: function (layer) { this._updateDashArray(layer); this._layers[L.stamp(layer)] = layer; var order = layer._order = { layer: layer, prev: this._drawLast, next: null }; if (this._drawLast) { this._drawLast.next = order; } this._drawLast = order; this._drawFirst = this._drawFirst || this._drawLast; }, _addPath: function (layer) { this._requestRedraw(layer); }, _removePath: function (layer) { var order = layer._order; var next = order.next; var prev = order.prev; if (next) { next.prev = prev; } else { this._drawLast = prev; } if (prev) { prev.next = next; } else { this._drawFirst = next; } delete layer._order; delete this._layers[L.stamp(layer)]; this._requestRedraw(layer); }, _updatePath: function (layer) { // Redraw the union of the layer's old pixel // bounds and the new pixel bounds. this._extendRedrawBounds(layer); layer._project(); layer._update(); // The redraw will extend the redraw bounds // with the new pixel bounds. this._requestRedraw(layer); }, _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; } this._extendRedrawBounds(layer); this._redrawRequest = this._redrawRequest || L.Util.requestAnimFrame(this._redraw, this); }, _extendRedrawBounds: function (layer) { 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])); }, _redraw: function () { this._redrawRequest = null; if (this._redrawBounds) { this._redrawBounds.min._floor(); this._redrawBounds.max._ceil(); } this._clear(); // clear layers in redraw bounds this._draw(); // draw layers this._redrawBounds = null; }, _clear: function () { var bounds = this._redrawBounds; if (bounds) { var size = bounds.getSize(); this._ctx.clearRect(bounds.min.x, bounds.min.y, size.x, size.y); } else { this._ctx.clearRect(0, 0, this._container.width, this._container.height); } }, _draw: function () { var layer, bounds = this._redrawBounds; this._ctx.save(); if (bounds) { var size = bounds.getSize(); this._ctx.beginPath(); this._ctx.rect(bounds.min.x, bounds.min.y, size.x, size.y); this._ctx.clip(); } this._drawing = true; for (var order = this._drawFirst; order; order = order.next) { layer = order.layer; if (!bounds || (layer._pxBounds && layer._pxBounds.intersects(bounds))) { layer._updatePath(); } } this._drawing = false; this._ctx.restore(); // Restore state before clipping. }, _updatePoly: function (layer, closed) { if (!this._drawing) { return; } 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 (!this._drawing || 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 options = layer.options; if (options.fill) { ctx.globalAlpha = options.fillOpacity; ctx.fillStyle = options.fillColor || options.color; ctx.fill(options.fillRule || 'evenodd'); } if (options.stroke && options.weight !== 0) { ctx.globalAlpha = options.opacity; ctx.lineWidth = 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), layer, clickedLayer; for (var order = this._drawFirst; order; order = order.next) { layer = order.layer; if (layer.options.interactive && layer._containsPoint(point) && !this._map._draggableMoved(layer)) { clickedLayer = layer; } } if (clickedLayer) { L.DomEvent._fakeStop(e); this._fireEvent([clickedLayer], e); } }, _onMouseMove: function (e) { if (!this._map || this._map.dragging.moving() || this._map._animatingZoom) { return; } var point = this._map.mouseEventToLayerPoint(e); this._handleMouseHover(e, point); }, _handleMouseOut: function (e) { var layer = this._hoveredLayer; if (layer) { // 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 layer, candidateHoveredLayer; for (var order = this._drawFirst; order; order = order.next) { layer = order.layer; if (layer.options.interactive && layer._containsPoint(point)) { candidateHoveredLayer = layer; } } if (candidateHoveredLayer !== this._hoveredLayer) { this._handleMouseOut(e); if (candidateHoveredLayer) { L.DomUtil.addClass(this._container, 'leaflet-interactive'); // change cursor this._fireEvent([candidateHoveredLayer], e, 'mouseover'); this._hoveredLayer = candidateHoveredLayer; } } if (this._hoveredLayer) { this._fireEvent([this._hoveredLayer], e); } }, _fireEvent: function (layers, e, type) { this._map._fireDOMEvent(e, type || e.type, layers); }, _bringToFront: function (layer) { var order = layer._order; var next = order.next; var prev = order.prev; if (next) { next.prev = prev; } else { // Already last return; } if (prev) { prev.next = next; } else if (next) { // Update first entry unless this is the // signle entry this._drawFirst = next; } order.prev = this._drawLast; this._drawLast.next = order; order.next = null; this._drawLast = order; this._requestRedraw(layer); }, _bringToBack: function (layer) { var order = layer._order; var next = order.next; var prev = order.prev; if (prev) { prev.next = next; } else { // Already first return; } if (next) { next.prev = prev; } else if (prev) { // Update last entry unless this is the // signle entry this._drawLast = prev; } order.prev = null; order.next = this._drawFirst; this._drawFirst.prev = order; this._drawFirst = order; this._requestRedraw(layer); } }); // @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(); };