Skip to content

Commit

Permalink
Implement BlanketOverlay as Renderer superclass (Leaflet#8611)
Browse files Browse the repository at this point in the history
Signed-off-by: Iván Sánchez Ortega <[email protected]>
  • Loading branch information
IvanSanchez authored Feb 28, 2023
1 parent 0dd1569 commit bedad7e
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 118 deletions.
1 change: 1 addition & 0 deletions build/docs-index.leafdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions build/leafdoc-templates/html.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
70 changes: 70 additions & 0 deletions debug/vector/blanket.html
Original file line number Diff line number Diff line change
@@ -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 changes: 165 additions & 0 deletions src/layer/BlanketOverlay.js
Original file line number Diff line number Diff line change
@@ -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,
});
2 changes: 2 additions & 0 deletions src/layer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
42 changes: 17 additions & 25 deletions src/layer/vector/Canvas.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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.
Expand All @@ -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() {
Expand All @@ -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');
Expand Down
Loading

0 comments on commit bedad7e

Please sign in to comment.