Merge pull request #13491 from ahocevar/map-loadstart-loadend

Add loadstart and loadend map events
This commit is contained in:
Andreas Hocevar
2022-03-19 16:39:32 +01:00
committed by GitHub
10 changed files with 251 additions and 69 deletions
+4 -2
View File
@@ -3,11 +3,13 @@ layout: example.html
title: Image Load Events title: Image Load Events
shortdesc: Example using image load events. shortdesc: Example using image load events.
docs: > docs: >
<p>Image sources fire events related to image loading. You can Image sources fire events related to image loading. You can
listen for <code>imageloadstart</code>, <code>imageloadend</code>, listen for <code>imageloadstart</code>, <code>imageloadend</code>,
and <code>imageloaderror</code> type events to monitor image loading and <code>imageloaderror</code> type events to monitor image loading
progress. This example registers listeners for these events and progress. This example registers listeners for these events and
renders an image loading progress bar at the bottom of the map.</p> renders an image loading progress bar at the bottom of the map. The
progress bar is shown and hidden according to the map's <code>loadstart</code>
and <code>loadend</code> events.
tags: "image, events, loading" tags: "image, events, loading"
--- ---
<div class="wrapper"> <div class="wrapper">
+15 -25
View File
@@ -18,9 +18,6 @@ function Progress(el) {
* Increment the count of loading tiles. * Increment the count of loading tiles.
*/ */
Progress.prototype.addLoading = function () { Progress.prototype.addLoading = function () {
if (this.loading === 0) {
this.show();
}
++this.loading; ++this.loading;
this.update(); this.update();
}; };
@@ -29,11 +26,8 @@ Progress.prototype.addLoading = function () {
* Increment the count of loaded tiles. * Increment the count of loaded tiles.
*/ */
Progress.prototype.addLoaded = function () { Progress.prototype.addLoaded = function () {
const this_ = this; ++this.loaded;
setTimeout(function () { this.update();
++this_.loaded;
this_.update();
}, 100);
}; };
/** /**
@@ -42,14 +36,6 @@ Progress.prototype.addLoaded = function () {
Progress.prototype.update = function () { Progress.prototype.update = function () {
const width = ((this.loaded / this.loading) * 100).toFixed(1) + '%'; const width = ((this.loaded / this.loading) * 100).toFixed(1) + '%';
this.el.style.width = width; this.el.style.width = width;
if (this.loading === this.loaded) {
this.loading = 0;
this.loaded = 0;
const this_ = this;
setTimeout(function () {
this_.hide();
}, 500);
}
}; };
/** /**
@@ -63,10 +49,11 @@ Progress.prototype.show = function () {
* Hide the progress bar. * Hide the progress bar.
*/ */
Progress.prototype.hide = function () { Progress.prototype.hide = function () {
if (this.loading === this.loaded) { const style = this.el.style;
this.el.style.visibility = 'hidden'; setTimeout(function () {
this.el.style.width = 0; style.visibility = 'hidden';
} style.width = 0;
}, 250);
}; };
const progress = new Progress(document.getElementById('progress')); const progress = new Progress(document.getElementById('progress'));
@@ -80,11 +67,7 @@ const source = new ImageWMS({
source.on('imageloadstart', function () { source.on('imageloadstart', function () {
progress.addLoading(); progress.addLoading();
}); });
source.on(['imageloadend', 'imageloaderror'], function () {
source.on('imageloadend', function () {
progress.addLoaded();
});
source.on('imageloaderror', function () {
progress.addLoaded(); progress.addLoaded();
}); });
@@ -96,3 +79,10 @@ const map = new Map({
zoom: 4, zoom: 4,
}), }),
}); });
map.on('loadstart', function () {
progress.show();
});
map.on('loadend', function () {
progress.hide();
});
+30
View File
@@ -0,0 +1,30 @@
.map {
background: #85ccf9;
position: relative;
}
#map {
height: 400px;
position: relative;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.spinner:after {
content: "";
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
width: 40px;
height: 40px;
margin-top: -20px;
margin-left: -20px;
border-radius: 50%;
border: 5px solid rgba(180, 180, 180, 0.6);
border-top-color: rgba(0, 0, 0, 0.6);
animation: spinner 0.6s linear infinite;
}
+13
View File
@@ -0,0 +1,13 @@
---
layout: example.html
title: Loading Spinner
shortdesc: Example using load events to show a loading spinner.
docs: >
You can listen for the map's <code>loadstart</code> and <code>loadend</code> events
to show a loading spinner on top of the map.
tags: "events, loading, spinner"
cloak:
- key: pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2t0cGdwMHVnMGdlbzMxbDhwazBic2xrNSJ9.WbcTL9uj8JPAsnT9mgb7oQ
value: Your Mapbox access token from https://mapbox.com/ here
---
<div id="map" class="map"></div>
+31
View File
@@ -0,0 +1,31 @@
import Map from '../src/ol/Map.js';
import TileLayer from '../src/ol/layer/Tile.js';
import View from '../src/ol/View.js';
import XYZ from '../src/ol/source/XYZ.js';
const key = 'get_your_own_D6rA4zTHduk6KOKTXzGB';
const attributions =
'<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>';
const source = new XYZ({
attributions: attributions,
url: 'https://api.maptiler.com/maps/streets/{z}/{x}/{y}.png?key=' + key,
tileSize: 512,
});
const map = new Map({
layers: [new TileLayer({source: source})],
target: 'map',
view: new View({
center: [0, 0],
zoom: 2,
}),
});
map.on('loadstart', function () {
map.getTargetElement().classList.add('spinner');
});
map.on('loadend', function () {
map.getTargetElement().classList.remove('spinner');
});
+3 -1
View File
@@ -7,7 +7,9 @@ docs: >
listen for <code>tileloadstart</code>, <code>tileloadend</code>, listen for <code>tileloadstart</code>, <code>tileloadend</code>,
and <code>tileloaderror</code> type events to monitor tile loading and <code>tileloaderror</code> type events to monitor tile loading
progress. This example registers listeners for these events and progress. This example registers listeners for these events and
renders a tile loading progress bar at the bottom of the map.</p> renders a tile loading progress bar at the bottom of the map. The
progress bar is shown and hidden according to the map's <code>loadstart</code>
and <code>loadend</code> events.
tags: "tile, events, loading" tags: "tile, events, loading"
cloak: cloak:
- key: pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2t0cGdwMHVnMGdlbzMxbDhwazBic2xrNSJ9.WbcTL9uj8JPAsnT9mgb7oQ - key: pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2t0cGdwMHVnMGdlbzMxbDhwazBic2xrNSJ9.WbcTL9uj8JPAsnT9mgb7oQ
+15 -25
View File
@@ -18,9 +18,6 @@ function Progress(el) {
* Increment the count of loading tiles. * Increment the count of loading tiles.
*/ */
Progress.prototype.addLoading = function () { Progress.prototype.addLoading = function () {
if (this.loading === 0) {
this.show();
}
++this.loading; ++this.loading;
this.update(); this.update();
}; };
@@ -29,11 +26,8 @@ Progress.prototype.addLoading = function () {
* Increment the count of loaded tiles. * Increment the count of loaded tiles.
*/ */
Progress.prototype.addLoaded = function () { Progress.prototype.addLoaded = function () {
const this_ = this; ++this.loaded;
setTimeout(function () { this.update();
++this_.loaded;
this_.update();
}, 100);
}; };
/** /**
@@ -42,14 +36,6 @@ Progress.prototype.addLoaded = function () {
Progress.prototype.update = function () { Progress.prototype.update = function () {
const width = ((this.loaded / this.loading) * 100).toFixed(1) + '%'; const width = ((this.loaded / this.loading) * 100).toFixed(1) + '%';
this.el.style.width = width; this.el.style.width = width;
if (this.loading === this.loaded) {
this.loading = 0;
this.loaded = 0;
const this_ = this;
setTimeout(function () {
this_.hide();
}, 500);
}
}; };
/** /**
@@ -63,10 +49,11 @@ Progress.prototype.show = function () {
* Hide the progress bar. * Hide the progress bar.
*/ */
Progress.prototype.hide = function () { Progress.prototype.hide = function () {
if (this.loading === this.loaded) { const style = this.el.style;
this.el.style.visibility = 'hidden'; setTimeout(function () {
this.el.style.width = 0; style.visibility = 'hidden';
} style.width = 0;
}, 250);
}; };
const progress = new Progress(document.getElementById('progress')); const progress = new Progress(document.getElementById('progress'));
@@ -85,11 +72,7 @@ const source = new XYZ({
source.on('tileloadstart', function () { source.on('tileloadstart', function () {
progress.addLoading(); progress.addLoading();
}); });
source.on(['tileloadend', 'tileloaderror'], function () {
source.on('tileloadend', function () {
progress.addLoaded();
});
source.on('tileloaderror', function () {
progress.addLoaded(); progress.addLoaded();
}); });
@@ -101,3 +84,10 @@ const map = new Map({
zoom: 2, zoom: 2,
}), }),
}); });
map.on('loadstart', function () {
progress.show();
});
map.on('loadend', function () {
progress.hide();
});
+15 -1
View File
@@ -26,8 +26,22 @@ export default {
* @api * @api
*/ */
MOVEEND: 'moveend', MOVEEND: 'moveend',
/**
* Triggered when loading of additional map data (tiles, images, features) starts.
* @event module:ol/render/Event~RenderEvent#loadstart
* @api
*/
LOADSTART: 'loadstart',
/**
* Triggered when loading of additional map data has completed.
* @event module:ol/render/Event~RenderEvent#loadend
* @api
*/
LOADEND: 'loadend',
}; };
/*** /***
* @typedef {'postrender'|'movestart'|'moveend'} Types * @typedef {'postrender'|'movestart'|'moveend'|'loadstart'|'loadend'} Types
*/ */
+34 -15
View File
@@ -211,10 +211,16 @@ class PluggableMap extends BaseObject {
/** /**
* @private * @private
* @type {boolean} * @type {boolean|undefined}
*/ */
this.renderComplete_; this.renderComplete_;
/**
* @private
* @type {boolean}
*/
this.loaded_ = true;
/** @private */ /** @private */
this.boundHandleBrowserEvent_ = this.handleBrowserEvent.bind(this); this.boundHandleBrowserEvent_ = this.handleBrowserEvent.bind(this);
@@ -1187,17 +1193,26 @@ class PluggableMap extends BaseObject {
} }
} }
if ( if (frameState && this.renderer_ && !frameState.animate) {
frameState && if (this.renderComplete_ === true) {
this.renderer_ && if (this.hasListener(RenderEventType.RENDERCOMPLETE)) {
this.hasListener(RenderEventType.RENDERCOMPLETE) && this.renderer_.dispatchRenderEvent(
!frameState.animate && RenderEventType.RENDERCOMPLETE,
this.renderComplete_ frameState
) { );
this.renderer_.dispatchRenderEvent( }
RenderEventType.RENDERCOMPLETE, if (this.loaded_ === false) {
frameState this.loaded_ = true;
); this.dispatchEvent(
new MapEvent(MapEventType.LOADEND, this, frameState)
);
}
} else if (this.loaded_ === true) {
this.loaded_ = false;
this.dispatchEvent(
new MapEvent(MapEventType.LOADSTART, this, frameState)
);
}
} }
const postRenderFunctions = this.postRenderFunctions_; const postRenderFunctions = this.postRenderFunctions_;
@@ -1573,9 +1588,13 @@ class PluggableMap extends BaseObject {
this.dispatchEvent(new MapEvent(MapEventType.POSTRENDER, this, frameState)); this.dispatchEvent(new MapEvent(MapEventType.POSTRENDER, this, frameState));
this.renderComplete_ = this.renderComplete_ =
!this.tileQueue_.getTilesLoading() && this.hasListener(MapEventType.LOADSTART) ||
!this.tileQueue_.getCount() && this.hasListener(MapEventType.LOADEND) ||
!this.getLoadingOrNotReady(); this.hasListener(RenderEventType.RENDERCOMPLETE)
? !this.tileQueue_.getTilesLoading() &&
!this.tileQueue_.getCount() &&
!this.getLoadingOrNotReady()
: undefined;
if (!this.postRenderTimeoutHandle_) { if (!this.postRenderTimeoutHandle_) {
this.postRenderTimeoutHandle_ = setTimeout(() => { this.postRenderTimeoutHandle_ = setTimeout(() => {
+91
View File
@@ -505,6 +505,97 @@ describe('ol/Map', function () {
}); });
}); });
describe('loadstart/loadend event sequence', function () {
let map;
beforeEach(function () {
const target = document.createElement('div');
target.style.width = '100px';
target.style.height = '100px';
document.body.appendChild(target);
map = new Map({
target: target,
layers: [
new TileLayer({
opacity: 0.5,
source: new XYZ({
url: 'spec/ol/data/osm-{z}-{x}-{y}.png',
}),
}),
new ImageLayer({
source: new ImageStatic({
url: 'spec/ol/data/osm-0-0-0.png',
imageExtent: getProjection('EPSG:3857').getExtent(),
projection: 'EPSG:3857',
}),
}),
new VectorLayer({
source: new VectorSource({
url: 'spec/ol/data/point.json',
format: new GeoJSON(),
}),
}),
new VectorLayer({
source: new VectorSource({
url: 'spec/ol/data/point.json',
format: new GeoJSON(),
strategy: tileStrategy(createXYZ()),
}),
}),
new VectorLayer({
source: new VectorSource({
features: [new Feature(new Point([0, 0]))],
}),
}),
new VectorLayer({
source: new VectorSource({
loader: function (extent, resolution, projection) {
this.addFeature(new Feature(new Point([0, 0])));
},
}),
}),
new WebGLPointsLayer({
source: new VectorSource({
features: [new Feature(new Point([0, 0]))],
}),
style: {
symbol: {
color: 'red',
symbolType: 'circle',
},
},
}),
],
});
});
afterEach(function () {
document.body.removeChild(map.getTargetElement());
map.setTarget(null);
map.dispose();
map.getLayers().forEach((layer) => layer.dispose());
});
it('is a reliable start-end sequence', function (done) {
const layers = map.getLayers().getArray();
expect(layers[6].getRenderer().ready).to.be(false);
let loading = 0;
map.on('loadstart', () => {
map.getView().setZoom(0.1);
loading++;
});
map.on('loadend', () => {
expect(loading).to.be(1);
done();
});
map.setView(
new View({
center: [0, 0],
zoom: 0,
})
);
});
});
describe('#getFeaturesAtPixel', function () { describe('#getFeaturesAtPixel', function () {
let target, map, layer; let target, map, layer;
beforeEach(function () { beforeEach(function () {