diff --git a/examples/image-load-events.html b/examples/image-load-events.html index 94bc19ad8c..0b4927239f 100644 --- a/examples/image-load-events.html +++ b/examples/image-load-events.html @@ -3,11 +3,13 @@ layout: example.html title: Image Load Events shortdesc: Example using image load events. docs: > -

Image sources fire events related to image loading. You can + Image sources fire events related to image loading. You can listen for imageloadstart, imageloadend, and imageloaderror 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.

+ renders an image loading progress bar at the bottom of the map. The + progress bar is shown and hidden according to the map's loadstart + and loadend events. tags: "image, events, loading" ---
diff --git a/examples/image-load-events.js b/examples/image-load-events.js index cdfa74eda2..5b13b7ece9 100644 --- a/examples/image-load-events.js +++ b/examples/image-load-events.js @@ -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(); +}); diff --git a/examples/load-events.css b/examples/load-events.css new file mode 100644 index 0000000000..1e02f3ac67 --- /dev/null +++ b/examples/load-events.css @@ -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; +} diff --git a/examples/load-events.html b/examples/load-events.html new file mode 100644 index 0000000000..69a8db0e61 --- /dev/null +++ b/examples/load-events.html @@ -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 loadstart and loadend 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 +--- +
diff --git a/examples/load-events.js b/examples/load-events.js new file mode 100644 index 0000000000..1b6d026ce1 --- /dev/null +++ b/examples/load-events.js @@ -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 = + '© MapTiler ' + + '© OpenStreetMap contributors'; + +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'); +}); diff --git a/examples/tile-load-events.html b/examples/tile-load-events.html index 821dd4967e..a640324d62 100644 --- a/examples/tile-load-events.html +++ b/examples/tile-load-events.html @@ -7,7 +7,9 @@ docs: > listen for tileloadstart, tileloadend, and tileloaderror 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.

+ renders a tile loading progress bar at the bottom of the map. The + progress bar is shown and hidden according to the map's loadstart + and loadend events. tags: "tile, events, loading" cloak: - key: pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiY2t0cGdwMHVnMGdlbzMxbDhwazBic2xrNSJ9.WbcTL9uj8JPAsnT9mgb7oQ diff --git a/examples/tile-load-events.js b/examples/tile-load-events.js index b7c29233bb..25d3eb05aa 100644 --- a/examples/tile-load-events.js +++ b/examples/tile-load-events.js @@ -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(); +}); diff --git a/src/ol/MapEventType.js b/src/ol/MapEventType.js index 08ac0645c2..be63b9c68d 100644 --- a/src/ol/MapEventType.js +++ b/src/ol/MapEventType.js @@ -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 */ diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 2cade9efc8..809843eae5 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -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(() => { diff --git a/test/browser/spec/ol/Map.test.js b/test/browser/spec/ol/Map.test.js index 480dbe6b2d..79ed62456d 100644 --- a/test/browser/spec/ol/Map.test.js +++ b/test/browser/spec/ol/Map.test.js @@ -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 () {