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

View File

@@ -3,11 +3,13 @@ layout: example.html
title: Image Load Events
shortdesc: Example using image load events.
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>,
and <code>imageloaderror</code> type events to monitor image loading
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"
---
<div class="wrapper">

View File

@@ -18,9 +18,6 @@ function Progress(el) {
* Increment the count of loading tiles.
*/
Progress.prototype.addLoading = function () {
if (this.loading === 0) {
this.show();
}
++this.loading;
this.update();
};
@@ -29,11 +26,8 @@ Progress.prototype.addLoading = function () {
* Increment the count of loaded tiles.
*/
Progress.prototype.addLoaded = function () {
const this_ = this;
setTimeout(function () {
++this_.loaded;
this_.update();
}, 100);
++this.loaded;
this.update();
};
/**
@@ -42,14 +36,6 @@ Progress.prototype.addLoaded = function () {
Progress.prototype.update = function () {
const width = ((this.loaded / this.loading) * 100).toFixed(1) + '%';
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.
*/
Progress.prototype.hide = function () {
if (this.loading === this.loaded) {
this.el.style.visibility = 'hidden';
this.el.style.width = 0;
}
const style = this.el.style;
setTimeout(function () {
style.visibility = 'hidden';
style.width = 0;
}, 250);
};
const progress = new Progress(document.getElementById('progress'));
@@ -80,11 +67,7 @@ const source = new ImageWMS({
source.on('imageloadstart', function () {
progress.addLoading();
});
source.on('imageloadend', function () {
progress.addLoaded();
});
source.on('imageloaderror', function () {
source.on(['imageloadend', 'imageloaderror'], function () {
progress.addLoaded();
});
@@ -96,3 +79,10 @@ const map = new Map({
zoom: 4,
}),
});
map.on('loadstart', function () {
progress.show();
});
map.on('loadend', function () {
progress.hide();
});

30
examples/load-events.css Normal file
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
examples/load-events.html Normal file
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
examples/load-events.js Normal file
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');
});

View File

@@ -7,7 +7,9 @@ docs: >
listen for <code>tileloadstart</code>, <code>tileloadend</code>,
and <code>tileloaderror</code> type events to monitor tile loading
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"
cloak:
- key: pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2t0cGdwMHVnMGdlbzMxbDhwazBic2xrNSJ9.WbcTL9uj8JPAsnT9mgb7oQ

View File

@@ -18,9 +18,6 @@ function Progress(el) {
* Increment the count of loading tiles.
*/
Progress.prototype.addLoading = function () {
if (this.loading === 0) {
this.show();
}
++this.loading;
this.update();
};
@@ -29,11 +26,8 @@ Progress.prototype.addLoading = function () {
* Increment the count of loaded tiles.
*/
Progress.prototype.addLoaded = function () {
const this_ = this;
setTimeout(function () {
++this_.loaded;
this_.update();
}, 100);
++this.loaded;
this.update();
};
/**
@@ -42,14 +36,6 @@ Progress.prototype.addLoaded = function () {
Progress.prototype.update = function () {
const width = ((this.loaded / this.loading) * 100).toFixed(1) + '%';
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.
*/
Progress.prototype.hide = function () {
if (this.loading === this.loaded) {
this.el.style.visibility = 'hidden';
this.el.style.width = 0;
}
const style = this.el.style;
setTimeout(function () {
style.visibility = 'hidden';
style.width = 0;
}, 250);
};
const progress = new Progress(document.getElementById('progress'));
@@ -85,11 +72,7 @@ const source = new XYZ({
source.on('tileloadstart', function () {
progress.addLoading();
});
source.on('tileloadend', function () {
progress.addLoaded();
});
source.on('tileloaderror', function () {
source.on(['tileloadend', 'tileloaderror'], function () {
progress.addLoaded();
});
@@ -101,3 +84,10 @@ const map = new Map({
zoom: 2,
}),
});
map.on('loadstart', function () {
progress.show();
});
map.on('loadend', function () {
progress.hide();
});

View File

@@ -26,8 +26,22 @@ export default {
* @api
*/
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
*/

View File

@@ -211,10 +211,16 @@ class PluggableMap extends BaseObject {
/**
* @private
* @type {boolean}
* @type {boolean|undefined}
*/
this.renderComplete_;
/**
* @private
* @type {boolean}
*/
this.loaded_ = true;
/** @private */
this.boundHandleBrowserEvent_ = this.handleBrowserEvent.bind(this);
@@ -1187,17 +1193,26 @@ class PluggableMap extends BaseObject {
}
}
if (
frameState &&
this.renderer_ &&
this.hasListener(RenderEventType.RENDERCOMPLETE) &&
!frameState.animate &&
this.renderComplete_
) {
this.renderer_.dispatchRenderEvent(
RenderEventType.RENDERCOMPLETE,
frameState
);
if (frameState && this.renderer_ && !frameState.animate) {
if (this.renderComplete_ === true) {
if (this.hasListener(RenderEventType.RENDERCOMPLETE)) {
this.renderer_.dispatchRenderEvent(
RenderEventType.RENDERCOMPLETE,
frameState
);
}
if (this.loaded_ === false) {
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_;
@@ -1573,9 +1588,13 @@ class PluggableMap extends BaseObject {
this.dispatchEvent(new MapEvent(MapEventType.POSTRENDER, this, frameState));
this.renderComplete_ =
!this.tileQueue_.getTilesLoading() &&
!this.tileQueue_.getCount() &&
!this.getLoadingOrNotReady();
this.hasListener(MapEventType.LOADSTART) ||
this.hasListener(MapEventType.LOADEND) ||
this.hasListener(RenderEventType.RENDERCOMPLETE)
? !this.tileQueue_.getTilesLoading() &&
!this.tileQueue_.getCount() &&
!this.getLoadingOrNotReady()
: undefined;
if (!this.postRenderTimeoutHandle_) {
this.postRenderTimeoutHandle_ = setTimeout(() => {

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 () {
let target, map, layer;
beforeEach(function () {