UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

243 lines (203 loc) • 6.89 kB
import {Renderer} from './Renderer.js'; import * as DomUtil from '../../dom/DomUtil.js'; import {splitWords, stamp} from '../../core/Util.js'; /** @import {RendererEventMap, RendererOptions} from '../../types.js' */ /** @import {Point} from '../../geometry/Point.js' */ /** @import {CircleMarker} from './CircleMarker.js' */ /** @import {Path} from './Path.js' */ /** @import {Polyline} from './Polyline.js' */ /** * Allows vector layers to be displayed with [SVG](https://developer.mozilla.org/docs/Web/SVG). Inherits `Renderer`. * @constructorDescription Creates an SVG renderer with the given options. * @example * Use SVG by default for all paths in the map: * * ```js * const map = new LeafletMap('map', { * renderer: new SVG() * }); * ``` * * Use a SVG renderer with extra padding for specific vector geometries: * * ```js * const map = new LeafletMap('map'); * const myRenderer = new SVG({ padding: 0.5 }); * const line = new Polyline( coordinates, { renderer: myRenderer } ); * const circle = new Circle( center, { renderer: myRenderer, radius: 100 } ); * ``` * @template {RendererOptions} [O=RendererOptions] * @template {RendererEventMap} [E=RendererEventMap] * @extends {Renderer<O, E>} */ export class SVG extends Renderer { _initContainer() { this._container = SVG.create('svg'); // makes it possible to click through svg root; we'll reset it back in individual paths this._container.setAttribute('pointer-events', 'none'); /** @type {SVGElement} */ this._rootGroup = SVG.create('g'); this._container.appendChild(this._rootGroup); } _destroyContainer() { super._destroyContainer(); delete /** @type {any} */ (this)._rootGroup; delete this._svgSize; } _resizeContainer() { const size = super._resizeContainer(); // set size of svg-container if changed if (!this._svgSize || !this._svgSize.equals(size)) { /** @type {Point | undefined} */ this._svgSize = size; this._container.setAttribute('width', `${size.x}`); this._container.setAttribute('height', `${size.y}`); } // Reset the <SVG>'s viewBox as per _update, but skip redrawing paths. // This keeps paths visually aligned when resizing. const b = this._bounds; if (b) { this._container.setAttribute('viewBox', [b.min.x, b.min.y, size.x, size.y].join(' ')); } return size; } _update() { if (this._map._animatingZoom && this._bounds) { return; } const b = this._bounds, size = b.getSize(); // movement: update container viewBox so that we don't have to change coordinates of individual layers this._container.setAttribute('viewBox', [b.min.x, b.min.y, size.x, size.y].join(' ')); this.fire('update'); } // methods below are called by vector layers implementations /** @param {Path} layer */ _initPath(layer) { const path = layer._path = SVG.create('path'); if (layer.options.className) { path.classList.add(...splitWords(layer.options.className)); } if (layer.options.interactive) { path.classList.add('leaflet-interactive'); } this._updateStyle(layer); this._layers[stamp(layer)] = layer; } /** @param {Path} layer */ _addPath(layer) { if (!this._rootGroup) { this._initContainer(); } this._rootGroup.appendChild(layer._path); layer.addInteractiveTarget(layer._path); } /** @param {Path} layer */ _removePath(layer) { layer._path.remove(); layer.removeInteractiveTarget(layer._path); delete this._layers[stamp(layer)]; } /** @param {Path} layer */ _updatePath(layer) { layer._project(); layer._update(); } /** @param {Path} layer */ _updateStyle(layer) { const path = layer._path, options = layer.options; if (!path) { return; } if (options.stroke) { path.setAttribute('stroke', options.color); path.setAttribute('stroke-opacity', `${options.opacity}`); path.setAttribute('stroke-width', `${options.weight}`); path.setAttribute('stroke-linecap', options.lineCap); path.setAttribute('stroke-linejoin', options.lineJoin); if (options.dashArray) { path.setAttribute('stroke-dasharray', `${options.dashArray}`); } else { path.removeAttribute('stroke-dasharray'); } if (options.dashOffset) { path.setAttribute('stroke-dashoffset', options.dashOffset); } else { path.removeAttribute('stroke-dashoffset'); } } else { path.setAttribute('stroke', 'none'); } if (options.fill) { path.setAttribute('fill', options.fillColor || options.color); path.setAttribute('fill-opacity', `${options.fillOpacity}`); path.setAttribute('fill-rule', options.fillRule || 'evenodd'); } else { path.setAttribute('fill', 'none'); } } /** * @param {Polyline} layer * @param {boolean} [closed] */ _updatePoly(layer, closed) { this._setPath(layer, SVG.pointsToPath(layer._parts, closed)); } /** @param {CircleMarker} layer */ _updateCircle(layer) { const p = layer._point, r = Math.max(Math.round(layer._pxRadius), 1), r2 = Math.max(Math.round(layer._pxRadiusY ?? r), 1), arc = `a${r},${r2} 0 1,0 `; // drawing a circle with two half-arcs const d = layer._empty() ? 'M0 0' : `M${num(p.x - r)},${num(p.y) }${arc}${r * 2},0 ${ arc}${-r * 2},0 `; this._setPath(layer, d); } /** * @param {Path} layer * @param {string} path */ _setPath(layer, path) { layer._path.setAttribute('d', path); } // SVG does not have the concept of zIndex so we resort to changing the DOM order of elements /** @param {Path} layer */ _bringToFront(layer) { DomUtil.toFront(layer._path); } /** @param {Path} layer */ _bringToBack(layer) { DomUtil.toBack(layer._path); } /** * Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement), * corresponding to the class name passed. For example, using 'line' will return an instance * of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement). * @param {string} name */ static create(name) { return document.createElementNS('http://www.w3.org/2000/svg', name); } /** * Generates a SVG path string for multiple rings, with each ring turning into "M..L..L.." instructions * @param {Point[][]} rings * @param {boolean} [closed] */ static pointsToPath(rings, closed) { const str = rings.flatMap(points => [ ...points.map((p, j) => `${(j ? 'L' : 'M') + num(p.x)} ${num(p.y)}`), // closes the ring for polygons closed ? 'z' : '' ]).join(''); // SVG complains about empty path strings return str || 'M0 0'; } } // Formats a pixel coordinate with 0.1px precision. Built from integer parts because V8 stringifies small integers // much faster than fractional numbers, and path strings with many vertices are rebuilt on every redraw. /** @param {number} v */ function num(v) { const n = Math.round(v * 10), a = n < 0 ? -n : n, i = Math.floor(a / 10), d = a - i * 10; return (n < 0 ? '-' : '') + (d ? `${i}.${d}` : i); }