mirror of
https://github.com/Leaflet/Leaflet.git
synced 2025-07-20 16:37:08 +00:00
Implement BlanketOverlay as Renderer superclass (#8611)
Signed-off-by: Iván Sánchez Ortega <ivan@sanchezortega.es>
This commit is contained in:

committed by
GitHub

parent
0dd15699fd
commit
bedad7e850
@ -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
|
||||
|
@ -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
70
debug/vector/blanket.html
Normal 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 = '© <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
165
src/layer/BlanketOverlay.js
Normal 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,
|
||||
});
|
@ -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';
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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');
|
||||
|
Reference in New Issue
Block a user