From 1baa8be269b4d64679de04b67d540832db360f40 Mon Sep 17 00:00:00 2001 From: ahocevar Date: Tue, 21 Aug 2018 17:29:10 +0200 Subject: [PATCH] Add 'rendercomplete' event --- examples/export-map.js | 2 +- examples/export-pdf.js | 61 +++++++---------------------------- src/ol/PluggableMap.js | 24 ++++++++++++++ src/ol/render/EventType.js | 9 +++++- src/ol/renderer/Map.js | 7 ++++ src/ol/renderer/canvas/Map.js | 7 ++-- src/ol/renderer/webgl/Map.js | 7 ++-- src/ol/source/Image.js | 3 ++ src/ol/source/Source.js | 7 ++++ src/ol/source/Vector.js | 2 ++ test/spec/ol/map.test.js | 59 +++++++++++++++++++++++++++++++++ 11 files changed, 128 insertions(+), 60 deletions(-) diff --git a/examples/export-map.js b/examples/export-map.js index c39805e347..d5417c8bc1 100644 --- a/examples/export-map.js +++ b/examples/export-map.js @@ -30,7 +30,7 @@ const map = new Map({ }); document.getElementById('export-png').addEventListener('click', function() { - map.once('postcompose', function(event) { + map.once('rendercomplete', function(event) { const canvas = event.context.canvas; if (navigator.msSaveBlob) { navigator.msSaveBlob(canvas.msToBlob(), 'map.png'); diff --git a/examples/export-pdf.js b/examples/export-pdf.js index 32b58cc791..49cc99e3a0 100644 --- a/examples/export-pdf.js +++ b/examples/export-pdf.js @@ -3,7 +3,6 @@ import View from '../src/ol/View.js'; import {defaults as defaultControls} from '../src/ol/control.js'; import WKT from '../src/ol/format/WKT.js'; import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; -import {unByKey} from '../src/ol/Observable.js'; import {OSM, Vector as VectorSource} from '../src/ol/source.js'; const raster = new TileLayer({ @@ -48,9 +47,6 @@ const dims = { a5: [210, 148] }; -let loading = 0; -let loaded = 0; - const exportButton = document.getElementById('export-pdf'); exportButton.addEventListener('click', function() { @@ -66,57 +62,22 @@ exportButton.addEventListener('click', function() { const size = /** @type {module:ol/size~Size} */ (map.getSize()); const extent = map.getView().calculateExtent(size); - const source = raster.getSource(); - - const tileLoadStart = function() { - ++loading; - }; - - let timer; - let keys = []; - - function tileLoadEndFactory(canvas) { - return () => { - ++loaded; - if (timer) { - clearTimeout(timer); - timer = null; - } - if (loading === loaded) { - timer = window.setTimeout(() => { - loading = 0; - loaded = 0; - const data = canvas.toDataURL('image/jpeg'); - const pdf = new jsPDF('landscape', undefined, format); - pdf.addImage(data, 'JPEG', 0, 0, dim[0], dim[1]); - pdf.save('map.pdf'); - keys.forEach(unByKey); - keys = []; - map.setSize(size); - map.getView().fit(extent, {size}); - map.renderSync(); - exportButton.disabled = false; - document.body.style.cursor = 'auto'; - }, 500); - } - }; - } - - map.once('postcompose', function(event) { + map.once('rendercomplete', function(event) { const canvas = event.context.canvas; - const tileLoadEnd = tileLoadEndFactory(canvas); - keys = [ - source.on('tileloadstart', tileLoadStart), - source.on('tileloadend', tileLoadEnd), - source.on('tileloaderror', tileLoadEnd) - ]; - tileLoadEnd(); + const data = canvas.toDataURL('image/jpeg'); + const pdf = new jsPDF('landscape', undefined, format); + pdf.addImage(data, 'JPEG', 0, 0, dim[0], dim[1]); + pdf.save('map.pdf'); + // Reset original map size + map.setSize(size); + map.getView().fit(extent, {size}); + exportButton.disabled = false; + document.body.style.cursor = 'auto'; }); + // Set print size const printSize = [width, height]; map.setSize(printSize); map.getView().fit(extent, {size: printSize}); - loaded = -1; - map.renderSync(); }, false); diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 7f70ddf671..afc3448bc6 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -10,6 +10,7 @@ import MapBrowserEventType from './MapBrowserEventType.js'; import MapEvent from './MapEvent.js'; import MapEventType from './MapEventType.js'; import MapProperty from './MapProperty.js'; +import RenderEventType from './render/EventType.js'; import BaseObject, {getChangeEventType} from './Object.js'; import ObjectEventType from './ObjectEventType.js'; import TileQueue from './TileQueue.js'; @@ -135,6 +136,7 @@ import {create as createTransform, apply as applyTransform} from './transform.js * @fires module:ol/MapEvent~MapEvent * @fires module:ol/render/Event~RenderEvent#postcompose * @fires module:ol/render/Event~RenderEvent#precompose + * @fires module:ol/render/Event~RenderEvent#rendercomplete * @api */ class PluggableMap extends BaseObject { @@ -961,6 +963,10 @@ class PluggableMap extends BaseObject { tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); } } + if (frameState && this.hasListener(MapEventType.RENDERCOMPLETE) && !frameState.animate && + !this.tileQueue_.getTilesLoading() && !getLoading(this.getLayers().getArray())) { + this.renderer_.dispatchRenderEvent(RenderEventType.RENDERCOMPLETE, frameState); + } const postRenderFunctions = this.postRenderFunctions_; for (let i = 0, ii = postRenderFunctions.length; i < ii; ++i) { @@ -1407,3 +1413,21 @@ function createOptionsInternal(options) { } export default PluggableMap; + +/** + * @param {Array} layers Layers. + * @return {boolean} Layers have sources that are still loading. + */ +function getLoading(layers) { + for (let i = 0, ii = layers.length; i < ii; ++i) { + const layer = layers[i]; + if (layer instanceof LayerGroup) { + return getLoading(layer.getLayers().getArray()); + } + const source = layers[i].getSource(); + if (source && source.loading) { + return true; + } + } + return false; +} diff --git a/src/ol/render/EventType.js b/src/ol/render/EventType.js index 492b73dfba..cb33e22568 100644 --- a/src/ol/render/EventType.js +++ b/src/ol/render/EventType.js @@ -20,5 +20,12 @@ export default { * @event module:ol/render/Event~RenderEvent#render * @api */ - RENDER: 'render' + RENDER: 'render', + /** + * Triggered when rendering is complete, i.e. all sources and tiles have + * finished loading for the current viewport, and all tiles are faded in. + * @event module:ol/render/Event~RenderEvent#rendercomplete + * @api + */ + RENDERCOMPLETE: 'rendercomplete' }; diff --git a/src/ol/renderer/Map.js b/src/ol/renderer/Map.js index 4e2a6621c2..40901f3b9d 100644 --- a/src/ol/renderer/Map.js +++ b/src/ol/renderer/Map.js @@ -332,6 +332,13 @@ function expireIconCache(map, frameState) { MapRenderer.prototype.renderFrame = VOID; +/** + * @param {module:ol/render/EventType} type Event type. + * @param {module:ol/PluggableMap~FrameState} frameState Frame state. + */ +MapRenderer.prototype.dispatchRenderEvent = VOID; + + /** * @param {module:ol/layer/Layer~State} state1 First layer state. * @param {module:ol/layer/Layer~State} state2 Second layer state. diff --git a/src/ol/renderer/canvas/Map.js b/src/ol/renderer/canvas/Map.js index d812da60de..9af0e9054f 100644 --- a/src/ol/renderer/canvas/Map.js +++ b/src/ol/renderer/canvas/Map.js @@ -69,9 +69,8 @@ class CanvasMapRenderer extends MapRenderer { /** * @param {module:ol/render/EventType} type Event type. * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @private */ - dispatchComposeEvent_(type, frameState) { + dispatchRenderEvent(type, frameState) { const map = this.getMap(); const context = this.context_; if (map.hasListener(type)) { @@ -135,7 +134,7 @@ class CanvasMapRenderer extends MapRenderer { this.calculateMatrices2D(frameState); - this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, frameState); + this.dispatchRenderEvent(RenderEventType.PRECOMPOSE, frameState); const layerStatesArray = frameState.layerStatesArray; stableSort(layerStatesArray, sortByZIndex); @@ -164,7 +163,7 @@ class CanvasMapRenderer extends MapRenderer { context.restore(); } - this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, frameState); + this.dispatchRenderEvent(RenderEventType.POSTCOMPOSE, frameState); if (!this.renderedVisible_) { this.canvas_.style.display = ''; diff --git a/src/ol/renderer/webgl/Map.js b/src/ol/renderer/webgl/Map.js index 1717ffedb3..062e9928bf 100644 --- a/src/ol/renderer/webgl/Map.js +++ b/src/ol/renderer/webgl/Map.js @@ -249,9 +249,8 @@ class WebGLMapRenderer extends MapRenderer { /** * @param {module:ol/render/EventType} type Event type. * @param {module:ol/PluggableMap~FrameState} frameState Frame state. - * @private */ - dispatchComposeEvent_(type, frameState) { + dispatchRenderEvent(type, frameState) { const map = this.getMap(); if (map.hasListener(type)) { const context = this.context_; @@ -411,7 +410,7 @@ class WebGLMapRenderer extends MapRenderer { this.textureCache_.set((-frameState.index).toString(), null); ++this.textureCacheFrameMarkerCount_; - this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, frameState); + this.dispatchRenderEvent(RenderEventType.PRECOMPOSE, frameState); /** @type {Array} */ const layerStatesToDraw = []; @@ -470,7 +469,7 @@ class WebGLMapRenderer extends MapRenderer { frameState.animate = true; } - this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, frameState); + this.dispatchRenderEvent(RenderEventType.POSTCOMPOSE, frameState); this.scheduleRemoveUnusedLayerRenderers(frameState); this.scheduleExpireIconCache(frameState); diff --git a/src/ol/source/Image.js b/src/ol/source/Image.js index 0319b43f53..857ef54c01 100644 --- a/src/ol/source/Image.js +++ b/src/ol/source/Image.js @@ -201,16 +201,19 @@ class ImageSource extends Source { const image = /** @type {module:ol/Image} */ (event.target); switch (image.getState()) { case ImageState.LOADING: + this.loading = true; this.dispatchEvent( new ImageSourceEvent(ImageSourceEventType.IMAGELOADSTART, image)); break; case ImageState.LOADED: + this.loading = false; this.dispatchEvent( new ImageSourceEvent(ImageSourceEventType.IMAGELOADEND, image)); break; case ImageState.ERROR: + this.loading = false; this.dispatchEvent( new ImageSourceEvent(ImageSourceEventType.IMAGELOADERROR, image)); diff --git a/src/ol/source/Source.js b/src/ol/source/Source.js index ee1562aebc..001bf26fb7 100644 --- a/src/ol/source/Source.js +++ b/src/ol/source/Source.js @@ -66,6 +66,13 @@ class Source extends BaseObject { */ this.attributions_ = this.adaptAttributions_(options.attributions); + /** + * This source is currently loading data. Sources that defer loading to the + * map's tile queue never set this to `true`. + * @type {boolean} + */ + this.loading = false; + /** * @private * @type {module:ol/source/State} diff --git a/src/ol/source/Vector.js b/src/ol/source/Vector.js index 28d2c61d9c..1b813701ff 100644 --- a/src/ol/source/Vector.js +++ b/src/ol/source/Vector.js @@ -871,6 +871,7 @@ class VectorSource extends Source { loadFeatures(extent, resolution, projection) { const loadedExtentsRtree = this.loadedExtentsRtree_; const extentsToLoad = this.strategy_(extent, resolution); + this.loading = false; for (let i = 0, ii = extentsToLoad.length; i < ii; ++i) { const extentToLoad = extentsToLoad[i]; const alreadyLoaded = loadedExtentsRtree.forEachInExtent(extentToLoad, @@ -884,6 +885,7 @@ class VectorSource extends Source { if (!alreadyLoaded) { this.loader_.call(this, extentToLoad, resolution, projection); loadedExtentsRtree.insert(extentToLoad, {extent: extentToLoad.slice()}); + this.loading = true; } } } diff --git a/test/spec/ol/map.test.js b/test/spec/ol/map.test.js index 73f095280c..3355b30e8b 100644 --- a/test/spec/ol/map.test.js +++ b/test/spec/ol/map.test.js @@ -1,4 +1,5 @@ import Feature from '../../../src/ol/Feature.js'; +import ImageState from '../../../src/ol/ImageState.js'; import Map from '../../../src/ol/Map.js'; import MapEvent from '../../../src/ol/MapEvent.js'; import Overlay from '../../../src/ol/Overlay.js'; @@ -7,14 +8,18 @@ import LineString from '../../../src/ol/geom/LineString.js'; import {TOUCH} from '../../../src/ol/has.js'; import {focus} from '../../../src/ol/events/condition.js'; import {defaults as defaultInteractions} from '../../../src/ol/interaction.js'; +import {get as getProjection} from '../../../src/ol/proj.js'; +import GeoJSON from '../../../src/ol/format/GeoJSON.js'; import DragPan from '../../../src/ol/interaction/DragPan.js'; import DoubleClickZoom from '../../../src/ol/interaction/DoubleClickZoom.js'; import Interaction from '../../../src/ol/interaction/Interaction.js'; import MouseWheelZoom from '../../../src/ol/interaction/MouseWheelZoom.js'; import PinchZoom from '../../../src/ol/interaction/PinchZoom.js'; +import ImageLayer from '../../../src/ol/layer/Image.js'; import TileLayer from '../../../src/ol/layer/Tile.js'; import VectorLayer from '../../../src/ol/layer/Vector.js'; import IntermediateCanvasRenderer from '../../../src/ol/renderer/canvas/IntermediateCanvas.js'; +import ImageStatic from '../../../src/ol/source/ImageStatic.js'; import VectorSource from '../../../src/ol/source/Vector.js'; import XYZ from '../../../src/ol/source/XYZ.js'; @@ -188,6 +193,60 @@ describe('ol.Map', function() { }); + describe('rendercomplete event', function() { + + let map; + beforeEach(function() { + const target = document.createElement('div'); + target.style.width = target.style.height = '100px'; + document.body.appendChild(target); + map = new Map({ + target: target, + layers: [ + new TileLayer({ + 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() + }) + }) + ] + }); + }); + + afterEach(function() { + document.body.removeChild(map.getTargetElement()); + map.setTarget(null); + map.dispose(); + }); + + it('triggers when all tiles and sources are loaded and faded in', function(done) { + map.once('rendercomplete', function() { + const layers = map.getLayers().getArray(); + expect(map.tileQueue_.getTilesLoading()).to.be(0); + expect(layers[1].getSource().image_.getState()).to.be(ImageState.LOADED); + expect(layers[2].getSource().getFeatures().length).to.be(1); + done(); + }); + map.setView(new View({ + center: [0, 0], + zoom: 0 + })); + }); + + }); + describe('#getFeaturesAtPixel', function() { let target, map;