import {Renderer} from './Renderer'; import * as DomUtil from '../../dom/DomUtil'; import * as DomEvent from '../../dom/DomEvent'; import * as Browser from '../../core/Browser'; import * as Util from '../../core/Util'; import {Bounds} from '../../geometry/Bounds'; /* * @class Canvas * @inherits Renderer * @aka L.Canvas * * Allows vector layers to be displayed with [``](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 } ); * ``` */ export var Canvas = Renderer.extend({ getEvents: function () { var events = 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 () { 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'); DomEvent.on(container, 'mousemove', Util.throttle(this._onMouseMove, 32, this), this); DomEvent.on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this); DomEvent.on(container, 'mouseout', this._handleMouseOut, this); this._ctx = container.getContext('2d'); }, _destroyContainer: function () { delete this._ctx; DomUtil.remove(this._container); DomEvent.off(this._container); delete this._container; }, _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 = {}; Renderer.prototype._update.call(this); var b = this._bounds, container = this._container, size = b.getSize(), m = Browser.retina ? 2 : 1; 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 (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 () { Renderer.prototype._reset.call(this); if (this._postponeUpdatePaths) { this._postponeUpdatePaths = false; this._updatePaths(); } }, _initPath: function (layer) { this._updateDashArray(layer); this._layers[Util.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 || Util.requestAnimFrame(this._redraw, this); }, _extendRedrawBounds: function (layer) { if (layer._pxBounds) { var padding = (layer.options.weight || 0) + 1; this._redrawBounds = this._redrawBounds || new 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(); 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 = Math.max(Math.round(layer._radius), 1), s = (Math.max(Math.round(layer._radiusY), 1) || 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) { if (ctx.setLineDash) { ctx.setLineDash(layer.options && layer.options._dashArray || []); } 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) { 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 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) { 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 // single 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 // single entry this._drawLast = prev; } order.prev = null; order.next = this._drawFirst; this._drawFirst.prev = order; this._drawFirst = order; this._requestRedraw(layer); } }); // @factory L.canvas(options?: Renderer options) // Creates a Canvas renderer with the given options. export function canvas(options) { return Browser.canvas ? new Canvas(options) : null; }