Implement BlanketOverlay as Renderer superclass (#8611)

Signed-off-by: Iván Sánchez Ortega <ivan@sanchezortega.es>
This commit is contained in:
Iván Sánchez Ortega
2023-02-28 20:54:48 +01:00
committed by GitHub
parent 0dd15699fd
commit bedad7e850
8 changed files with 293 additions and 118 deletions

View File

@ -61,5 +61,6 @@ This file just defines the order of the classes in the docs.
@class Handler
@class Projection
@class CRS
@class BlanketOverlay
@class Renderer
@class Event objects

View File

@ -113,6 +113,7 @@ bodyclass: api-page
<!--<li><a class="nodocs" href="#">IFeature</a></li>-->
<li><a href="#projection">Projection</a></li>
<li><a href="#crs">CRS</a></li>
<li><a href="#blanketoverlay">BlanketOverlay</a></li>
<li><a href="#renderer">Renderer</a></li>
</ul>

70
debug/vector/blanket.html Normal file
View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<title>Leaflet debug page</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="../../dist/leaflet.css" />
<link rel="stylesheet" href="../css/mobile.css" />
</head>
<body>
<div id="map"></div>
<script type="module">
import {tileLayer, Map, LatLng, BlanketOverlay, Browser, Canvas, SVG, CircleMarker} from '../../dist/leaflet-src.esm.js';
const osmUrl = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
osmAttrib = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
osm = tileLayer(osmUrl, {maxZoom: 18, attribution: osmAttrib});
const map = new Map('map', {
center: new LatLng(0,0),
zoom: 1,
layers: [osm]
});
const DebugBlanket = BlanketOverlay.extend({
_initContainer(){
const container = this._container = document.createElement('div');
container.style.border = '2px solid black';
container.style.display= "flex";
container.style.justifyContent = "center";
container.style.alignItems = "center";
},
_onSettled(ev){
this._container.innerHTML = `
lat: ${this._center.lat.toFixed(6)}<br>
lng: ${this._center.lng.toFixed(6)}<br>
zoom: ${this._zoom}<br>
map bounds: <br>${this._map.getBounds().toBBoxString().split(',').map(n=>Number(n).toFixed(6)).join('<br>')}<br>
px bounds: ${this._bounds.min}, ${this._bounds.max}`;
}
})
new DebugBlanket({
padding: -0.1,
continuous: true
}).addTo(map);
const canvas = new Canvas({
padding:-0.1,
//continuous: true
}).addTo(map);
canvas._container.style.border='2px solid red';
const redCircle = new CircleMarker([40.5, -3.6], {radius: 20, color: 'red', renderer: canvas}).addTo(map);
const svg = new SVG({
padding:-0.1,
//continuous: true,
}).addTo(map);
svg._container.style.border='2px solid blue';
const blueCircle = new CircleMarker([63.4, 10.4], {radius: 20, color: 'blue', renderer: svg}).addTo(map);
</script>
</body>
</html>

165
src/layer/BlanketOverlay.js Normal file
View File

@ -0,0 +1,165 @@
import {Layer} from './Layer.js';
import * as DomUtil from '../dom/DomUtil.js';
import * as Util from '../core/Util.js';
import * as DomEvent from '../dom/DomEvent.js';
import {Bounds} from '../geometry/Bounds.js';
/*
* @class BlanketOverlay
* @inherits Layer
* @aka L.BlanketOverlay
*
* Represents an HTML element that covers ("blankets") the entire surface
* of the map.
*
* Do not use this class directly. It's meant for `Renderer`, and for plugins
* that rely on one single HTML element
*/
export const BlanketOverlay = Layer.extend({
// @section
// @aka BlanketOverlay options
options: {
// @option padding: Number = 0.1
// How much to extend the clip area around the map view (relative to its size)
// e.g. 0.1 would be 10% of map view in each direction
padding: 0.1,
// @option continuous: Boolean = false
// When `false`, the blanket will update its position only when the
// map state settles (*after* a pan/zoom animation). When `true`,
// it will update when the map state changes (*during* pan/zoom
// animations)
continuous: false,
},
initialize(options) {
Util.setOptions(this, options);
},
onAdd() {
if (!this._container) {
this._initContainer(); // defined by renderer implementations
// always keep transform-origin as 0 0, #8794
this._container.classList.add('leaflet-zoom-animated');
}
this.getPane().appendChild(this._container);
this._resizeContainer();
this._onMoveEnd();
},
onRemove() {
this._destroyContainer();
},
getEvents() {
const events = {
viewreset: this._reset,
zoom: this._onZoom,
moveend: this._onMoveEnd,
zoomend: this._onZoomEnd,
resize: this._resizeContainer,
};
if (this._zoomAnimated) {
events.zoomanim = this._onAnimZoom;
}
if (this.options.continuous) {
events.move = this._onMoveEnd;
}
return events;
},
_onAnimZoom(ev) {
this._updateTransform(ev.center, ev.zoom);
},
_onZoom() {
this._updateTransform(this._map.getCenter(), this._map.getZoom());
},
_updateTransform(center, zoom) {
const scale = this._map.getZoomScale(zoom, this._zoom),
viewHalf = this._map.getSize().multiplyBy(0.5 + this.options.padding),
currentCenterPoint = this._map.project(this._center, zoom),
topLeftOffset = viewHalf.multiplyBy(-scale).add(currentCenterPoint)
.subtract(this._map._getNewPixelOrigin(center, zoom));
DomUtil.setTransform(this._container, topLeftOffset, scale);
},
_onMoveEnd(ev) {
// Update pixel bounds of renderer container (for positioning/sizing/clipping later)
const p = this.options.padding,
size = this._map.getSize(),
min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round();
this._bounds = new Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round());
this._center = this._map.getCenter();
this._zoom = this._map.getZoom();
this._updateTransform(this._center, this._zoom);
this._onSettled(ev);
},
_reset() {
this._onSettled();
this._updateTransform(this._center, this._zoom);
this._onViewReset();
},
/*
* @section Subclass interface
* @uninheritable
* Subclasses must define the following methods:
*
* @method _initContainer(): undefined
* Must initialize the HTML element to use as blanket, and store it as
* `this._container`. The base implementation creates a blank `<div>`
*
* @method _destroyContainer(): undefined
* Must destroy the HTML element in `this._container` and free any other
* resources. The base implementation destroys the element and removes
* any event handlers attached to it.
*
* @method _resizeContainer(): Point
* The base implementation resizes the container (based on the map's size
* and taking into account the padding), returning the new size in CSS pixels.
*
* Subclass implementations shall reset container parameters and data
* structures as needed.
*
* @method _onZoomEnd(ev?: MouseEvent): undefined
* (Optional) Runs on the map's `zoomend` event.
*
* @method _onViewReset(ev?: MouseEvent): undefined
* (Optional) Runs on the map's `viewreset` event.
*
* @method _onSettled(): undefined
* Runs whenever the map state settles after changing (at the end of pan/zoom
* animations, etc). This should trigger the bulk of any rendering logic.
*
* If the `continuous` option is set to `true`, then this also runs on
* any map state change (including *during* pan/zoom animations).
*/
_initContainer() {
this._container = DomUtil.create('div');
},
_destroyContainer() {
DomEvent.off(this._container);
this._container.remove();
delete this._container;
},
_resizeContainer() {
const p = this.options.padding,
size = this._map.getSize().multiplyBy(1 + p * 2).round();
this._container.style.width = `${size.x}px`;
this._container.style.height = `${size.y}px`;
return size;
},
_onZoomEnd: Util.falseFn,
_onViewReset: Util.falseFn,
_onSettled: Util.falseFn,
});

View File

@ -11,6 +11,8 @@ GeoJSON.getFeature = getFeature;
GeoJSON.asFeature = asFeature;
export {GeoJSON, geoJSON, geoJson};
export {BlanketOverlay} from './BlanketOverlay.js';
export {ImageOverlay, imageOverlay} from './ImageOverlay.js';
export {VideoOverlay, videoOverlay} from './VideoOverlay.js';
export {SVGOverlay, svgOverlay} from './SVGOverlay.js';

View File

@ -1,7 +1,5 @@
import {Renderer} from './Renderer.js';
import * as DomUtil from '../../dom/DomUtil.js';
import * as DomEvent from '../../dom/DomEvent.js';
import Browser from '../../core/Browser.js';
import * as Util from '../../core/Util.js';
import {Bounds} from '../../geometry/Bounds.js';
@ -58,8 +56,8 @@ export const Canvas = Renderer.extend({
this._postponeUpdatePaths = true;
},
onAdd() {
Renderer.prototype.onAdd.call(this);
onAdd(map) {
Renderer.prototype.onAdd.call(this, map);
// Redraw vectors since canvas is cleared upon removal,
// in case of removing the renderer itself from the map.
@ -80,9 +78,16 @@ export const Canvas = Renderer.extend({
_destroyContainer() {
Util.cancelAnimFrame(this._redrawRequest);
delete this._ctx;
this._container.remove();
DomEvent.off(this._container);
delete this._container;
Renderer.prototype._destroyContainer.call(this);
},
_resizeContainer() {
const size = Renderer.prototype._resizeContainer.call(this);
const m = this._ctxScale = window.devicePixelRatio;
// set canvas size (also clearing it); use double size on retina
this._container.width = m * size.x;
this._container.height = m * size.y;
},
_updatePaths() {
@ -100,27 +105,14 @@ export const Canvas = Renderer.extend({
_update() {
if (this._map._animatingZoom && this._bounds) { return; }
Renderer.prototype._update.call(this);
const 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);
}
s = this._ctxScale;
// translate so we use the same path coordinates after canvas element moves
this._ctx.translate(-b.min.x, -b.min.y);
this._ctx.setTransform(
s, 0, 0, s,
-b.min.x * s,
-b.min.y * s);
// Tell paths to redraw themselves
this.fire('update');

View File

@ -1,13 +1,9 @@
import {Layer} from '../Layer.js';
import * as DomUtil from '../../dom/DomUtil.js';
import {BlanketOverlay} from '../BlanketOverlay.js';
import * as Util from '../../core/Util.js';
import {Bounds} from '../../geometry/Bounds.js';
/*
* @class Renderer
* @inherits Layer
* @inherits BlanketOverlay
* @aka L.Renderer
*
* Base class for vector renderer implementations (`SVG`, `Canvas`). Handles the
@ -20,88 +16,36 @@ import {Bounds} from '../../geometry/Bounds.js';
*
* Do not use this class directly, use `SVG` and `Canvas` instead.
*
* The `continuous` option inherited from `BlanketOverlay` cannot be set to `true`
* (otherwise, renderers get out of place during a pinch-zoom operation).
*
* @event update: Event
* Fired when the renderer updates its bounds, center and zoom, for example when
* its map has moved
*/
export const Renderer = Layer.extend({
// @section
// @aka Renderer options
options: {
// @option padding: Number = 0.1
// How much to extend the clip area around the map view (relative to its size)
// e.g. 0.1 would be 10% of map view in each direction
padding: 0.1
},
export const Renderer = BlanketOverlay.extend({
initialize(options) {
Util.setOptions(this, options);
Util.setOptions(this, {...options, continuous: false});
Util.stamp(this);
this._layers = this._layers || {};
},
onAdd() {
if (!this._container) {
this._initContainer(); // defined by renderer implementations
// always keep transform-origin as 0 0
this._container.classList.add('leaflet-zoom-animated');
}
this.getPane().appendChild(this._container);
this._update();
onAdd(map) {
BlanketOverlay.prototype.onAdd.call(this, map);
this.on('update', this._updatePaths, this);
},
onRemove() {
BlanketOverlay.prototype.onRemove.call(this);
this.off('update', this._updatePaths, this);
this._destroyContainer();
},
getEvents() {
const events = {
viewreset: this._reset,
zoom: this._onZoom,
moveend: this._update,
zoomend: this._onZoomEnd
};
if (this._zoomAnimated) {
events.zoomanim = this._onAnimZoom;
}
return events;
},
_onAnimZoom(ev) {
this._updateTransform(ev.center, ev.zoom);
},
_onZoom() {
this._updateTransform(this._map.getCenter(), this._map.getZoom());
},
_updateTransform(center, zoom) {
const scale = this._map.getZoomScale(zoom, this._zoom),
viewHalf = this._map.getSize().multiplyBy(0.5 + this.options.padding),
currentCenterPoint = this._map.project(this._center, zoom),
topLeftOffset = viewHalf.multiplyBy(-scale).add(currentCenterPoint)
.subtract(this._map._getNewPixelOrigin(center, zoom));
DomUtil.setTransform(this._container, topLeftOffset, scale);
},
_reset() {
this._update();
this._updateTransform(this._center, this._zoom);
for (const id in this._layers) {
this._layers[id]._reset();
}
},
_onZoomEnd() {
// When a zoom ends, the "origin pixel" changes. Internal coordinates
// of paths are relative to the origin pixel and therefore need to
// be recalculated.
for (const id in this._layers) {
this._layers[id]._project();
}
@ -113,16 +57,18 @@ export const Renderer = Layer.extend({
}
},
_update() {
// Update pixel bounds of renderer container (for positioning/sizing/clipping later)
// Subclasses are responsible of firing the 'update' event.
const p = this.options.padding,
size = this._map.getSize(),
min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round();
_onViewReset() {
for (const id in this._layers) {
this._layers[id]._reset();
}
},
this._bounds = new Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round());
_onSettled() {
this._update();
},
// Subclasses are responsible of implementing `_update()`. It should fire
// the 'update' event whenever appropriate (before/after rendering).
_update: Util.falseFn,
this._center = this._map.getCenter();
this._zoom = this._map.getZoom();
}
});

View File

@ -1,6 +1,5 @@
import {Renderer} from './Renderer.js';
import * as DomUtil from '../../dom/DomUtil.js';
import * as DomEvent from '../../dom/DomEvent.js';
import {splitWords, stamp} from '../../core/Util.js';
import {svgCreate, pointsToPath} from './SVG.Util.js';
export {pointsToPath};
@ -48,31 +47,30 @@ export const SVG = Renderer.extend({
},
_destroyContainer() {
this._container.remove();
DomEvent.off(this._container);
delete this._container;
Renderer.prototype._destroyContainer.call(this);
delete this._rootGroup;
delete this._svgSize;
},
_resizeContainer() {
const size = Renderer.prototype._resizeContainer.call(this);
// 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);
}
},
_update() {
if (this._map._animatingZoom && this._bounds) { return; }
Renderer.prototype._update.call(this);
const b = this._bounds,
size = b.getSize(),
container = this._container;
// set size of svg-container if changed
if (!this._svgSize || !this._svgSize.equals(size)) {
this._svgSize = size;
container.setAttribute('width', size.x);
container.setAttribute('height', size.y);
}
// movement: update container viewBox so that we don't have to change coordinates of individual layers
DomUtil.setPosition(container, b.min);
container.setAttribute('viewBox', [b.min.x, b.min.y, size.x, size.y].join(' '));
this.fire('update');