diff --git a/src/ol/interaction/dragpaninteraction.js b/src/ol/interaction/dragpaninteraction.js index 8a49b6846a..d3a2c2fa7d 100644 --- a/src/ol/interaction/dragpaninteraction.js +++ b/src/ol/interaction/dragpaninteraction.js @@ -40,6 +40,7 @@ ol.interaction.DragPan.prototype.handleDrag = function(mapBrowserEvent) { } var newCenter = new ol.Coordinate( this.startCenter.x + delta.x, this.startCenter.y + delta.y); + map.requestRenderFrame(); map.setCenter(newCenter); }; @@ -50,6 +51,7 @@ ol.interaction.DragPan.prototype.handleDrag = function(mapBrowserEvent) { ol.interaction.DragPan.prototype.handleDragStart = function(mapBrowserEvent) { var browserEvent = mapBrowserEvent.browserEvent; if (this.condition_(browserEvent)) { + mapBrowserEvent.map.requestRenderFrame(); return true; } else { return false; diff --git a/src/ol/interaction/dragrotateandzoominteraction.js b/src/ol/interaction/dragrotateandzoominteraction.js index 730b4def6a..0fa9992f65 100644 --- a/src/ol/interaction/dragrotateandzoominteraction.js +++ b/src/ol/interaction/dragrotateandzoominteraction.js @@ -50,7 +50,9 @@ ol.interaction.DragRotateAndZoom.prototype.handleDrag = browserEvent.offsetX - size.width / 2, size.height / 2 - browserEvent.offsetY); var theta = Math.atan2(delta.y, delta.x); - // FIXME this should use map.withFrozenRendering but an assertion fails :-( + map.requestRenderFrame(); + // FIXME the calls to map.rotate and map.zoomToResolution should use + // map.withFrozenRendering but an assertion fails :-( map.rotate(this.startRotation_, -theta); var resolution = this.startRatio_ * delta.magnitude(); map.zoomToResolution(resolution); @@ -73,6 +75,7 @@ ol.interaction.DragRotateAndZoom.prototype.handleDragStart = var theta = Math.atan2(delta.y, delta.x); this.startRotation_ = (map.getRotation() || 0) + theta; this.startRatio_ = resolution / delta.magnitude(); + map.requestRenderFrame(); return true; } else { return false; diff --git a/src/ol/interaction/dragrotateinteraction.js b/src/ol/interaction/dragrotateinteraction.js index b40ab74690..d574a72a64 100644 --- a/src/ol/interaction/dragrotateinteraction.js +++ b/src/ol/interaction/dragrotateinteraction.js @@ -42,6 +42,7 @@ ol.interaction.DragRotate.prototype.handleDrag = function(mapBrowserEvent) { var theta = Math.atan2( size.height / 2 - offset.y, offset.x - size.width / 2); + map.requestRenderFrame(); map.rotate(this.startRotation_, -theta); }; @@ -55,6 +56,7 @@ ol.interaction.DragRotate.prototype.handleDragStart = var map = mapBrowserEvent.map; if (browserEvent.isMouseActionButton() && this.condition_(browserEvent) && map.canRotate()) { + map.requestRenderFrame(); var size = map.getSize(); var offset = mapBrowserEvent.getPixel(); var theta = Math.atan2( diff --git a/src/ol/interaction/mousewheelzoominteraction.js b/src/ol/interaction/mousewheelzoominteraction.js index c9b9ec47a7..0e439742c3 100644 --- a/src/ol/interaction/mousewheelzoominteraction.js +++ b/src/ol/interaction/mousewheelzoominteraction.js @@ -36,6 +36,7 @@ ol.interaction.MouseWheelZoom.prototype.handleMapBrowserEvent = goog.asserts.assert(mouseWheelEvent instanceof goog.events.MouseWheelEvent); var anchor = mapBrowserEvent.getCoordinate(); var delta = mouseWheelEvent.deltaY < 0 ? this.delta_ : -this.delta_; + map.requestRenderFrame(); map.zoom(delta, anchor); mapBrowserEvent.preventDefault(); mouseWheelEvent.preventDefault(); diff --git a/src/ol/map.js b/src/ol/map.js index 6ad2e77d52..3e0296695d 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -8,6 +8,7 @@ goog.provide('ol.MapProperty'); goog.provide('ol.RendererHint'); goog.require('goog.array'); +goog.require('goog.async.AnimationDelay'); goog.require('goog.debug.Logger'); goog.require('goog.dispose'); goog.require('goog.dom'); @@ -22,8 +23,6 @@ goog.require('goog.events.MouseWheelEvent'); goog.require('goog.events.MouseWheelHandler'); goog.require('goog.events.MouseWheelHandler.EventType'); goog.require('goog.functions'); -goog.require('goog.fx.anim'); -goog.require('goog.fx.anim.Animated'); goog.require('goog.object'); goog.require('ol.BrowserFeature'); goog.require('ol.Collection'); @@ -115,7 +114,6 @@ ol.MapProperty = { /** * @constructor * @extends {ol.Object} - * @implements {goog.fx.anim.Animated} * @param {ol.MapOptions} mapOptions Map options. */ ol.Map = function(mapOptions) { @@ -146,15 +144,11 @@ ol.Map = function(mapOptions) { /** * @private - * @type {boolean} + * @type {goog.async.AnimationDelay} */ - this.animatedRenderer_ = false; - - /** - * @private - * @type {number} - */ - this.animatingCount_ = 0; + this.animationDelay_ = + new goog.async.AnimationDelay(this.renderFrame_, undefined, this); + this.registerDisposable(this.animationDelay_); /** * @private @@ -652,14 +646,6 @@ ol.Map.prototype.handleBrowserWindowResize = function() { }; -/** - * @return {boolean} Is animating. - */ -ol.Map.prototype.isAnimating = function() { - return this.animatingCount_ > 0; -}; - - /** * @return {boolean} Is defined. */ @@ -670,17 +656,6 @@ ol.Map.prototype.isDef = function() { }; -/** - * @inheritDoc - */ -ol.Map.prototype.onAnimationFrame = function() { - if (goog.DEBUG) { - this.logger.info('onAnimationFrame'); - } - this.renderFrame_(); -}; - - /** * @private */ @@ -704,33 +679,43 @@ ol.Map.prototype.recalculateTransforms_ = function() { * Render. */ ol.Map.prototype.render = function() { - if (this.animatingCount_ < 1) { - if (this.freezeRenderingCount_ === 0) { - this.renderFrame_(); - } else { - this.dirty_ = true; - } + if (this.animationDelay_.isActive()) { + // pass + } else if (this.freezeRenderingCount_ === 0) { + this.animationDelay_.fire(); + } else { + this.dirty_ = true; } }; /** + * Request that renderFrame_ be called some time in the future. + */ +ol.Map.prototype.requestRenderFrame = function() { + if (this.freezeRenderingCount_ === 0) { + if (!this.animationDelay_.isActive()) { + this.animationDelay_.start(); + } + } else { + this.dirty_ = true; + } +}; + + +/** + * @param {number} time Time. * @private */ -ol.Map.prototype.renderFrame_ = function() { +ol.Map.prototype.renderFrame_ = function(time) { + if (this.freezeRenderingCount_ != 0) { + return; + } if (goog.DEBUG) { this.logger.info('renderFrame_'); } - var animatedRenderer = this.renderer_.render(); + this.renderer_.renderFrame(time); this.dirty_ = false; - if (animatedRenderer != this.animatedRenderer_) { - if (animatedRenderer) { - this.startAnimating(); - } else { - this.stopAnimating(); - } - this.animatedRenderer_ = animatedRenderer; - } if (goog.DEBUG) { this.logger.info('postrender'); } @@ -852,42 +837,13 @@ goog.exportProperty( ol.Map.prototype.setUserProjection); -/** - * Start animating. - */ -ol.Map.prototype.startAnimating = function() { - if (++this.animatingCount_ == 1) { - if (goog.DEBUG) { - this.logger.info('startAnimating'); - } - goog.fx.anim.registerAnimation(this); - } -}; - - -/** - * Stop animating. - */ -ol.Map.prototype.stopAnimating = function() { - goog.asserts.assert(this.animatingCount_ > 0); - if (--this.animatingCount_ === 0) { - if (goog.DEBUG) { - this.logger.info('stopAnimating'); - } - goog.fx.anim.unregisterAnimation(this); - } -}; - - /** * Unfreeze rendering. */ ol.Map.prototype.unfreezeRendering = function() { goog.asserts.assert(this.freezeRenderingCount_ > 0); - if (--this.freezeRenderingCount_ === 0 && - this.animatingCount_ < 1 && - this.dirty_) { - this.renderFrame_(); + if (--this.freezeRenderingCount_ === 0 && this.dirty_) { + this.animationDelay_.fire(); } }; diff --git a/src/ol/renderer/dom/domlayerrenderer.js b/src/ol/renderer/dom/domlayerrenderer.js index cddf4ce527..ed7ae1203f 100644 --- a/src/ol/renderer/dom/domlayerrenderer.js +++ b/src/ol/renderer/dom/domlayerrenderer.js @@ -71,8 +71,9 @@ ol.renderer.dom.Layer.prototype.handleLayerVisibleChange = function() { /** * Render. + * @param {number} time Time. */ -ol.renderer.dom.Layer.prototype.render = goog.abstractMethod; +ol.renderer.dom.Layer.prototype.renderFrame = goog.abstractMethod; /** diff --git a/src/ol/renderer/dom/dommaprenderer.js b/src/ol/renderer/dom/dommaprenderer.js index 82f8d5cd1c..108f8a2cb9 100644 --- a/src/ol/renderer/dom/dommaprenderer.js +++ b/src/ol/renderer/dom/dommaprenderer.js @@ -135,11 +135,7 @@ ol.renderer.dom.Map.prototype.createLayerRenderer = function(layer) { */ ol.renderer.dom.Map.prototype.handleCenterChanged = function() { goog.base(this, 'handleCenterChanged'); - var map = this.getMap(); - if (!map.isDef()) { - return; - } - map.render(); + this.getMap().render(); }; @@ -148,11 +144,7 @@ ol.renderer.dom.Map.prototype.handleCenterChanged = function() { */ ol.renderer.dom.Map.prototype.handleResolutionChanged = function() { goog.base(this, 'handleResolutionChanged'); - var map = this.getMap(); - if (!map.isDef()) { - return; - } - map.render(); + this.getMap().render(); }; @@ -160,11 +152,8 @@ ol.renderer.dom.Map.prototype.handleResolutionChanged = function() { * @inheritDoc */ ol.renderer.dom.Map.prototype.handleRotationChanged = function() { - var map = this.getMap(); - if (!map.isDef()) { - return; - } - map.render(); + goog.base(this, 'handleRotationChanged'); + this.getMap().render(); }; @@ -173,24 +162,19 @@ ol.renderer.dom.Map.prototype.handleRotationChanged = function() { */ ol.renderer.dom.Map.prototype.handleSizeChanged = function() { goog.base(this, 'handleSizeChanged'); - var map = this.getMap(); - if (!map.isDef()) { - return; - } - map.render(); + this.getMap().render(); }; /** * Render the map. Sets up the layers pane on first render and adjusts its * position as needed on subsequent calls. - * - * @return {boolean} Animating. + * @inheritDoc */ -ol.renderer.dom.Map.prototype.render = function() { +ol.renderer.dom.Map.prototype.renderFrame = function(time) { var map = this.getMap(); if (!map.isDef()) { - return false; + return; } var mapCenter = map.getCenter(); @@ -228,14 +212,16 @@ ol.renderer.dom.Map.prototype.render = function() { this.renderedRotation_ = mapRotation; this.renderedSize_ = mapSize; - var animate = false; + var requestRenderFrame = false; this.forEachReadyVisibleLayer(function(layer, layerRenderer) { - if (layerRenderer.render()) { - animate = true; + if (layerRenderer.renderFrame(time)) { + requestRenderFrame = true; } }); - return animate; + if (requestRenderFrame) { + map.requestRenderFrame(); + } }; diff --git a/src/ol/renderer/dom/domtilelayerrenderer.js b/src/ol/renderer/dom/domtilelayerrenderer.js index 7b3abd7d0d..e753a6020c 100644 --- a/src/ol/renderer/dom/domtilelayerrenderer.js +++ b/src/ol/renderer/dom/domtilelayerrenderer.js @@ -33,11 +33,6 @@ ol.renderer.dom.TileLayer = function(mapRenderer, tileLayer, target) { */ this.renderedMapResolution_ = undefined; - /** - * @type {Object.} - * @private - */ - this.tileChangeListenerKeys_ = {}; }; goog.inherits(ol.renderer.dom.TileLayer, ol.renderer.dom.Layer); @@ -91,24 +86,10 @@ ol.renderer.dom.TileLayer.prototype.removeExtraTiles_ = }; -/** - * @param {goog.events.Event} event Tile change event. - * @private - */ -ol.renderer.dom.TileLayer.prototype.handleTileChange_ = function(event) { - var tile = /** @type {ol.Tile} */ (event.target); - goog.asserts.assert(tile.getState() == ol.TileState.LOADED); - var tileKey = goog.getUid(tile); - goog.asserts.assert(tileKey in this.tileChangeListenerKeys_); - delete this.tileChangeListenerKeys_[tileKey]; - this.render(); -}; - - /** * @inheritDoc */ -ol.renderer.dom.TileLayer.prototype.render = function() { +ol.renderer.dom.TileLayer.prototype.renderFrame = function(time) { var map = this.getMap(); if (!map.isDef()) { @@ -134,6 +115,7 @@ ol.renderer.dom.TileLayer.prototype.render = function() { var tileRange = tileGrid.getTileRangeForExtentAndResolution(mapExtent, mapResolution); + var allTilesLoaded = true; // first pass through the tile range to determine all the tiles needed tileRange.forEachTileCoord(z, function(tileCoord) { @@ -145,18 +127,15 @@ ol.renderer.dom.TileLayer.prototype.render = function() { var key = tile.tileCoord.toString(); var state = tile.getState(); - if (state == ol.TileState.LOADED) { + if (state == ol.TileState.IDLE) { + tile.load(); + } else if (state == ol.TileState.LOADED) { tilesToDrawByZ[z][key] = tile; return; - } else { - var tileKey = goog.getUid(tile); - if (!(tileKey in this.tileChangeListenerKeys_)) { - this.tileChangeListenerKeys_[tileKey] = goog.events.listen(tile, - goog.events.EventType.CHANGE, this.handleTileChange_, false, this); - tile.load(); - } } + allTilesLoaded = false; + /** * Look for already loaded tiles at alternate z that can serve as * placeholders until tiles at the current z have loaded. @@ -243,4 +222,6 @@ ol.renderer.dom.TileLayer.prototype.render = function() { this.renderedMapResolution_ = mapResolution; this.removeExtraTiles_(tilesToDrawByZ); + + return !allTilesLoaded; }; diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index 55b0fd5e08..b1caf6a48d 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -309,9 +309,10 @@ ol.renderer.Map.prototype.removeLayerRenderer = function(layer) { /** - * @return {boolean} Animating. + * Render. + * @param {number} time Time. */ -ol.renderer.Map.prototype.render = goog.functions.FALSE; +ol.renderer.Map.prototype.renderFrame = goog.functions.FALSE; /** diff --git a/src/ol/renderer/webgl/webgllayerrenderer.js b/src/ol/renderer/webgl/webgllayerrenderer.js index 90e457fa2d..465d6ac55a 100644 --- a/src/ol/renderer/webgl/webgllayerrenderer.js +++ b/src/ol/renderer/webgl/webgllayerrenderer.js @@ -112,5 +112,7 @@ ol.renderer.webgl.Layer.prototype.handleWebGLContextLost = goog.nullFunction; /** * Render. + * @param {number} time Time. + * @return {boolean} Request render frame. */ -ol.renderer.webgl.Layer.prototype.render = goog.abstractMethod; +ol.renderer.webgl.Layer.prototype.renderFrame = goog.abstractMethod; diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index 13ea482b40..5a7d893bfb 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -601,12 +601,20 @@ ol.renderer.webgl.Map.prototype.removeLayerRenderer = function(layer) { /** * @inheritDoc */ -ol.renderer.webgl.Map.prototype.render = function() { +ol.renderer.webgl.Map.prototype.renderFrame = function(time) { if (!this.getMap().isDef()) { - return false; + return; } + var requestRenderFrame = false; + + this.forEachReadyVisibleLayer(function(layer, layerRenderer) { + if (layerRenderer.renderFrame(time)) { + requestRenderFrame = true; + } + }); + var size = /** @type {ol.Size} */ this.getMap().getSize(); if (!this.canvasSize_.equals(size)) { this.canvas_.width = size.width; @@ -616,7 +624,7 @@ ol.renderer.webgl.Map.prototype.render = function() { var animate = false; this.forEachReadyVisibleLayer(function(layer, layerRenderer) { - if (layerRenderer.render()) { + if (layerRenderer.renderFrame(time)) { animate = true; } }); @@ -684,7 +692,9 @@ ol.renderer.webgl.Map.prototype.render = function() { gl.drawArrays(goog.webgl.TRIANGLE_STRIP, 0, 4); }, this); - return animate; + if (requestRenderFrame) { + this.getMap().requestRenderFrame(); + } }; diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index 8722ce4064..2b7e9d5b9c 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -1,5 +1,7 @@ // FIXME large resolutions lead to too large framebuffers :-( // FIXME animated shaders! check in redraw +// FIXME throttle texture uploads +// FIXME prioritize texture uploads goog.provide('ol.renderer.webgl.TileLayer'); goog.provide('ol.renderer.webgl.tilelayerrenderer'); @@ -141,12 +143,6 @@ ol.renderer.webgl.TileLayer = function(mapRenderer, tileLayer) { */ this.framebufferDimension_ = undefined; - /** - * @private - * @type {Object.} - */ - this.tileChangeListenerKeys_ = {}; - /** * @private * @type {goog.vec.Mat4.AnyType} @@ -276,19 +272,6 @@ ol.renderer.webgl.TileLayer.prototype.getTexture = function() { }; -/** - * @param {goog.events.Event} event Event. - * @protected - */ -ol.renderer.webgl.TileLayer.prototype.handleTileChange = function(event) { - var tile = /** @type {ol.Tile} */ (event.target); - var tileKey = goog.getUid(tile); - goog.asserts.assert(tileKey in this.tileChangeListenerKeys_); - delete this.tileChangeListenerKeys_[tileKey]; - this.dispatchChangeEvent(); -}; - - /** * @inheritDoc */ @@ -304,9 +287,9 @@ ol.renderer.webgl.TileLayer.prototype.handleWebGLContextLost = function() { /** * @inheritDoc */ -ol.renderer.webgl.TileLayer.prototype.render = function() { +ol.renderer.webgl.TileLayer.prototype.renderFrame = function(time) { - var animate = false; + var requestRenderFrame = false; var mapRenderer = this.getMapRenderer(); var map = this.getMap(); @@ -413,26 +396,26 @@ ol.renderer.webgl.TileLayer.prototype.render = function() { var tile = tileSource.getTile(tileCoord); if (goog.isNull(tile)) { - // FIXME consider returning here as this is outside the source's extent - } else if (tile.getState() == ol.TileState.LOADED) { - if (mapRenderer.isImageTextureLoaded(tile.getImage())) { + return; + } + + var tileState = tile.getState(); + if (tileState == ol.TileState.IDLE) { + tile.load(); + } else if (tileState == ol.TileState.LOADED) { + var image = tile.getImage(); + if (mapRenderer.isImageTextureLoaded(image)) { tilesToDrawByZ[z][tileCoord.toString()] = tile; return; } else { - imagesToLoad.push(tile.getImage()); - allTilesLoaded = false; + imagesToLoad.push(image); } - } else { - var tileKey = goog.getUid(tile); - if (!(tileKey in this.tileChangeListenerKeys_)) { - tile.load(); - // FIXME will need to handle aborts as well - this.tileChangeListenerKeys_[tileKey] = goog.events.listen(tile, - goog.events.EventType.CHANGE, this.handleTileChange, false, this); - } - allTilesLoaded = false; + } else if (tileState == ol.TileState.ERROR) { + return; } + allTilesLoaded = false; + // FIXME this could be more efficient about filling partial holes tileGrid.forEachTileCoordParentTileRange( tileCoord, @@ -497,7 +480,6 @@ ol.renderer.webgl.TileLayer.prototype.render = function() { this.logger.info('uploaded textures'); } }, mapRenderer, imagesToLoad)); - animate = true; } if (allTilesLoaded) { @@ -506,6 +488,7 @@ ol.renderer.webgl.TileLayer.prototype.render = function() { } else { this.renderedTileRange_ = null; this.renderedFramebufferExtent_ = null; + requestRenderFrame = true; } } @@ -531,6 +514,6 @@ ol.renderer.webgl.TileLayer.prototype.render = function() { -0.5, 0); - return animate; + return requestRenderFrame; }; diff --git a/test/spec/ol/map.test.js b/test/spec/ol/map.test.js index a936388a7d..1ce6d9f694 100644 --- a/test/spec/ol/map.test.js +++ b/test/spec/ol/map.test.js @@ -1,5 +1,11 @@ +goog.require('goog.async.AnimationDelay'); goog.require('goog.dom'); +goog.require('ol.Collection'); +goog.require('ol.Coordinate'); goog.require('ol.Map'); +goog.require('ol.RendererHint'); +goog.require('ol.layer.TileLayer'); +goog.require('ol.source.XYZ'); describe('ol.Map', function() { @@ -135,4 +141,102 @@ describe('ol.Map', function() { }); }); }); + + describe('user animation', function() { + + var layer, map; + beforeEach(function() { + // always use setTimeout based shim for requestAnimationFrame + spyOn(goog.async.AnimationDelay.prototype, 'getRaf_') + .andCallFake(function() {return null;}); + + layer = new ol.layer.TileLayer({ + source: new ol.source.XYZ({ + url: 'foo', + maxZoom: 2 + }) + }); + + map = new ol.Map({ + center: new ol.Coordinate(0, 0), + layers: new ol.Collection([layer]), + renderer: ol.RendererHint.DOM, + target: 'map', + zoom: 1 + }); + }); + + afterEach(function() { + map.dispose(); + layer.dispose(); + }); + + it('can set up an animation loop', function() { + + function quadInOut(t, b, c, d) { + if ((t /= d / 2) < 1) { + return c / 2 * t * t + b; + } + return -c / 2 * ((--t) * (t - 2) - 1) + b; + } + + var duration = 500; + var destination = new ol.Coordinate(1000, 1000); + + var origin = map.getCenter(); + var start = new Date().getTime(); + var x0 = origin.x; + var y0 = origin.y; + var dx = destination.x - origin.x; + var dy = destination.y - origin.y; + + var o = { + callback: function() { + var dt = new Date().getTime() - start; + var more = dt <= duration, + x, y; + if (more) { + x = quadInOut(dt, x0, dx, duration); + y = quadInOut(dt, y0, dy, duration); + } else { + x = destination.x; + y = destination.y; + } + map.setCenter(new ol.Coordinate(x, y)); + if (more) { + animationDelay.start(); + } + } + }; + + spyOn(o, 'callback').andCallThrough(); + + var animationDelay = new goog.async.AnimationDelay(o.callback); + + animationDelay.start(); + + // confirm that the center is somewhere between origin and destination + // after a short delay + waits(100); + runs(function() { + expect(o.callback).toHaveBeenCalled(); + var loc = map.getCenter(); + expect(loc.x).not.toEqual(origin.x); + expect(loc.y).not.toEqual(origin.y); + expect(loc.x).not.toEqual(destination.x); + expect(loc.y).not.toEqual(destination.y); + }); + + // confirm that the map has reached the destination after the duration + waits(duration); + runs(function() { + var loc = map.getCenter(); + expect(loc.x).toEqual(destination.x); + expect(loc.y).toEqual(destination.y); + }); + + }); + + }); + });