maplair
Version:
JavaScript library for mobile-friendly interactive maps
292 lines (291 loc) • 8.71 kB
JavaScript
import { stamp } from "../../core/Util.js";
import { on } from "../../dom/DomEvent.js";
import { Bounds } from "../../geometry/Bounds.js";
import { Renderer } from "./Renderer.js";
//#region src/layer/vector/Canvas.js
const DASH_SEPARATOR_RE = /[, ]+/;
var Canvas = class Canvas extends Renderer {
static {
this.setDefaultOptions({ tolerance: 0 });
}
constructor(...args) {
super(...args);
this.callInitHooks(Canvas);
}
getEvents() {
const events = super.getEvents();
events.viewprereset = this._onViewPreReset;
return events;
}
_onViewPreReset() {
this._postponeUpdatePaths = true;
}
onAdd(map) {
super.onAdd(map);
this._draw();
}
onRemove() {
super.onRemove();
clearTimeout(this._pointerHoverThrottleTimeout);
}
_initContainer() {
const container = this._container = document.createElement("canvas");
on(container, "pointermove", this._onPointerMove, this);
on(container, "click dblclick pointerdown pointerup contextmenu", this._onClick, this);
on(container, "pointerout", this._handlePointerOut, this);
container["_maplair_disable_events"] = true;
this._ctx = container.getContext("2d");
}
_destroyContainer() {
cancelAnimationFrame(this._redrawRequest);
this._redrawRequest = null;
delete this._ctx;
super._destroyContainer();
}
_resizeContainer() {
const size = super._resizeContainer();
const m = this._ctxScale = window.devicePixelRatio;
this._container.width = m * size.x;
this._container.height = m * size.y;
}
_updatePaths() {
if (this._postponeUpdatePaths) return;
this._redrawBounds = null;
for (const layer of Object.values(this._layers)) layer._update();
this._redraw();
}
_update() {
if (this._map._animatingZoom && this._bounds) return;
const b = this._bounds, s = this._ctxScale;
this._ctx.setTransform(s, 0, 0, s, -b.min.x * s, -b.min.y * s);
this.fire("update");
}
_reset() {
super._reset();
if (this._postponeUpdatePaths) {
this._postponeUpdatePaths = false;
this._updatePaths();
}
}
_initPath(layer) {
this._updateDashArray(layer);
this._layers[stamp(layer)] = layer;
const order = layer._order = {
layer,
prev: this._drawLast,
next: null
};
if (this._drawLast) this._drawLast.next = order;
this._drawLast = order;
this._drawFirst ??= this._drawLast;
}
_addPath(layer) {
this._requestRedraw(layer);
}
_removePath(layer) {
const order = layer._order;
const next = order.next;
const 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[stamp(layer)];
this._requestRedraw(layer);
}
_updatePath(layer) {
this._extendRedrawBounds(layer);
layer._project();
layer._update();
this._requestRedraw(layer);
}
_updateStyle(layer) {
this._updateDashArray(layer);
this._requestRedraw(layer);
}
_updateDashArray(layer) {
if (typeof layer.options.dashArray === "string") {
const parts = layer.options.dashArray.split(DASH_SEPARATOR_RE);
layer.options._dashArray = parts.map((n) => Number(n)).filter((n) => !isNaN(n));
} else layer.options._dashArray = layer.options.dashArray;
}
_requestRedraw(layer) {
if (!this._map) return;
this._extendRedrawBounds(layer);
this._redrawRequest ??= requestAnimationFrame(this._redraw.bind(this));
}
_extendRedrawBounds(layer) {
if (layer._pxBounds) {
const padding = (layer.options.weight ?? 0) + 1;
this._redrawBounds ??= new Bounds();
this._redrawBounds.extend(layer._pxBounds.min.subtract([padding, padding]));
this._redrawBounds.extend(layer._pxBounds.max.add([padding, padding]));
}
}
_redraw() {
this._redrawRequest = null;
if (this._redrawBounds) {
this._redrawBounds.min._floor();
this._redrawBounds.max._ceil();
}
this._clear();
this._draw();
this._redrawBounds = null;
}
_clear() {
const bounds = this._redrawBounds;
if (bounds) {
const size = bounds.getSize();
this._ctx.clearRect(bounds.min.x, bounds.min.y, size.x, size.y);
} else {
this._ctx.save();
this._ctx.setTransform(1, 0, 0, 1, 0, 0);
this._ctx.clearRect(0, 0, this._container.width, this._container.height);
this._ctx.restore();
}
}
_draw() {
let layer;
const bounds = this._redrawBounds;
this._ctx.save();
if (bounds) {
const 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 (let 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();
}
_updatePoly(layer, closed) {
if (!this._drawing) return;
const parts = layer._parts, ctx = this._ctx;
if (!parts.length) return;
ctx.beginPath();
parts.forEach((p0) => {
p0.forEach((p, j) => {
ctx[j ? "lineTo" : "moveTo"](p.x, p.y);
});
if (closed) ctx.closePath();
});
this._fillStroke(ctx, layer);
}
_updateCircle(layer) {
if (!this._drawing || layer._empty()) return;
const p = layer._point, ctx = this._ctx, r = Math.max(Math.round(layer._pxRadius), 1), s = (Math.max(Math.round(layer._pxRadiusY), 1) || r) / r;
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(ctx, layer) {
const 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.lineDashOffset = Number(options.dashOffset ?? 0);
ctx.setLineDash(options._dashArray ?? []);
ctx.globalAlpha = options.opacity;
ctx.lineWidth = options.weight;
ctx.strokeStyle = options.color;
ctx.lineCap = options.lineCap;
ctx.lineJoin = options.lineJoin;
ctx.stroke();
}
}
_onClick(e) {
const point = this._map.pointerEventToLayerPoint(e);
let layer, clickedLayer;
for (let order = this._drawFirst; order; order = order.next) {
layer = order.layer;
if (layer.options.interactive && layer._containsPoint(point)) {
if (!(e.type === "click" || e.type === "preclick") || !this._map._draggableMoved(layer)) clickedLayer = layer;
}
}
this._fireEvent(clickedLayer ? [clickedLayer] : false, e);
}
_onPointerMove(e) {
if (!this._map || this._map.dragging.moving() || this._map._animatingZoom) return;
const point = this._map.pointerEventToLayerPoint(e);
this._handlePointerHover(e, point);
}
_handlePointerOut(e) {
const layer = this._hoveredLayer;
if (layer) {
this._container.classList.remove("maplair-interactive");
this._fireEvent([layer], e, "pointerout");
this._hoveredLayer = null;
this._pointerHoverThrottled = false;
}
}
_handlePointerHover(e, point) {
if (this._pointerHoverThrottled) return;
let layer, candidateHoveredLayer;
for (let order = this._drawFirst; order; order = order.next) {
layer = order.layer;
if (layer.options.interactive && layer._containsPoint(point)) candidateHoveredLayer = layer;
}
if (candidateHoveredLayer !== this._hoveredLayer) {
this._handlePointerOut(e);
if (candidateHoveredLayer) {
this._container.classList.add("maplair-interactive");
this._fireEvent([candidateHoveredLayer], e, "pointerover");
this._hoveredLayer = candidateHoveredLayer;
}
}
this._fireEvent(this._hoveredLayer ? [this._hoveredLayer] : false, e);
this._pointerHoverThrottled = true;
this._pointerHoverThrottleTimeout = setTimeout((() => {
this._pointerHoverThrottled = false;
}), 32);
}
_fireEvent(layers, e, type) {
this._map._fireDOMEvent(e, type || e.type, layers);
}
_bringToFront(layer) {
const order = layer._order;
if (!order) return;
const next = order.next;
const prev = order.prev;
if (next) next.prev = prev;
else return;
if (prev) prev.next = next;
else if (next) this._drawFirst = next;
order.prev = this._drawLast;
this._drawLast.next = order;
order.next = null;
this._drawLast = order;
this._requestRedraw(layer);
}
_bringToBack(layer) {
const order = layer._order;
if (!order) return;
const next = order.next;
const prev = order.prev;
if (prev) prev.next = next;
else return;
if (next) next.prev = prev;
else if (prev) this._drawLast = prev;
order.prev = null;
order.next = this._drawFirst;
this._drawFirst.prev = order;
this._drawFirst = order;
this._requestRedraw(layer);
}
};
//#endregion
export { Canvas };
//# sourceMappingURL=Canvas.js.map