OpenSourcePolitics / decidim-ditp

Decidim instance for DITP OGP
1 stars 2 forks source link

optimization: 1 fill/stroke for all features with equal style instead of 1 for e... #43

Open github-actions[bot] opened 1 year ago

github-actions[bot] commented 1 year ago

so we emulate that by calculating what's under the mouse on mousemove/click manually

single entry

single entry

Creates a Canvas renderer with the given options.

https://api.github.com/OpenSourcePolitics/decidim-ditp/blob/3f60854d1041295c99daa13fb6aef7e1a7fb37d1/packages/core/node_modules/leaflet/src/layer/vector/Canvas.js#L288


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 [`<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 } );
 * ```
 */

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;
}