maplair
Version:
JavaScript library for mobile-friendly interactive maps
217 lines (177 loc) • 5.87 kB
JavaScript
import {Renderer} from './Renderer.js';
import * as DomUtil from '../../dom/DomUtil.js';
import {splitWords, stamp} from '../../core/Util.js';
/*
* @class SVG
* @inherits Renderer
*
* Allows vector layers to be displayed with [SVG](https://developer.mozilla.org/docs/Web/SVG).
* Inherits `Renderer`.
*
* @example
*
* Use SVG by default for all paths in the map:
*
* ```js
* const map = new MaplairMap('map', {
* renderer: new SVG()
* });
* ```
*
* Use a SVG renderer with extra padding for specific vector geometries:
*
* ```js
* const map = new MaplairMap('map');
* const myRenderer = new SVG({ padding: 0.5 });
* const line = new Polyline( coordinates, { renderer: myRenderer } );
* const circle = new Circle( center, { renderer: myRenderer, radius: 100 } );
* ```
*/
// @namespace SVG
// @constructor SVG(options?: Renderer options)
// Creates a SVG renderer with the given options.
export class SVG extends Renderer {
constructor(...args) {
super(...args);
this.callInitHooks(SVG);
}
_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');
this._rootGroup = SVG.create('g');
this._container.appendChild(this._rootGroup);
}
_destroyContainer() {
super._destroyContainer();
delete this._rootGroup;
delete this._svgSize;
}
_resizeContainer() {
const size = super._resizeContainer();
// set size of svg-container if changed
if (!this._svgSize || !this._svgSize.equals(size)) {
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(' '));
}
}
_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
_initPath(layer) {
const path = layer._path = SVG.create('path');
// @namespace Path
// @option className: String = null
// Custom class name set on an element. Only for SVG renderer.
if (layer.options.className) {
path.classList.add(...splitWords(layer.options.className));
}
if (layer.options.interactive) {
path.classList.add('maplair-interactive');
}
this._updateStyle(layer);
this._layers[stamp(layer)] = layer;
}
_addPath(layer) {
if (!this._rootGroup) { this._initContainer(); }
this._rootGroup.appendChild(layer._path);
layer.addInteractiveTarget(layer._path);
}
_removePath(layer) {
layer._path.remove();
layer.removeInteractiveTarget(layer._path);
delete this._layers[stamp(layer)];
}
_updatePath(layer) {
layer._project();
layer._update();
}
_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');
}
}
_updatePoly(layer, closed) {
this._setPath(layer, SVG.pointsToPath(layer._parts, closed));
}
_updateCircle(layer) {
const p = layer._point,
r = Math.max(Math.round(layer._pxRadius), 1),
r2 = Math.max(Math.round(layer._pxRadiusY), 1) || r,
arc = `a${r},${r2} 0 1,0 `;
// drawing a circle with two half-arcs
const d = layer._empty() ? 'M0 0' :
`M${p.x - r},${p.y
}${arc}${r * 2},0 ${
arc}${-r * 2},0 `;
this._setPath(layer, d);
}
_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
_bringToFront(layer) {
DomUtil.toFront(layer._path);
}
_bringToBack(layer) {
DomUtil.toBack(layer._path);
}
// @function create(name: String): SVGElement
// 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).
static create(name) {
return document.createElementNS('http://www.w3.org/2000/svg', name);
}
// @function pointsToPath(rings: Point[], closed: Boolean): String
// Generates a SVG path string for multiple rings, with each ring turning
// into "M..L..L.." instructions
static pointsToPath(rings, closed) {
const str = rings.flatMap(points => [
...points.map((p, j) => `${(j ? 'L' : 'M') + p.x} ${p.y}`),
// closes the ring for polygons
closed ? 'z' : ''
]).join('');
// SVG complains about empty path strings
return str || 'M0 0';
}
}