diff --git a/css/ol.css b/css/ol.css index 435276a222..5e679f5ddc 100644 --- a/css/ol.css +++ b/css/ol.css @@ -78,8 +78,8 @@ } } .ol-zoom-in { - border-radius: 4px 4px 0 0; + border-radius: 2px 2px 0 0; } .ol-zoom-out { - border-radius: 0 0 4px 4px; + border-radius: 0 0 2px 2px; } diff --git a/examples/overlay-and-popup.html b/examples/anchored-elements.html similarity index 86% rename from examples/overlay-and-popup.html rename to examples/anchored-elements.html index 067adbf8b4..5d7b25b747 100644 --- a/examples/overlay-and-popup.html +++ b/examples/anchored-elements.html @@ -80,7 +80,7 @@ margin-left: -13px; } - Overlay example + Anchored elements example
@@ -89,16 +89,16 @@
-

Overlay example

-
Demonstrates Overlays.
+

Anchored elements example

+
Demonstrates anchored elements.

See the - overlay-and-popup.js source + anchored-elements.js source to see how this is done.

-
overlay, popup, mapquest, openaerial
- +
anchored elements, overlay, popup, mapquest, openaerial
+ diff --git a/examples/overlay-and-popup.js b/examples/anchored-elements.js similarity index 86% rename from examples/overlay-and-popup.js rename to examples/anchored-elements.js index 1e19bb425b..80fac6d6bf 100644 --- a/examples/overlay-and-popup.js +++ b/examples/anchored-elements.js @@ -1,12 +1,12 @@ goog.require('goog.debug.Console'); goog.require('goog.debug.Logger'); goog.require('goog.debug.Logger.Level'); +goog.require('ol.AnchoredElement'); goog.require('ol.Collection'); goog.require('ol.Coordinate'); goog.require('ol.Map'); goog.require('ol.RendererHints'); goog.require('ol.View2D'); -goog.require('ol.overlay.Overlay'); goog.require('ol.source.MapQuestOpenAerial'); @@ -31,15 +31,15 @@ var map = new ol.Map({ }); // Vienna label -var vienna = new ol.overlay.Overlay({ +var vienna = new ol.AnchoredElement({ map: map, - coordinate: ol.Projection.transformWithCodes( + position: ol.Projection.transformWithCodes( new ol.Coordinate(16.3725, 48.208889), 'EPSG:4326', 'EPSG:3857'), element: document.getElementById('vienna') }); // Popup showing the position the user clicked -var popup = new ol.overlay.Overlay({ +var popup = new ol.AnchoredElement({ map: map, element: document.getElementById('popup') }); @@ -49,5 +49,5 @@ map.addEventListener('click', function(evt) { 'Welcome to ol3. The location you clicked was
' + ol.Coordinate.toStringHDMS(ol.Projection.transformWithCodes( coordinate, 'EPSG:3857', 'EPSG:4326')); - popup.setCoordinate(coordinate); + popup.setPosition(coordinate); }); diff --git a/examples/side-by-side.html b/examples/side-by-side.html index ffc817397b..d113c5ea8d 100644 --- a/examples/side-by-side.html +++ b/examples/side-by-side.html @@ -45,7 +45,7 @@ Rotate: - Alt+drag, r to reset + Alt+Shift+drag, r to reset Brightness/contrast: diff --git a/examples/side-by-side.js b/examples/side-by-side.js index 1c32f475e5..017b072750 100644 --- a/examples/side-by-side.js +++ b/examples/side-by-side.js @@ -107,25 +107,41 @@ keyboardInteraction.addCallback('H', function() { layer.setHue(layer.getHue() + (Math.PI / 5)); }); keyboardInteraction.addCallback('j', function() { - var bounce = ol.animation.createBounce(2 * view.getResolution()); + var bounce = ol.animation.createBounce({ + resolution: 2 * view.getResolution() + }); domMap.addPreRenderFunction(bounce); webglMap.addPreRenderFunction(bounce); canvasMap.addPreRenderFunction(bounce); }); keyboardInteraction.addCallback('l', function() { - var panFrom = ol.animation.createPanFrom(view.getCenter()); + var panFrom = ol.animation.createPanFrom({ + source: view.getCenter(), + easing: ol.easing.elastic + }); domMap.addPreRenderFunction(panFrom); webglMap.addPreRenderFunction(panFrom); canvasMap.addPreRenderFunction(panFrom); view.setCenter(LONDON); }); keyboardInteraction.addCallback('L', function() { - var start = Date.now(); + var start = goog.now(); var duration = 5000; - var bounce = ol.animation.createBounce( - 2 * view.getResolution(), duration, start); - var panFrom = ol.animation.createPanFrom(view.getCenter(), duration, start); - var spin = ol.animation.createSpin(duration, 2, start); + var bounce = ol.animation.createBounce({ + resolution: 2 * view.getResolution(), + start: start, + duration: duration + }); + var panFrom = ol.animation.createPanFrom({ + source: view.getCenter(), + start: start, + duration: duration + }); + var spin = ol.animation.createSpin({ + turns: 2, + start: start, + duration: duration + }); var preRenderFunctions = [bounce, panFrom, spin]; domMap.addPreRenderFunctions(preRenderFunctions); webglMap.addPreRenderFunctions(preRenderFunctions); @@ -133,19 +149,34 @@ keyboardInteraction.addCallback('L', function() { view.setCenter(LONDON); }); keyboardInteraction.addCallback('m', function() { - var panFrom = ol.animation.createPanFrom(view.getCenter(), 1000); + var panFrom = ol.animation.createPanFrom({ + source: view.getCenter(), + duration: 1000, + easing: ol.easing.bounce + }); domMap.addPreRenderFunction(panFrom); webglMap.addPreRenderFunction(panFrom); canvasMap.addPreRenderFunction(panFrom); view.setCenter(MOSCOW); }); keyboardInteraction.addCallback('M', function() { - var start = Date.now(); + var start = goog.now(); var duration = 5000; - var bounce = ol.animation.createBounce( - 2 * view.getResolution(), duration, start); - var panFrom = ol.animation.createPanFrom(view.getCenter(), duration, start); - var spin = ol.animation.createSpin(duration, -2, start); + var bounce = ol.animation.createBounce({ + resolution: 2 * view.getResolution(), + start: start, + duration: duration + }); + var panFrom = ol.animation.createPanFrom({ + source: view.getCenter(), + start: start, + duration: duration + }); + var spin = ol.animation.createSpin({ + turns: -2, + start: start, + duration: duration + }); var preRenderFunctions = [bounce, panFrom, spin]; domMap.addPreRenderFunctions(preRenderFunctions); webglMap.addPreRenderFunctions(preRenderFunctions); @@ -172,13 +203,19 @@ keyboardInteraction.addCallback('vV', function() { layer.setVisible(!layer.getVisible()); }); keyboardInteraction.addCallback('x', function() { - var spin = ol.animation.createSpin(2000, 2); + var spin = ol.animation.createSpin({ + turns: 2, + duration: 2000 + }); domMap.addPreRenderFunction(spin); webglMap.addPreRenderFunction(spin); canvasMap.addPreRenderFunction(spin); }); keyboardInteraction.addCallback('X', function() { - var spin = ol.animation.createSpin(2000, -2); + var spin = ol.animation.createSpin({ + turns: -2, + duration: 2000 + }); domMap.addPreRenderFunction(spin); webglMap.addPreRenderFunction(spin); canvasMap.addPreRenderFunction(spin); diff --git a/examples/two-layers.html b/examples/two-layers.html index 4d7ac617ce..2ef70164e2 100644 --- a/examples/two-layers.html +++ b/examples/two-layers.html @@ -21,7 +21,7 @@ DOM WebGL - Canvas + Canvas export map as jpeg
diff --git a/examples/two-layers.js b/examples/two-layers.js index 4817a59a15..884f8db98f 100644 --- a/examples/two-layers.js +++ b/examples/two-layers.js @@ -48,3 +48,7 @@ var canvasMap = new ol.Map({ }); canvasMap.bindTo('layers', webglMap); canvasMap.bindTo('view', webglMap); + +goog.events.listen(goog.dom.getElement('canvas-export'), 'click', function(e) { + e.target.href = canvasMap.getRenderer().getCanvas().toDataURL('image/jpeg'); +}); diff --git a/examples/wms-custom-proj.js b/examples/wms-custom-proj.js index 8052557a0e..ae6dea5cc9 100644 --- a/examples/wms-custom-proj.js +++ b/examples/wms-custom-proj.js @@ -52,9 +52,6 @@ var layers = new ol.Collection([ ]); var map = new ol.Map({ - // By setting userProjection to the same as projection, we do not need - // proj4js because we do not need any transforms. - userProjection: epsg21781, layers: layers, renderers: ol.RendererHints.createFromQueryData(), target: 'map', diff --git a/readme.md b/readme.md index e065102702..ee031aafbc 100644 --- a/readme.md +++ b/readme.md @@ -2,13 +2,18 @@ [![Travis CI Status](https://secure.travis-ci.org/openlayers/ol3.png)](http://travis-ci.org/#!/openlayers/ol3) -## Build it -Run make: +## Hosted Examples + +The examples are hosted on GitHub (as GitHub pages). See http://openlayers.github.com/ol3/master/examples/. + +## Build OpenLayers 3 + +Run build.py: $ ./build.py -## Run the examples in debug mode +## Run examples locally Run the [Plovr](http://plovr.com/) web server with: diff --git a/src/objectliterals.exports b/src/objectliterals.exports index 171672ef67..bbd66ec223 100644 --- a/src/objectliterals.exports +++ b/src/objectliterals.exports @@ -1,3 +1,55 @@ +@exportObjectLiteral ol.MapOptions +@exportObjectLiteralProperty ol.MapOptions.controls ol.Collection|undefined +@exportObjectLiteralProperty ol.MapOptions.doubleClickZoom boolean|undefined +@exportObjectLiteralProperty ol.MapOptions.dragPan boolean|undefined +@exportObjectLiteralProperty ol.MapOptions.interactions ol.Collection|undefined +@exportObjectLiteralProperty ol.MapOptions.keyboard boolean|undefined +@exportObjectLiteralProperty ol.MapOptions.keyboardPanOffset number|undefined +@exportObjectLiteralProperty ol.MapOptions.layers ol.Collection|undefined +@exportObjectLiteralProperty ol.MapOptions.mouseWheelZoom boolean|undefined +@exportObjectLiteralProperty ol.MapOptions.mouseWheelZoomDelta number|undefined +@exportObjectLiteralProperty ol.MapOptions.renderer ol.RendererHint|undefined +@exportObjectLiteralProperty ol.MapOptions.renderers Array.|undefined +@exportObjectLiteralProperty ol.MapOptions.shiftDragZoom boolean|undefined +@exportObjectLiteralProperty ol.MapOptions.target Element|string +@exportObjectLiteralProperty ol.MapOptions.view ol.IView|undefined +@exportObjectLiteralProperty ol.MapOptions.zoomDelta number|undefined + +@exportObjectLiteral ol.View2DOptions +@exportObjectLiteralProperty ol.View2DOptions.center ol.Coordinate|undefined +@exportObjectLiteralProperty ol.View2DOptions.maxResolution number|undefined +@exportObjectLiteralProperty ol.View2DOptions.numZoomLevels number|undefined +@exportObjectLiteralProperty ol.View2DOptions.projection ol.Projection|string|undefined +@exportObjectLiteralProperty ol.View2DOptions.resolution number|undefined +@exportObjectLiteralProperty ol.View2DOptions.resolutions Array.|undefined +@exportObjectLiteralProperty ol.View2DOptions.rotation number|undefined +@exportObjectLiteralProperty ol.View2DOptions.zoom number|undefined +@exportObjectLiteralProperty ol.View2DOptions.zoomFactor number|undefined + +@exportObjectLiteral ol.animation.BounceOptions +@exportObjectLiteralProperty ol.animation.BounceOptions.resolution number +@exportObjectLiteralProperty ol.animation.BounceOptions.start number|undefined +@exportObjectLiteralProperty ol.animation.BounceOptions.duration number|undefined +@exportObjectLiteralProperty ol.animation.BounceOptions.easing function(number):number|undefined + +@exportObjectLiteral ol.animation.PanFromOptions +@exportObjectLiteralProperty ol.animation.PanFromOptions.source ol.Coordinate +@exportObjectLiteralProperty ol.animation.PanFromOptions.start number|undefined +@exportObjectLiteralProperty ol.animation.PanFromOptions.duration number|undefined +@exportObjectLiteralProperty ol.animation.PanFromOptions.easing function(number):number|undefined + +@exportObjectLiteral ol.animation.SpinOptions +@exportObjectLiteralProperty ol.animation.SpinOptions.turns number +@exportObjectLiteralProperty ol.animation.SpinOptions.start number|undefined +@exportObjectLiteralProperty ol.animation.SpinOptions.duration number|undefined +@exportObjectLiteralProperty ol.animation.SpinOptions.easing function(number):number|undefined + +@exportObjectLiteral ol.animation.ZoomFromOptions +@exportObjectLiteralProperty ol.animation.ZoomFromOptions.resolution number +@exportObjectLiteralProperty ol.animation.ZoomFromOptions.start number|undefined +@exportObjectLiteralProperty ol.animation.ZoomFromOptions.duration number|undefined +@exportObjectLiteralProperty ol.animation.ZoomFromOptions.easing function(number):number|undefined + @exportObjectLiteral ol.control.AttributionOptions @exportObjectLiteralProperty ol.control.AttributionOptions.map ol.Map|undefined @exportObjectLiteralProperty ol.control.AttributionOptions.target Element|undefined @@ -23,34 +75,22 @@ @exportObjectLiteralProperty ol.layer.LayerOptions.source ol.source.Source @exportObjectLiteralProperty ol.layer.LayerOptions.visible boolean|undefined -@exportObjectLiteral ol.MapOptions -@exportObjectLiteralProperty ol.MapOptions.controls ol.Collection|undefined -@exportObjectLiteralProperty ol.MapOptions.doubleClickZoom boolean|undefined -@exportObjectLiteralProperty ol.MapOptions.dragPan boolean|undefined -@exportObjectLiteralProperty ol.MapOptions.interactions ol.Collection|undefined -@exportObjectLiteralProperty ol.MapOptions.keyboard boolean|undefined -@exportObjectLiteralProperty ol.MapOptions.keyboardPanOffset number|undefined -@exportObjectLiteralProperty ol.MapOptions.layers ol.Collection|undefined -@exportObjectLiteralProperty ol.MapOptions.mouseWheelZoom boolean|undefined -@exportObjectLiteralProperty ol.MapOptions.mouseWheelZoomDelta number|undefined -@exportObjectLiteralProperty ol.MapOptions.renderer ol.RendererHint|undefined -@exportObjectLiteralProperty ol.MapOptions.renderers Array.|undefined -@exportObjectLiteralProperty ol.MapOptions.shiftDragZoom boolean|undefined -@exportObjectLiteralProperty ol.MapOptions.target Element|string -@exportObjectLiteralProperty ol.MapOptions.view ol.IView|undefined -@exportObjectLiteralProperty ol.MapOptions.zoomDelta number|undefined - -@exportObjectLiteral ol.overlay.OverlayOptions -@exportObjectLiteralProperty ol.overlay.OverlayOptions.coordinate ol.Coordinate|undefined -@exportObjectLiteralProperty ol.overlay.OverlayOptions.element Element|undefined -@exportObjectLiteralProperty ol.overlay.OverlayOptions.map ol.Map|undefined -@exportObjectLiteralProperty ol.overlay.OverlayOptions.positioning Array.|undefined +@exportObjectLiteral ol.AnchoredElementOptions +@exportObjectLiteralProperty ol.AnchoredElementOptions.element Element|undefined +@exportObjectLiteralProperty ol.AnchoredElementOptions.map ol.Map|undefined +@exportObjectLiteralProperty ol.AnchoredElementOptions.position ol.Coordinate|undefined +@exportObjectLiteralProperty ol.AnchoredElementOptions.positioning ol.AnchoredElementPositioning|undefined @exportObjectLiteral ol.source.BingMapsOptions @exportObjectLiteralProperty ol.source.BingMapsOptions.culture string|undefined @exportObjectLiteralProperty ol.source.BingMapsOptions.key string @exportObjectLiteralProperty ol.source.BingMapsOptions.style ol.BingMapsStyle +@exportObjectLiteral ol.source.DebugTileSourceOptions +@exportObjectLiteralProperty ol.source.DebugTileSourceOptions.extent ol.Extent|undefined +@exportObjectLiteralProperty ol.source.DebugTileSourceOptions.projection ol.Projection|undefined +@exportObjectLiteralProperty ol.source.DebugTileSourceOptions.tileGrid ol.tilegrid.TileGrid|undefined + @exportObjectLiteral ol.source.StamenOptions @exportObjectLiteralProperty ol.source.StamenOptions.flavor string|undefined @exportObjectLiteralProperty ol.source.StamenOptions.provider string @@ -76,14 +116,3 @@ @exportObjectLiteral ol.tilegrid.XYZOptions @exportObjectLiteralProperty ol.tilegrid.XYZOptions.maxZoom number - -@exportObjectLiteral ol.View2DOptions -@exportObjectLiteralProperty ol.View2DOptions.center ol.Coordinate|undefined -@exportObjectLiteralProperty ol.View2DOptions.maxResolution number|undefined -@exportObjectLiteralProperty ol.View2DOptions.numZoomLevels number|undefined -@exportObjectLiteralProperty ol.View2DOptions.projection ol.Projection|string|undefined -@exportObjectLiteralProperty ol.View2DOptions.resolution number|undefined -@exportObjectLiteralProperty ol.View2DOptions.resolutions Array.|undefined -@exportObjectLiteralProperty ol.View2DOptions.rotation number|undefined -@exportObjectLiteralProperty ol.View2DOptions.zoom number|undefined -@exportObjectLiteralProperty ol.View2DOptions.zoomFactor number|undefined diff --git a/src/ol/anchoredelement.exports b/src/ol/anchoredelement.exports new file mode 100644 index 0000000000..29fbb3bfc8 --- /dev/null +++ b/src/ol/anchoredelement.exports @@ -0,0 +1,7 @@ +@exportClass ol.AnchoredElement ol.AnchoredElementOptions + +@exportSymbol ol.AnchoredElementPositioning +@exportProperty ol.AnchoredElementPositioning.BOTTOM_LEFT +@exportProperty ol.AnchoredElementPositioning.BOTTOM_RIGHT +@exportProperty ol.AnchoredElementPositioning.TOP_LEFT +@exportProperty ol.AnchoredElementPositioning.TOP_RIGHT diff --git a/src/ol/anchoredelement.js b/src/ol/anchoredelement.js new file mode 100644 index 0000000000..14c60cd0fe --- /dev/null +++ b/src/ol/anchoredelement.js @@ -0,0 +1,318 @@ +goog.provide('ol.AnchoredElement'); +goog.provide('ol.AnchoredElementPositioning'); +goog.provide('ol.AnchoredElementProperty'); + +goog.require('goog.dom'); +goog.require('goog.events'); +goog.require('goog.style'); +goog.require('ol.Coordinate'); +goog.require('ol.Map'); +goog.require('ol.MapEventType'); +goog.require('ol.Object'); + + +/** + * @enum {string} + */ +ol.AnchoredElementProperty = { + ELEMENT: 'element', + MAP: 'map', + POSITION: 'position', + POSITIONING: 'positioning' +}; + + +/** + * @enum {string} + */ +ol.AnchoredElementPositioning = { + BOTTOM_LEFT: 'bottom-left', + BOTTOM_RIGHT: 'bottom-right', + TOP_LEFT: 'top-left', + TOP_RIGHT: 'top-right' +}; + + + +/** + * @constructor + * @extends {ol.Object} + * @param {ol.AnchoredElementOptions} anchoredElementOptions Anchored element + * options. + */ +ol.AnchoredElement = function(anchoredElementOptions) { + + goog.base(this); + + /** + * @private + * @type {Element} + */ + this.element_ = goog.dom.createElement(goog.dom.TagName.DIV); + this.element_.style.position = 'absolute'; + + /** + * @private + * @type {{bottom_: string, + * left_: string, + * right_: string, + * top_: string, + * visible: boolean}} + */ + this.rendered_ = { + bottom_: '', + left_: '', + right_: '', + top_: '', + visible: true + }; + + goog.events.listen( + this, ol.Object.getChangedEventType(ol.AnchoredElementProperty.ELEMENT), + this.handleElementChanged, false, this); + + goog.events.listen( + this, ol.Object.getChangedEventType(ol.AnchoredElementProperty.MAP), + this.handleMapChanged, false, this); + + goog.events.listen( + this, ol.Object.getChangedEventType(ol.AnchoredElementProperty.POSITION), + this.handlePositionChanged, false, this); + + goog.events.listen( + this, + ol.Object.getChangedEventType(ol.AnchoredElementProperty.POSITIONING), + this.handlePositioningChanged, false, this); + + if (goog.isDef(anchoredElementOptions.element)) { + this.setElement(anchoredElementOptions.element); + } + if (goog.isDef(anchoredElementOptions.position)) { + this.setPosition(anchoredElementOptions.position); + } + if (goog.isDef(anchoredElementOptions.positioning)) { + this.setPositioning(anchoredElementOptions.positioning); + } + if (goog.isDef(anchoredElementOptions.map)) { + this.setMap(anchoredElementOptions.map); + } + +}; +goog.inherits(ol.AnchoredElement, ol.Object); + + +/** + * @return {Element|undefined} Element. + */ +ol.AnchoredElement.prototype.getElement = function() { + return /** @type {Element|undefined} */ ( + this.get(ol.AnchoredElementProperty.ELEMENT)); +}; +goog.exportProperty( + ol.AnchoredElement.prototype, + 'getElement', + ol.AnchoredElement.prototype.getElement); + + +/** + * @return {ol.Map|undefined} Map. + */ +ol.AnchoredElement.prototype.getMap = function() { + return /** @type {ol.Map|undefined} */ ( + this.get(ol.AnchoredElementProperty.MAP)); +}; +goog.exportProperty( + ol.AnchoredElement.prototype, + 'getMap', + ol.AnchoredElement.prototype.getMap); + + +/** + * @return {ol.Coordinate|undefined} Position. + */ +ol.AnchoredElement.prototype.getPosition = function() { + return /** @type {ol.Coordinate|undefined} */ ( + this.get(ol.AnchoredElementProperty.POSITION)); +}; +goog.exportProperty( + ol.AnchoredElement.prototype, + 'getPosition', + ol.AnchoredElement.prototype.getPosition); + + +/** + * @return {ol.AnchoredElementPositioning|undefined} Positioning. + */ +ol.AnchoredElement.prototype.getPositioning = function() { + return /** @type {ol.AnchoredElementPositioning|undefined} */ ( + this.get(ol.AnchoredElementProperty.POSITIONING)); +}; +goog.exportProperty( + ol.AnchoredElement.prototype, + 'getPositioning', + ol.AnchoredElement.prototype.getPositioning); + + +/** + * @protected + */ +ol.AnchoredElement.prototype.handleElementChanged = function() { + goog.dom.removeChildren(this.element_); + var element = this.getElement(); + if (goog.isDefAndNotNull(element)) { + goog.dom.append(/** @type {!Node} */ (this.element_), element); + } +}; + + +/** + * @protected + */ +ol.AnchoredElement.prototype.handleMapChanged = function() { + if (!goog.isNull(this.mapPostrenderListenerKey_)) { + goog.dom.removeNode(this.element_); + goog.events.unlistenByKey(this.mapPostrenderListenerKey_); + this.mapPostrenderListenerKey_ = null; + } + var map = this.getMap(); + if (goog.isDefAndNotNull(map)) { + this.mapPostrenderListenerKey_ = goog.events.listen(map, + ol.MapEventType.POSTRENDER, this.handleMapPostrender, false, this); + this.updatePixelPosition_(); + goog.dom.append( + /** @type {!Node} */ (map.getOverlayContainer()), this.element_); + } +}; + + +/** + * @protected + */ +ol.AnchoredElement.prototype.handleMapPostrender = function() { + this.updatePixelPosition_(); +}; + + +/** + * @protected + */ +ol.AnchoredElement.prototype.handlePositionChanged = function() { + this.updatePixelPosition_(); +}; + + +/** + * @protected + */ +ol.AnchoredElement.prototype.handlePositioningChanged = function() { + this.updatePixelPosition_(); +}; + + +/** + * @param {Element|undefined} element Element. + */ +ol.AnchoredElement.prototype.setElement = function(element) { + this.set(ol.AnchoredElementProperty.ELEMENT, element); +}; +goog.exportProperty( + ol.AnchoredElement.prototype, + 'setElement', + ol.AnchoredElement.prototype.setElement); + + +/** + * @param {ol.Map|undefined} map Map. + */ +ol.AnchoredElement.prototype.setMap = function(map) { + this.set(ol.AnchoredElementProperty.MAP, map); +}; +goog.exportProperty( + ol.AnchoredElement.prototype, + 'setMap', + ol.AnchoredElement.prototype.setMap); + + +/** + * @param {ol.Coordinate|undefined} position Position. + */ +ol.AnchoredElement.prototype.setPosition = function(position) { + this.set(ol.AnchoredElementProperty.POSITION, position); +}; +goog.exportProperty( + ol.AnchoredElement.prototype, + 'setPosition', + ol.AnchoredElement.prototype.setPosition); + + +/** + * @param {ol.AnchoredElementPositioning|undefined} positioning Positioning. + */ +ol.AnchoredElement.prototype.setPositioning = function(positioning) { + this.set(ol.AnchoredElementProperty.POSITIONING, positioning); +}; + + +/** + * @private + */ +ol.AnchoredElement.prototype.updatePixelPosition_ = function() { + + var map = this.getMap(); + var position = this.getPosition(); + if (!goog.isDef(map) || !map.isDef() || !goog.isDef(position)) { + if (this.rendered_.visible) { + goog.style.showElement(this.element_, false); + this.rendered_.visible = false; + } + return; + } + + var pixel = map.getPixelFromCoordinate(position); + var mapSize = map.getSize(); + goog.asserts.assert(goog.isDef(mapSize)); + var style = this.element_.style; + var positioning = this.getPositioning(); + if (positioning == ol.AnchoredElementPositioning.BOTTOM_RIGHT || + positioning == ol.AnchoredElementPositioning.TOP_RIGHT) { + if (this.rendered_.left_ !== '') { + this.rendered_.left_ = style.left = ''; + } + var right = Math.round(mapSize.width - pixel.x) + 'px'; + if (this.rendered_.right_ != right) { + this.rendered_.right_ = style.right = right; + } + } else { + if (this.rendered_.right_ !== '') { + this.rendered_.right_ = style.right = ''; + } + var left = Math.round(pixel.x) + 'px'; + if (this.rendered_.left_ != left) { + this.rendered_.left_ = style.left = left; + } + } + if (positioning == ol.AnchoredElementPositioning.TOP_LEFT || + positioning == ol.AnchoredElementPositioning.TOP_RIGHT) { + if (this.rendered_.bottom_ !== '') { + this.rendered_.bottom_ = style.bottom = ''; + } + var top = Math.round(pixel.y) + 'px'; + if (this.rendered_.top_ != top) { + this.rendered_.top_ = style.top = top; + } + } else { + if (this.rendered_.top_ !== '') { + this.rendered_.top_ = style.top = ''; + } + var bottom = Math.round(mapSize.height - pixel.y) + 'px'; + if (this.rendered_.bottom_ != bottom) { + this.rendered_.bottom_ = style.bottom = bottom; + } + } + + if (!this.rendered_.visible) { + goog.style.showElement(this.element_, true); + this.rendered_.visible = true; + } + +}; diff --git a/src/ol/animation.js b/src/ol/animation.js index 4049186bb8..958817aa2e 100644 --- a/src/ol/animation.js +++ b/src/ol/animation.js @@ -9,25 +9,22 @@ goog.require('ol.easing'); /** - * @param {number} resolution Resolution. - * @param {number=} opt_duration Duration. - * @param {number=} opt_start Start. - * @param {function(number): number=} opt_easingFunction Easing function. + * @param {ol.animation.BounceOptions} options Options. * @return {ol.PreRenderFunction} Pre-render function. */ -ol.animation.createBounce = - function(resolution, opt_duration, opt_start, opt_easingFunction) { - var start = goog.isDef(opt_start) ? opt_start : Date.now(); - var duration = goog.isDef(opt_duration) ? opt_duration : 1000; - var easingFunction = goog.isDef(opt_easingFunction) ? - opt_easingFunction : ol.easing.upAndDown; +ol.animation.createBounce = function(options) { + var resolution = options.resolution; + var start = goog.isDef(options.start) ? options.start : goog.now(); + var duration = goog.isDef(options.duration) ? options.duration : 1000; + var easing = goog.isDef(options.easing) ? + options.easing : ol.easing.upAndDown; return function(map, frameState) { if (frameState.time < start) { frameState.animate = true; frameState.viewHints[ol.ViewHint.ANIMATING] += 1; return true; } else if (frameState.time < start + duration) { - var delta = easingFunction((frameState.time - start) / duration); + var delta = easing((frameState.time - start) / duration); var deltaResolution = resolution - frameState.view2DState.resolution; frameState.animate = true; frameState.view2DState.resolution += delta * deltaResolution; @@ -41,27 +38,24 @@ ol.animation.createBounce = /** - * @param {ol.Coordinate} source Source. - * @param {number=} opt_duration Duration. - * @param {number=} opt_start Start. - * @param {function(number): number=} opt_easingFunction Easing function. + * @param {ol.animation.PanFromOptions} options Options. * @return {ol.PreRenderFunction} Pre-render function. */ -ol.animation.createPanFrom = - function(source, opt_duration, opt_start, opt_easingFunction) { - var start = goog.isDef(opt_start) ? opt_start : Date.now(); +ol.animation.createPanFrom = function(options) { + var source = options.source; + var start = goog.isDef(options.start) ? options.start : goog.now(); var sourceX = source.x; var sourceY = source.y; - var duration = goog.isDef(opt_duration) ? opt_duration : 1000; - var easingFunction = goog.isDef(opt_easingFunction) ? - opt_easingFunction : goog.fx.easing.inAndOut; + var duration = goog.isDef(options.duration) ? options.duration : 1000; + var easing = goog.isDef(options.easing) ? + options.easing : goog.fx.easing.inAndOut; return function(map, frameState) { if (frameState.time < start) { frameState.animate = true; frameState.viewHints[ol.ViewHint.ANIMATING] += 1; return true; } else if (frameState.time < start + duration) { - var delta = 1 - easingFunction((frameState.time - start) / duration); + var delta = 1 - easing((frameState.time - start) / duration); var deltaX = sourceX - frameState.view2DState.center.x; var deltaY = sourceY - frameState.view2DState.center.y; frameState.animate = true; @@ -77,27 +71,23 @@ ol.animation.createPanFrom = /** - * @param {number=} opt_duration Duration. - * @param {number=} opt_turns Turns. - * @param {number=} opt_start Start. - * @param {function(number): number=} opt_easingFunction Easing function. + * @param {ol.animation.SpinOptions} options Options. * @return {ol.PreRenderFunction} Pre-render function. */ -ol.animation.createSpin = - function(opt_duration, opt_turns, opt_start, opt_easingFunction) { - var start = goog.isDef(opt_start) ? opt_start : Date.now(); - var duration = goog.isDef(opt_duration) ? opt_duration : 1000; - var turns = goog.isDef(opt_turns) ? opt_turns : 1; - var deltaTheta = 2 * turns * Math.PI; - var easingFunction = goog.isDef(opt_easingFunction) ? - opt_easingFunction : goog.fx.easing.inAndOut; +ol.animation.createSpin = function(options) { + var start = goog.isDef(options.start) ? options.start : goog.now(); + var duration = goog.isDef(options.duration) ? options.duration : 1000; + var easing = goog.isDef(options.easing) ? + options.easing : goog.fx.easing.inAndOut; + var deltaTheta = 2 * options.turns * Math.PI; + return function(map, frameState) { if (frameState.time < start) { frameState.animate = true; frameState.viewHints[ol.ViewHint.ANIMATING] += 1; return true; } else if (frameState.time < start + duration) { - var delta = easingFunction((frameState.time - start) / duration); + var delta = easing((frameState.time - start) / duration); frameState.animate = true; frameState.view2DState.rotation += delta * deltaTheta; frameState.viewHints[ol.ViewHint.ANIMATING] += 1; @@ -110,25 +100,22 @@ ol.animation.createSpin = /** - * @param {number} sourceResolution Source resolution. - * @param {number=} opt_duration Duration. - * @param {number=} opt_start Start. - * @param {function(number): number=} opt_easingFunction Easing function. + * @param {ol.animation.ZoomFromOptions} options Options. * @return {ol.PreRenderFunction} Pre-render function. */ -ol.animation.createZoomFrom = - function(sourceResolution, opt_duration, opt_start, opt_easingFunction) { - var start = goog.isDef(opt_start) ? opt_start : Date.now(); - var duration = goog.isDef(opt_duration) ? opt_duration : 1000; - var easingFunction = goog.isDef(opt_easingFunction) ? - opt_easingFunction : ol.easing.linear; +ol.animation.createZoomFrom = function(options) { + var sourceResolution = options.resolution; + var start = goog.isDef(options.start) ? options.start : goog.now(); + var duration = goog.isDef(options.duration) ? options.duration : 1000; + var easing = goog.isDef(options.easing) ? + options.easing : ol.easing.linear; return function(map, frameState) { if (frameState.time < start) { frameState.animate = true; frameState.viewHints[ol.ViewHint.ANIMATING] += 1; return true; } else if (frameState.time < start + duration) { - var delta = 1 - easingFunction((frameState.time - start) / duration); + var delta = 1 - easing((frameState.time - start) / duration); var deltaResolution = sourceResolution - frameState.view2DState.resolution; frameState.animate = true; diff --git a/src/ol/control/attributioncontrol.js b/src/ol/control/attributioncontrol.js index f7148722e8..52e33effba 100644 --- a/src/ol/control/attributioncontrol.js +++ b/src/ol/control/attributioncontrol.js @@ -69,7 +69,7 @@ ol.control.Attribution.prototype.handleMapPostrender = function(mapEvent) { if (goog.isNull(frameState)) { this.updateElement_(null); } else { - this.updateElement_(frameState.tileUsage); + this.updateElement_(frameState.usedTiles); } }; @@ -94,12 +94,12 @@ ol.control.Attribution.prototype.setMap = function(map) { /** * @private - * @param {?Object.>} tileUsage Tile - * usage. + * @param {?Object.>} usedTiles Used + * tiles. */ -ol.control.Attribution.prototype.updateElement_ = function(tileUsage) { +ol.control.Attribution.prototype.updateElement_ = function(usedTiles) { - if (goog.isNull(tileUsage)) { + if (goog.isNull(usedTiles)) { if (this.renderedVisible_) { goog.style.showElement(this.element, false); this.renderedVisible_ = false; @@ -136,14 +136,14 @@ ol.control.Attribution.prototype.updateElement_ = function(tileUsage) { var attributions = {}; var i, tileRanges, tileSource, tileSourceAttribution, tileSourceAttributionKey, tileSourceAttributions, tileSourceKey, z; - for (tileSourceKey in tileUsage) { + for (tileSourceKey in usedTiles) { goog.asserts.assert(tileSourceKey in tileSources); tileSource = tileSources[tileSourceKey]; tileSourceAttributions = tileSource.getAttributions(); if (goog.isNull(tileSourceAttributions)) { continue; } - tileRanges = tileUsage[tileSourceKey]; + tileRanges = usedTiles[tileSourceKey]; for (i = 0; i < tileSourceAttributions.length; ++i) { tileSourceAttribution = tileSourceAttributions[i]; tileSourceAttributionKey = goog.getUid(tileSourceAttribution).toString(); diff --git a/src/ol/control/mousepositioncontrol.js b/src/ol/control/mousepositioncontrol.js index ec45c3cabf..a20c25a1cd 100644 --- a/src/ol/control/mousepositioncontrol.js +++ b/src/ol/control/mousepositioncontrol.js @@ -3,6 +3,7 @@ goog.provide('ol.control.MousePosition'); +goog.require('goog.array'); goog.require('goog.dom'); goog.require('goog.events'); goog.require('goog.events.EventType'); diff --git a/src/ol/easing.js b/src/ol/easing.js index 7700c7fee6..944279cfcc 100644 --- a/src/ol/easing.js +++ b/src/ol/easing.js @@ -21,3 +21,40 @@ ol.easing.upAndDown = function(t) { return 1 - goog.fx.easing.inAndOut(2 * (t - 0.5)); } }; + + +/** + * from https://raw.github.com/DmitryBaranovskiy/raphael/master/raphael.js + * @param {number} t Input between 0 and 1. + * @return {number} Output between 0 and 1. + */ +ol.easing.elastic = function(t) { + return Math.pow(2, -10 * t) * Math.sin((t - 0.075) * (2 * Math.PI) / 0.3) + 1; +}; + + +/** + * from https://raw.github.com/DmitryBaranovskiy/raphael/master/raphael.js + * @param {number} t Input between 0 and 1. + * @return {number} Output between 0 and 1. + */ +ol.easing.bounce = function(t) { + var s = 7.5625, p = 2.75, l; + if (t < (1 / p)) { + l = s * t * t; + } else { + if (t < (2 / p)) { + t -= (1.5 / p); + l = s * t * t + 0.75; + } else { + if (t < (2.5 / p)) { + t -= (2.25 / p); + l = s * t * t + 0.9375; + } else { + t -= (2.625 / p); + l = s * t * t + 0.984375; + } + } + } + return l; +}; diff --git a/src/ol/extent.js b/src/ol/extent.js index 1cbeb217ea..3d0ab215d6 100644 --- a/src/ol/extent.js +++ b/src/ol/extent.js @@ -94,7 +94,8 @@ ol.Extent.prototype.getTopRight = function() { * @return {ol.Extent} Extent. */ ol.Extent.prototype.transform = function(transformFn) { - var min = transformFn(new ol.Coordinate(this.minX, this.minY)); - var max = transformFn(new ol.Coordinate(this.maxX, this.maxY)); - return new ol.Extent(min.x, min.y, max.x, max.y); + var a = transformFn(new ol.Coordinate(this.minX, this.minY)); + var b = transformFn(new ol.Coordinate(this.maxX, this.maxY)); + return new ol.Extent(Math.min(a.x, b.x), Math.min(a.y, b.y), + Math.max(a.x, b.x), Math.max(a.y, b.y)); }; diff --git a/src/ol/framestate.js b/src/ol/framestate.js index 574fefe20f..6f06b5fff6 100644 --- a/src/ol/framestate.js +++ b/src/ol/framestate.js @@ -1,4 +1,5 @@ // FIXME add view3DState +// FIXME factor out common code between usedTiles and wantedTiles goog.provide('ol.FrameState'); goog.provide('ol.PostRenderFunction'); @@ -26,10 +27,11 @@ goog.require('ol.layer.LayerState'); * postRenderFunctions: Array., * size: ol.Size, * tileQueue: ol.TileQueue, - * tileUsage: Object.>, * time: number, + * usedTiles: Object.>, * view2DState: ol.View2DState, - * viewHints: Array.}} + * viewHints: Array., + * wantedTiles: Object.>}} */ ol.FrameState; diff --git a/src/ol/interaction/condition.js b/src/ol/interaction/condition.js index 81a34c4f63..3644843ddc 100644 --- a/src/ol/interaction/condition.js +++ b/src/ol/interaction/condition.js @@ -20,6 +20,18 @@ ol.interaction.condition.altKeyOnly = function(browserEvent) { }; +/** + * @param {goog.events.BrowserEvent} browserEvent Browser event. + * @return {boolean} True if only the alt and shift keys are pressed. + */ +ol.interaction.condition.altShiftKeysOnly = function(browserEvent) { + return ( + browserEvent.altKey && + !browserEvent.platformModifierKey && + browserEvent.shiftKey); +}; + + /** * @param {goog.events.BrowserEvent} browserEvent Browser event. * @return {boolean} True if only the no modifier keys are pressed. diff --git a/src/ol/map.exports b/src/ol/map.exports index b8cb394c18..8c4285f1c3 100644 --- a/src/ol/map.exports +++ b/src/ol/map.exports @@ -1,4 +1,6 @@ @exportClass ol.Map ol.MapOptions +@exportProperty ol.Map.prototype.addPreRenderFunction +@exportProperty ol.Map.prototype.addPreRenderFunctions @exportProperty ol.Map.prototype.getControls @exportProperty ol.Map.prototype.getInteractions diff --git a/src/ol/map.js b/src/ol/map.js index a0400327e3..277ef4a172 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -8,7 +8,6 @@ goog.provide('ol.RendererHint'); goog.provide('ol.RendererHints'); goog.require('goog.Uri.QueryData'); -goog.require('goog.array'); goog.require('goog.async.AnimationDelay'); goog.require('goog.debug.Logger'); goog.require('goog.dispose'); @@ -175,6 +174,12 @@ ol.Map = function(mapOptions) { */ this.target_ = mapOptionsInternal.target; + /** + * @private + * @type {?number} + */ + this.viewPropertyListenerKey_ = null; + /** * @private * @type {Element} @@ -278,8 +283,16 @@ ol.Map = function(mapOptions) { */ this.tileQueue_ = new ol.TileQueue(goog.bind(this.getTilePriority, this)); + goog.events.listen(this, ol.Object.getChangedEventType(ol.MapProperty.VIEW), + this.handleViewChanged_, false, this); + goog.events.listen(this, ol.Object.getChangedEventType(ol.MapProperty.SIZE), + this.handleSizeChanged_, false, this); + goog.events.listen( + this, ol.Object.getChangedEventType(ol.MapProperty.BACKGROUND_COLOR), + this.handleBackgroundColorChanged_, false, this), this.setValues(mapOptionsInternal.values); + // this gives the map an initial size this.handleBrowserWindowResize(); this.controls_.forEach( @@ -345,6 +358,14 @@ goog.exportProperty( ol.Map.prototype.getBackgroundColor); +/** + * @return {ol.renderer.Map} Renderer. + */ +ol.Map.prototype.getRenderer = function() { + return this.renderer_; +}; + + /** * @return {Element} Container. */ @@ -391,6 +412,10 @@ ol.Map.prototype.getInteractions = function() { ol.Map.prototype.getLayers = function() { return /** @type {ol.Collection} */ (this.get(ol.MapProperty.LAYERS)); }; +goog.exportProperty( + ol.Map.prototype, + 'getLayers', + ol.Map.prototype.getLayers); /** @@ -453,19 +478,24 @@ ol.Map.prototype.getOverlayContainer = function() { /** * @param {ol.Tile} tile Tile. + * @param {string} tileSourceKey Tile source key. * @param {ol.Coordinate} tileCenter Tile center. - * @param {number} tileResolution Tile resolution. - * @return {number|undefined} Tile priority. + * @return {number} Tile priority. */ -ol.Map.prototype.getTilePriority = function(tile, tileCenter, tileResolution) { - if (goog.isNull(this.frameState_)) { - return undefined; - } else { - var center = this.frameState_.view2DState.center; - var deltaX = tileCenter.x - center.x; - var deltaY = tileCenter.y - center.y; - return Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution; +ol.Map.prototype.getTilePriority = function(tile, tileSourceKey, tileCenter) { + var frameState = this.frameState_; + if (goog.isNull(frameState) || !(tileSourceKey in frameState.wantedTiles)) { + return ol.TileQueue.DROP; } + var zKey = tile.tileCoord.z.toString(); + if (!(zKey in frameState.wantedTiles[tileSourceKey]) || + !frameState.wantedTiles[tileSourceKey][zKey].contains(tile.tileCoord)) { + return ol.TileQueue.DROP; + } + var center = frameState.view2DState.center; + var deltaX = tileCenter.x - center.x; + var deltaY = tileCenter.y - center.y; + return deltaX * deltaX + deltaY * deltaY; }; @@ -525,13 +555,20 @@ ol.Map.prototype.handleMapBrowserEvent = function(mapBrowserEvent) { ol.Map.prototype.handlePostRender = function() { this.tileQueue_.reprioritize(); // FIXME only call if needed this.tileQueue_.loadMoreTiles(); - goog.array.forEach( - this.postRenderFunctions_, - function(postRenderFunction) { - postRenderFunction(this, this.frameState_); - }, - this); - this.postRenderFunctions_.length = 0; + var postRenderFunctions = this.postRenderFunctions_; + var i; + for (i = 0; i < postRenderFunctions.length; ++i) { + postRenderFunctions[i](this, this.frameState_); + } + postRenderFunctions.length = 0; +}; + + +/** + * @private + */ +ol.Map.prototype.handleBackgroundColorChanged_ = function() { + this.render(); }; @@ -544,6 +581,40 @@ ol.Map.prototype.handleBrowserWindowResize = function() { }; +/** + * @private + */ +ol.Map.prototype.handleSizeChanged_ = function() { + this.render(); +}; + + +/** + * @private + */ +ol.Map.prototype.handleViewPropertyChanged_ = function() { + this.render(); +}; + + +/** + * @private + */ +ol.Map.prototype.handleViewChanged_ = function() { + if (!goog.isNull(this.viewPropertyListenerKey_)) { + goog.events.unlistenByKey(this.viewPropertyListenerKey_); + this.viewPropertyListenerKey_ = null; + } + var view = this.getView(); + if (goog.isDefAndNotNull(view)) { + this.viewPropertyListenerKey_ = goog.events.listen( + view, ol.ObjectEventType.CHANGED, + this.handleViewPropertyChanged_, false, this); + } + this.render(); +}; + + /** * @return {boolean} Is defined. */ @@ -611,9 +682,11 @@ ol.Map.prototype.renderFrame_ = function(time) { var backgroundColor = this.getBackgroundColor(); var viewHints = view.getHints(); var layerStates = {}; - goog.array.forEach(layersArray, function(layer) { + var layer; + for (i = 0; i < layersArray.length; ++i) { + layer = layersArray[i]; layerStates[goog.getUid(layer)] = layer.getLayerState(); - }); + } var view2DState = view2D.getView2DState(); frameState = { animate: false, @@ -627,19 +700,23 @@ ol.Map.prototype.renderFrame_ = function(time) { postRenderFunctions: [], size: size, tileQueue: this.tileQueue_, - tileUsage: {}, + time: time, + usedTiles: {}, view2DState: view2DState, viewHints: viewHints, - time: time + wantedTiles: {} }; } - this.preRenderFunctions_ = goog.array.filter( - this.preRenderFunctions_, - function(preRenderFunction) { - return preRenderFunction(this, frameState); - }, - this); + var preRenderFunctions = this.preRenderFunctions_; + var n = 0, preRenderFunction; + for (i = 0; i < preRenderFunctions.length; ++i) { + preRenderFunction = preRenderFunctions[i]; + if (preRenderFunction(this, frameState)) { + preRenderFunctions[n++] = preRenderFunction; + } + } + preRenderFunctions.length = n; if (!goog.isNull(frameState)) { // FIXME works for View2D only @@ -664,6 +741,8 @@ ol.Map.prototype.renderFrame_ = function(time) { } this.renderer_.renderFrame(frameState); + this.frameState_ = frameState; + this.dirty_ = false; if (!goog.isNull(frameState)) { if (frameState.animate) { @@ -672,8 +751,6 @@ ol.Map.prototype.renderFrame_ = function(time) { Array.prototype.push.apply( this.postRenderFunctions_, frameState.postRenderFunctions); } - this.frameState_ = frameState; - this.dirty_ = false; this.dispatchEvent( new ol.MapEvent(ol.MapEventType.POSTRENDER, this, frameState)); @@ -893,8 +970,8 @@ ol.Map.createInteractions_ = function(mapOptions) { var rotate = goog.isDef(mapOptions.rotate) ? mapOptions.rotate : true; if (rotate) { - interactions.push( - new ol.interaction.DragRotate(ol.interaction.condition.altKeyOnly)); + interactions.push(new ol.interaction.DragRotate( + ol.interaction.condition.altShiftKeysOnly)); } var doubleClickZoom = goog.isDef(mapOptions.doubleClickZoom) ? diff --git a/src/ol/mapbrowserevent.js b/src/ol/mapbrowserevent.js index c39f838928..17defe5f20 100644 --- a/src/ol/mapbrowserevent.js +++ b/src/ol/mapbrowserevent.js @@ -2,6 +2,7 @@ goog.provide('ol.MapBrowserEvent'); goog.provide('ol.MapBrowserEvent.EventType'); goog.provide('ol.MapBrowserEventHandler'); +goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.events.BrowserEvent'); goog.require('goog.events.EventTarget'); diff --git a/src/ol/overlay/overlay.exports b/src/ol/overlay/overlay.exports deleted file mode 100644 index b60b8a16a0..0000000000 --- a/src/ol/overlay/overlay.exports +++ /dev/null @@ -1,5 +0,0 @@ -@exportClass ol.overlay.Overlay ol.overlay.OverlayOptions -@exportProperty ol.overlay.Overlay.prototype.getElement -@exportProperty ol.overlay.Overlay.prototype.setCoordinate -@exportProperty ol.overlay.Overlay.prototype.setMap - diff --git a/src/ol/overlay/overlay.js b/src/ol/overlay/overlay.js deleted file mode 100644 index 1c96bb3d5c..0000000000 --- a/src/ol/overlay/overlay.js +++ /dev/null @@ -1,210 +0,0 @@ -goog.provide('ol.overlay.Overlay'); -goog.provide('ol.overlay.OverlayPositioning'); - -goog.require('goog.events'); -goog.require('goog.style'); - - - -/** - * @constructor - * @param {ol.overlay.OverlayOptions} overlayOptions Overlay options. - */ -ol.overlay.Overlay = function(overlayOptions) { - - /** - * @type {ol.Coordinate} - * @private - */ - this.coordinate_ = null; - - /** - * @type {Element} - * @private - */ - this.element_ = null; - - /** - * @private - * @type {ol.Map} - */ - this.map_ = null; - - /** - * @type {Array.} - * @private - */ - this.positioning_ = [ - ol.overlay.OverlayPositioning.LEFT, - ol.overlay.OverlayPositioning.BOTTOM - ]; - - /** - * @private - * @type {Array.} - */ - this.mapListenerKeys_ = null; - - /** - * @private - * @type {Array.} - */ - this.viewListenerKeys_ = null; - - if (goog.isDef(overlayOptions.coordinate)) { - this.setCoordinate(overlayOptions.coordinate); - } - if (goog.isDef(overlayOptions.element)) { - this.setElement(overlayOptions.element); - } - if (goog.isDef(overlayOptions.map)) { - this.setMap(overlayOptions.map); - } - if (goog.isDef(overlayOptions.positioning)) { - this.setPositioning(overlayOptions.positioning); - } -}; - - -/** - * @private - */ -ol.overlay.Overlay.prototype.handleViewChanged_ = function() { - goog.asserts.assert(!goog.isNull(this.map_)); - if (!goog.isNull(this.viewListenerKeys_)) { - goog.array.forEach(this.viewListenerKeys_, goog.events.unlistenByKey); - this.viewListenerKeys_ = null; - } - var view = this.map_.getView(); - if (goog.isDefAndNotNull(view)) { - // FIXME works for View2D only - goog.asserts.assert(view instanceof ol.View2D); - this.viewListenerKeys_ = [ - goog.events.listen( - view, ol.Object.getChangedEventType(ol.View2DProperty.CENTER), - this.updatePixelPosition_, false, this), - - goog.events.listen( - view, ol.Object.getChangedEventType(ol.View2DProperty.RESOLUTION), - this.updatePixelPosition_, false, this), - - goog.events.listen( - view, ol.Object.getChangedEventType(ol.View2DProperty.ROTATION), - this.updatePixelPosition_, false, this) - ]; - this.updatePixelPosition_(); - } -}; - - -/** - * @param {ol.Coordinate} coordinate Coordinate for the overlay's position on - * the map. - */ -ol.overlay.Overlay.prototype.setCoordinate = function(coordinate) { - this.coordinate_ = coordinate; - this.updatePixelPosition_(); -}; - - -/** - * @param {Element} element The DOM element for the overlay. - */ -ol.overlay.Overlay.prototype.setElement = function(element) { - if (this.element_) { - goog.dom.removeNode(this.element_); - } - this.element_ = element; - if (this.map_) { - goog.style.setStyle(this.element_, 'position', 'absolute'); - goog.dom.append(/** @type {!Node} */ (this.map_.getOverlayContainer()), - this.element_); - } - this.updatePixelPosition_(); -}; - - -/** - * @return {Element} The DOM element for the overlay. - */ -ol.overlay.Overlay.prototype.getElement = function() { - return this.element_; -}; - - -/** - * @param {ol.Map} map Map. - */ -ol.overlay.Overlay.prototype.setMap = function(map) { - this.map_ = map; - if (!goog.isNull(this.mapListenerKeys_)) { - goog.array.forEach(this.mapListenerKeys_, goog.events.unlistenByKey); - this.mapListenerKeys_ = null; - } - if (this.element_) { - this.setElement(this.element_); - } - if (goog.isDefAndNotNull(map)) { - this.mapListenerKeys_ = [ - goog.events.listen( - map, ol.Object.getChangedEventType(ol.MapProperty.SIZE), - this.updatePixelPosition_, false, this), - goog.events.listen( - map, ol.Object.getChangedEventType(ol.MapProperty.VIEW), - this.handleViewChanged_, false, this) - ]; - this.handleViewChanged_(); - } -}; - - -/** - * @return {ol.Map} Map. - */ -ol.overlay.Overlay.prototype.getMap = function() { - return this.map_; -}; - - -/** - * Set the CSS properties to use for x- and y-positioning of the element. If - * not set, the default is {@code ['left', 'bottom']}. - * @param {Array.} positioning The positioning. - */ -ol.overlay.Overlay.prototype.setPositioning = function(positioning) { - this.positioning_ = positioning; - this.updatePixelPosition_(); -}; - - -/** - * @private - */ -ol.overlay.Overlay.prototype.updatePixelPosition_ = function() { - if (!goog.isNull(this.map_) && !goog.isNull(this.coordinate_) && - !goog.isNull(this.element_)) { - var pixel = this.map_.getPixelFromCoordinate(this.coordinate_); - var mapSize = this.map_.get(ol.MapProperty.SIZE); - var x = Math.round(pixel.x); - if (this.positioning_[0] === ol.overlay.OverlayPositioning.RIGHT) { - x = mapSize.width - x; - } - var y = Math.round(pixel.y); - if (this.positioning_[1] === ol.overlay.OverlayPositioning.BOTTOM) { - y = mapSize.height - y; - } - goog.style.setStyle(this.element_, this.positioning_[0], x + 'px'); - goog.style.setStyle(this.element_, this.positioning_[1], y + 'px'); - } -}; - - -/** - * @enum {string} - */ -ol.overlay.OverlayPositioning = { - LEFT: 'left', - RIGHT: 'right', - TOP: 'top', - BOTTOM: 'bottom' -}; diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index 79e4478681..2697b07d1d 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -2,6 +2,7 @@ goog.provide('ol.renderer.canvas.Map'); +goog.require('goog.array'); goog.require('goog.dom'); goog.require('goog.style'); goog.require('goog.vec.Mat4'); @@ -73,44 +74,8 @@ ol.renderer.canvas.Map.prototype.createLayerRenderer = function(layer) { /** * @inheritDoc */ -ol.renderer.canvas.Map.prototype.handleBackgroundColorChanged = function() { - this.getMap().render(); -}; - - -/** - * @inheritDoc - */ -ol.renderer.canvas.Map.prototype.handleViewPropertyChanged = function() { - goog.base(this, 'handleViewPropertyChanged'); - this.getMap().render(); -}; - - -/** - * @param {goog.events.Event} event Event. - * @protected - */ -ol.renderer.canvas.Map.prototype.handleLayerRendererChange = function(event) { - this.getMap().render(); -}; - - -/** - * @inheritDoc - */ -ol.renderer.canvas.Map.prototype.handleSizeChanged = function() { - goog.base(this, 'handleSizeChanged'); - this.getMap().render(); -}; - - -/** - * @inheritDoc - */ -ol.renderer.canvas.Map.prototype.handleViewChanged = function() { - goog.base(this, 'handleViewChanged'); - this.getMap().render(); +ol.renderer.canvas.Map.prototype.getCanvas = function() { + return this.canvas_; }; diff --git a/src/ol/renderer/canvas/canvastilelayerrenderer.js b/src/ol/renderer/canvas/canvastilelayerrenderer.js index 4c17795648..de1008c364 100644 --- a/src/ol/renderer/canvas/canvastilelayerrenderer.js +++ b/src/ol/renderer/canvas/canvastilelayerrenderer.js @@ -4,6 +4,7 @@ goog.provide('ol.renderer.canvas.TileLayer'); +goog.require('goog.array'); goog.require('goog.dom'); goog.require('goog.style'); goog.require('goog.vec.Mat4'); @@ -87,6 +88,7 @@ ol.renderer.canvas.TileLayer.prototype.renderFrame = var tileLayer = this.getTileLayer(); var tileSource = tileLayer.getTileSource(); + var tileSourceKey = goog.getUid(tileSource).toString(); var tileGrid = tileSource.getTileGrid(); var tileSize = tileGrid.getTileSize(); var z = tileGrid.getZForResolution(view2DState.resolution); @@ -165,7 +167,7 @@ ol.renderer.canvas.TileLayer.prototype.renderFrame = tileState = tile.getState(); if (tileState == ol.TileState.IDLE) { tileCenter = tileGrid.getTileCoordCenter(tileCoord); - frameState.tileQueue.enqueue(tile, tileCenter, tileResolution); + frameState.tileQueue.enqueue(tile, tileSourceKey, tileCenter); } else if (tileState == ol.TileState.LOADED) { tilesToDrawByZ[z][tileCoord.toString()] = tile; continue; @@ -213,9 +215,11 @@ ol.renderer.canvas.TileLayer.prototype.renderFrame = if (!allTilesLoaded) { frameState.animate = true; + this.updateWantedTiles(frameState.wantedTiles, tileSource, z, tileRange); } - this.updateTileUsage(frameState.tileUsage, tileSource, z, tileRange); + this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); + this.scheduleExpireCache(frameState, tileSource); var transform = this.transform_; goog.vec.Mat4.makeIdentity(transform); diff --git a/src/ol/renderer/dom/dommaprenderer.js b/src/ol/renderer/dom/dommaprenderer.js index 79853a8d6c..6f9724a746 100644 --- a/src/ol/renderer/dom/dommaprenderer.js +++ b/src/ol/renderer/dom/dommaprenderer.js @@ -1,5 +1,6 @@ goog.provide('ol.renderer.dom.Map'); +goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); diff --git a/src/ol/renderer/dom/domtilelayerrenderer.js b/src/ol/renderer/dom/domtilelayerrenderer.js index 58a55295f7..27b2b6a804 100644 --- a/src/ol/renderer/dom/domtilelayerrenderer.js +++ b/src/ol/renderer/dom/domtilelayerrenderer.js @@ -83,6 +83,7 @@ ol.renderer.dom.TileLayer.prototype.renderFrame = var tileLayer = this.getTileLayer(); var tileSource = tileLayer.getTileSource(); + var tileSourceKey = goog.getUid(tileSource).toString(); var tileGrid = tileSource.getTileGrid(); var z = tileGrid.getZForResolution(view2DState.resolution); var tileResolution = tileGrid.getResolution(z); @@ -132,7 +133,7 @@ ol.renderer.dom.TileLayer.prototype.renderFrame = tileState = tile.getState(); if (tileState == ol.TileState.IDLE) { tileCenter = tileGrid.getTileCoordCenter(tileCoord); - frameState.tileQueue.enqueue(tile, tileCenter, tileResolution); + frameState.tileQueue.enqueue(tile, tileSourceKey, tileCenter); } else if (tileState == ol.TileState.LOADED) { tilesToDrawByZ[z][tileCoord.toString()] = tile; continue; @@ -234,9 +235,11 @@ ol.renderer.dom.TileLayer.prototype.renderFrame = if (!allTilesLoaded) { frameState.animate = true; + this.updateWantedTiles(frameState.wantedTiles, tileSource, z, tileRange); } - this.updateTileUsage(frameState.tileUsage, tileSource, z, tileRange); + this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); + this.scheduleExpireCache(frameState, tileSource); }; diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index b2003467ab..b3243519e9 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -2,10 +2,12 @@ goog.provide('ol.renderer.Layer'); goog.require('goog.events'); goog.require('goog.events.EventType'); +goog.require('ol.FrameState'); goog.require('ol.Object'); goog.require('ol.TileRange'); goog.require('ol.layer.Layer'); goog.require('ol.layer.LayerProperty'); +goog.require('ol.source.TileSource'); @@ -130,24 +132,66 @@ ol.renderer.Layer.prototype.handleLayerVisibleChange = goog.nullFunction; /** * @protected - * @param {Object.>} tileUsage Tile usage. + * @param {ol.FrameState} frameState Frame state. + * @param {ol.source.TileSource} tileSource Tile source. + */ +ol.renderer.Layer.prototype.scheduleExpireCache = + function(frameState, tileSource) { + if (tileSource.canExpireCache()) { + frameState.postRenderFunctions.push( + goog.partial(function(tileSource, map, frameState) { + var tileSourceKey = goog.getUid(tileSource).toString(); + tileSource.expireCache(frameState.usedTiles[tileSourceKey]); + }, tileSource)); + } +}; + + +/** + * @protected + * @param {Object.>} usedTiles Used tiles. * @param {ol.source.Source} source Source. * @param {number} z Z. * @param {ol.TileRange} tileRange Tile range. */ -ol.renderer.Layer.prototype.updateTileUsage = - function(tileUsage, source, z, tileRange) { +ol.renderer.Layer.prototype.updateUsedTiles = + function(usedTiles, source, z, tileRange) { // FIXME should we use tilesToDrawByZ instead? var sourceKey = goog.getUid(source).toString(); var zKey = z.toString(); - if (sourceKey in tileUsage) { - if (z in tileUsage[sourceKey]) { - tileUsage[sourceKey][zKey].extend(tileRange); + if (sourceKey in usedTiles) { + if (zKey in usedTiles[sourceKey]) { + usedTiles[sourceKey][zKey].extend(tileRange); } else { - tileUsage[sourceKey][zKey] = tileRange; + usedTiles[sourceKey][zKey] = tileRange; } } else { - tileUsage[sourceKey] = {}; - tileUsage[sourceKey][zKey] = tileRange; + usedTiles[sourceKey] = {}; + usedTiles[sourceKey][zKey] = tileRange; + } +}; + + +/** + * @protected + * @param {Object.>} wantedTiles Wanted + * tile ranges. + * @param {ol.source.Source} source Source. + * @param {number} z Z. + * @param {ol.TileRange} tileRange Tile range. + */ +ol.renderer.Layer.prototype.updateWantedTiles = + function(wantedTiles, source, z, tileRange) { + var sourceKey = goog.getUid(source).toString(); + var zKey = z.toString(); + if (sourceKey in wantedTiles) { + if (zKey in wantedTiles[sourceKey]) { + wantedTiles[sourceKey][zKey].extend(tileRange); + } else { + wantedTiles[sourceKey][zKey] = tileRange; + } + } else { + wantedTiles[sourceKey] = {}; + wantedTiles[sourceKey][zKey] = tileRange; } }; diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index 14b3db6d9f..01313a98b3 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -1,11 +1,10 @@ goog.provide('ol.renderer.Map'); goog.require('goog.Disposable'); +goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.events'); goog.require('goog.functions'); -goog.require('goog.fx.anim'); -goog.require('goog.fx.anim.Animated'); goog.require('goog.vec.Mat4'); goog.require('ol.FrameState'); goog.require('ol.View2D'); @@ -41,40 +40,24 @@ ol.renderer.Map = function(container, map) { */ this.layerRenderers = {}; - /** - * @private - * @type {Array.} - */ - this.layersListenerKeys_ = null; - + // + // We listen to layer add/remove to add/remove layer renderers. + // /** * @private * @type {?number} */ - this.viewPropertyListenerKey_ = null; + this.mapLayersChangedListenerKey_ = + goog.events.listen( + map, ol.Object.getChangedEventType(ol.MapProperty.LAYERS), + this.handleLayersChanged, false, this); /** * @private * @type {Array.} */ - this.mapListenerKeys_ = [ - goog.events.listen( - map, ol.Object.getChangedEventType(ol.MapProperty.BACKGROUND_COLOR), - this.handleBackgroundColorChanged, false, this), - - goog.events.listen( - map, ol.Object.getChangedEventType(ol.MapProperty.LAYERS), - this.handleLayersChanged, false, this), - - goog.events.listen( - map, ol.Object.getChangedEventType(ol.MapProperty.SIZE), - this.handleSizeChanged, false, this), - - goog.events.listen( - map, ol.Object.getChangedEventType(ol.MapProperty.VIEW), - this.handleViewChanged, false, this) - ]; + this.layersListenerKeys_ = null; }; goog.inherits(ol.renderer.Map, goog.Disposable); @@ -137,10 +120,7 @@ ol.renderer.Map.prototype.disposeInternal = function() { goog.object.forEach(this.layerRenderers, function(layerRenderer) { goog.dispose(layerRenderer); }); - goog.array.forEach(this.mapListenerKeys_, goog.events.unlistenByKey); - if (!goog.isNull(this.viewPropertyListenerKey_)) { - goog.events.unlistenByKey(this.viewPropertyListenerKey_); - } + goog.events.unlistenByKey(this.mapLayersChangedListenerKey_); if (!goog.isNull(this.layersListenerKeys_)) { goog.array.forEach(this.layersListenerKeys_, goog.events.unlistenByKey); } @@ -148,6 +128,12 @@ ol.renderer.Map.prototype.disposeInternal = function() { }; +/** + * @return {Element} Canvas. + */ +ol.renderer.Map.prototype.getCanvas = goog.functions.NULL; + + /** * @param {ol.layer.Layer} layer Layer. * @protected @@ -169,12 +155,6 @@ ol.renderer.Map.prototype.getMap = function() { }; -/** - * Handle background color changed. - */ -ol.renderer.Map.prototype.handleBackgroundColorChanged = goog.nullFunction; - - /** * @param {ol.CollectionEvent} collectionEvent Collection event. * @protected @@ -218,40 +198,6 @@ ol.renderer.Map.prototype.handleLayersRemove = function(collectionEvent) { }; -/** - * @protected - */ -ol.renderer.Map.prototype.handleViewPropertyChanged = function() { - this.getMap().render(); -}; - - -/** - * @protected - */ -ol.renderer.Map.prototype.handleSizeChanged = function() { - this.getMap().render(); -}; - - -/** - * @protected - */ -ol.renderer.Map.prototype.handleViewChanged = function() { - if (!goog.isNull(this.viewPropertyListenerKey_)) { - goog.events.unlistenByKey(this.viewPropertyListenerKey_); - this.viewPropertyListenerKey_ = null; - } - var view = this.getMap().getView(); - if (goog.isDefAndNotNull(view)) { - this.viewPropertyListenerKey_ = goog.events.listen( - view, ol.ObjectEventType.CHANGED, - this.handleViewPropertyChanged, false, this); - } - this.getMap().render(); -}; - - /** * @param {ol.layer.Layer} layer Layer. * @protected diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index 6e2e6c3990..6a62129e67 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -1,10 +1,9 @@ -// FIXME clear textureCache -// FIXME generational tile texture garbage collector newFrame/get // FIXME check against gl.getParameter(webgl.MAX_TEXTURE_SIZE) goog.provide('ol.renderer.webgl.Map'); goog.provide('ol.renderer.webgl.map.shader'); +goog.require('goog.array'); goog.require('goog.debug.Logger'); goog.require('goog.dispose'); goog.require('goog.dom'); @@ -17,13 +16,21 @@ goog.require('goog.webgl'); goog.require('ol.Tile'); goog.require('ol.layer.Layer'); goog.require('ol.layer.TileLayer'); +goog.require('ol.renderer.Map'); goog.require('ol.renderer.webgl.FragmentShader'); goog.require('ol.renderer.webgl.TileLayer'); goog.require('ol.renderer.webgl.VertexShader'); +goog.require('ol.structs.LinkedMap'); goog.require('ol.webgl'); goog.require('ol.webgl.WebGLContextEventType'); +/** + * @define {number} Texture cache high water mark. + */ +ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK = 1024; + + /** * @typedef {{magFilter: number, minFilter: number, texture: WebGLTexture}} */ @@ -177,9 +184,15 @@ ol.renderer.webgl.Map = function(container, map) { /** * @private - * @type {Object.} + * @type {ol.structs.LinkedMap} */ - this.textureCache_ = {}; + this.textureCache_ = new ol.structs.LinkedMap(undefined, true); + + /** + * @private + * @type {number} + */ + this.textureCacheFrameMarkerCount_ = 0; /** * @private @@ -225,8 +238,8 @@ ol.renderer.webgl.Map.prototype.bindTileTexture = function(tile, magFilter, minFilter) { var gl = this.getGL(); var tileKey = tile.getKey(); - var textureCacheEntry = this.textureCache_[tileKey]; - if (goog.isDef(textureCacheEntry)) { + if (this.textureCache_.containsKey(tileKey)) { + var textureCacheEntry = this.textureCache_.get(tileKey); gl.bindTexture(goog.webgl.TEXTURE_2D, textureCacheEntry.texture); if (textureCacheEntry.magFilter != magFilter) { gl.texParameteri( @@ -251,11 +264,11 @@ ol.renderer.webgl.Map.prototype.bindTileTexture = goog.webgl.CLAMP_TO_EDGE); gl.texParameteri(goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_T, goog.webgl.CLAMP_TO_EDGE); - this.textureCache_[tileKey] = { + this.textureCache_.set(tileKey, { texture: texture, magFilter: magFilter, minFilter: minFilter - }; + }); } }; @@ -285,14 +298,50 @@ ol.renderer.webgl.Map.prototype.disposeInternal = function() { goog.object.forEach(this.shaderCache_, function(shader) { gl.deleteShader(shader); }); - goog.object.forEach(this.textureCache_, function(textureCacheEntry) { - gl.deleteTexture(textureCacheEntry.texture); + this.textureCache_.forEach(function(textureCacheEntry) { + if (!goog.isNull(textureCacheEntry)) { + gl.deleteTexture(textureCacheEntry.texture); + } }); } goog.base(this, 'disposeInternal'); }; +/** + * @param {ol.Map} map Map. + * @param {ol.FrameState} frameState Frame state. + * @private + */ +ol.renderer.webgl.Map.prototype.expireCache_ = function(map, frameState) { + var gl = this.getGL(); + var key, textureCacheEntry; + while (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ > + ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) { + textureCacheEntry = /** @type {?ol.renderer.webgl.TextureCacheEntry} */ + (this.textureCache_.peekLast()); + if (goog.isNull(textureCacheEntry)) { + if (+this.textureCache_.peekLastKey() == frameState.time) { + break; + } else { + --this.textureCacheFrameMarkerCount_; + } + } else { + gl.deleteTexture(textureCacheEntry.texture); + } + this.textureCache_.pop(); + } +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.Map.prototype.getCanvas = function() { + return this.canvas_; +}; + + /** * @return {WebGLRenderingContext} GL. */ @@ -360,14 +409,6 @@ ol.renderer.webgl.Map.prototype.getShader = function(shaderObject) { }; -/** - * @inheritDoc - */ -ol.renderer.webgl.Map.prototype.handleBackgroundColorChanged = function() { - this.getMap().render(); -}; - - /** * @param {goog.events.Event} event Event. * @protected @@ -390,7 +431,8 @@ ol.renderer.webgl.Map.prototype.handleWebGLContextLost = function(event) { this.arrayBuffer_ = null; this.shaderCache_ = {}; this.programCache_ = {}; - this.textureCache_ = {}; + this.textureCache_.clear(); + this.textureCacheFrameMarkerCount_ = 0; goog.object.forEach(this.layerRenderers, function(layerRenderer) { layerRenderer.handleWebGLContextLost(); }); @@ -427,7 +469,7 @@ ol.renderer.webgl.Map.prototype.initializeGL_ = function() { * @return {boolean} Is tile texture loaded. */ ol.renderer.webgl.Map.prototype.isTileTextureLoaded = function(tile) { - return tile.getKey() in this.textureCache_; + return this.textureCache_.containsKey(tile.getKey()); }; @@ -471,6 +513,9 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) { return false; } + this.textureCache_.set(frameState.time.toString(), null); + ++this.textureCacheFrameMarkerCount_; + goog.array.forEach(frameState.layersArray, function(layer) { var layerState = frameState.layerStates[goog.getUid(layer)]; if (!layerState.visible || !layerState.ready) { @@ -553,6 +598,11 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) { this.calculateMatrices2D(frameState); + if (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ > + ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) { + frameState.postRenderFunctions.push(goog.bind(this.expireCache_, this)); + } + }; diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index de1f18e1e2..fd4e0842e2 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -270,6 +270,7 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame = var tileLayer = this.getTileLayer(); var tileSource = tileLayer.getTileSource(); + var tileSourceKey = goog.getUid(tileSource).toString(); var tileGrid = tileSource.getTileGrid(); var z = tileGrid.getZForResolution(view2DState.resolution); var tileResolution = tileGrid.getResolution(z); @@ -392,7 +393,7 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame = tileState = tile.getState(); if (tileState == ol.TileState.IDLE) { tileCenter = tileGrid.getTileCoordCenter(tileCoord); - frameState.tileQueue.enqueue(tile, tileCenter, tileResolution); + frameState.tileQueue.enqueue(tile, tileSourceKey, tileCenter); } else if (tileState == ol.TileState.LOADED) { if (mapRenderer.isTileTextureLoaded(tile)) { tilesToDrawByZ[z][tileCoord.toString()] = tile; @@ -455,11 +456,13 @@ ol.renderer.webgl.TileLayer.prototype.renderFrame = this.renderedTileRange_ = null; this.renderedFramebufferExtent_ = null; frameState.animate = true; + this.updateWantedTiles(frameState.wantedTiles, tileSource, z, tileRange); } } - this.updateTileUsage(frameState.tileUsage, tileSource, z, tileRange); + this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange); + this.scheduleExpireCache(frameState, tileSource); goog.vec.Mat4.makeIdentity(this.matrix_); goog.vec.Mat4.translate(this.matrix_, diff --git a/src/ol/source/bingmapssource.js b/src/ol/source/bingmapssource.js index a700ba09d2..ed89f2363c 100644 --- a/src/ol/source/bingmapssource.js +++ b/src/ol/source/bingmapssource.js @@ -2,6 +2,7 @@ goog.provide('ol.BingMapsStyle'); goog.provide('ol.source.BingMaps'); goog.require('goog.Uri'); +goog.require('goog.array'); goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('goog.net.Jsonp'); diff --git a/src/ol/source/debugtilesource.exports b/src/ol/source/debugtilesource.exports new file mode 100644 index 0000000000..2c74b974f3 --- /dev/null +++ b/src/ol/source/debugtilesource.exports @@ -0,0 +1 @@ +@exportClass ol.source.DebugTileSource ol.source.DebugTileSourceOptions diff --git a/src/ol/source/debugtilesource.js b/src/ol/source/debugtilesource.js index cfe149bb20..1cb3e059e7 100644 --- a/src/ol/source/debugtilesource.js +++ b/src/ol/source/debugtilesource.js @@ -1,8 +1,8 @@ goog.provide('ol.source.DebugTileSource'); -goog.provide('ol.source.DebugTileSourceOptions'); goog.require('ol.Size'); goog.require('ol.Tile'); +goog.require('ol.TileCache'); goog.require('ol.TileCoord'); goog.require('ol.source.TileSource'); goog.require('ol.tilegrid.TileGrid'); @@ -79,14 +79,6 @@ ol.DebugTile_.prototype.getImage = function(opt_context) { }; -/** - * @typedef {{extent: (ol.Extent|undefined), - * projection: (ol.Projection|undefined), - * tileGrid: (ol.tilegrid.TileGrid|undefined)}} - */ -ol.source.DebugTileSourceOptions; - - /** * @constructor @@ -103,26 +95,40 @@ ol.source.DebugTileSource = function(options) { /** * @private - * @type {Object.} - * FIXME will need to expire elements from this cache - * FIXME see elemoine's work with goog.structs.LinkedMap + * @type {ol.TileCache} */ - this.tileCache_ = {}; + this.tileCache_ = new ol.TileCache(); }; goog.inherits(ol.source.DebugTileSource, ol.source.TileSource); +/** + * @inheritDoc + */ +ol.source.DebugTileSource.prototype.canExpireCache = function() { + return this.tileCache_.canExpireCache(); +}; + + +/** + * @inheritDoc + */ +ol.source.DebugTileSource.prototype.expireCache = function(usedTiles) { + this.tileCache_.expireCache(usedTiles); +}; + + /** * @inheritDoc */ ol.source.DebugTileSource.prototype.getTile = function(tileCoord) { var key = tileCoord.toString(); - if (goog.object.containsKey(this.tileCache_, key)) { - return this.tileCache_[key]; + if (this.tileCache_.containsKey(key)) { + return /** @type {ol.DebugTile_} */ (this.tileCache_.get(key)); } else { var tile = new ol.DebugTile_(tileCoord, this.tileGrid); - this.tileCache_[key] = tile; + this.tileCache_.set(key, tile); return tile; } }; diff --git a/src/ol/source/imagetilesource.js b/src/ol/source/imagetilesource.js index 5baea4539f..b025985f00 100644 --- a/src/ol/source/imagetilesource.js +++ b/src/ol/source/imagetilesource.js @@ -5,6 +5,7 @@ goog.require('ol.Attribution'); goog.require('ol.Extent'); goog.require('ol.ImageTile'); goog.require('ol.Projection'); +goog.require('ol.TileCache'); goog.require('ol.TileCoord'); goog.require('ol.TileUrlFunction'); goog.require('ol.TileUrlFunctionType'); @@ -55,32 +56,46 @@ ol.source.ImageTileSource = function(options) { /** * @private - * @type {Object.} - * FIXME will need to expire elements from this cache - * FIXME see elemoine's work with goog.structs.LinkedMap + * @type {ol.TileCache} */ - this.tileCache_ = {}; + this.tileCache_ = new ol.TileCache(); }; goog.inherits(ol.source.ImageTileSource, ol.source.TileSource); +/** + * @inheritDoc + */ +ol.source.ImageTileSource.prototype.canExpireCache = function() { + return this.tileCache_.canExpireCache(); +}; + + +/** + * @inheritDoc + */ +ol.source.ImageTileSource.prototype.expireCache = function(usedTiles) { + this.tileCache_.expireCache(usedTiles); +}; + + /** * @inheritDoc */ ol.source.ImageTileSource.prototype.getTile = function(tileCoord) { var key = tileCoord.toString(); - if (goog.object.containsKey(this.tileCache_, key)) { - return this.tileCache_[key]; + if (this.tileCache_.containsKey(key)) { + return /** @type {ol.Tile} */ (this.tileCache_.get(key)); } else { var tileUrl = this.getTileCoordUrl(tileCoord); var tile; if (goog.isDef(tileUrl)) { tile = new ol.ImageTile(tileCoord, tileUrl, this.crossOrigin_); + this.tileCache_.set(key, tile); } else { tile = null; } - this.tileCache_[key] = tile; return tile; } }; diff --git a/src/ol/source/source.js b/src/ol/source/source.js index f2a95444fa..727c732789 100644 --- a/src/ol/source/source.js +++ b/src/ol/source/source.js @@ -10,7 +10,7 @@ goog.require('ol.TileUrlFunction'); /** - * @typedef {{attribtions: (Array.|undefined), + * @typedef {{attributions: (Array.|undefined), * extent: (ol.Extent|undefined), * projection: (ol.Projection|undefined)}} */ diff --git a/src/ol/source/tiledwmssource.js b/src/ol/source/tiledwmssource.js index 40b0c699ff..41c5024327 100644 --- a/src/ol/source/tiledwmssource.js +++ b/src/ol/source/tiledwmssource.js @@ -3,6 +3,7 @@ goog.provide('ol.source.TiledWMS'); +goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.object'); goog.require('goog.uri.utils'); diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index 6b82b8f3b8..cff0556921 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -1,6 +1,7 @@ goog.provide('ol.source.TileSource'); goog.provide('ol.source.TileSourceOptions'); +goog.require('goog.functions'); goog.require('ol.Attribution'); goog.require('ol.Extent'); goog.require('ol.Projection'); @@ -46,6 +47,18 @@ ol.source.TileSource = function(tileSourceOptions) { goog.inherits(ol.source.TileSource, ol.source.Source); +/** + * @return {boolean} Can expire cache. + */ +ol.source.TileSource.prototype.canExpireCache = goog.functions.FALSE; + + +/** + * @param {Object.} usedTiles Used tiles. + */ +ol.source.TileSource.prototype.expireCache = goog.abstractMethod; + + /** * @inheritDoc */ diff --git a/src/ol/structs/linkedmap.js b/src/ol/structs/linkedmap.js new file mode 100644 index 0000000000..63971748c5 --- /dev/null +++ b/src/ol/structs/linkedmap.js @@ -0,0 +1,486 @@ +// Copyright 2007 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// FIXME replace this with goog.structs.LinkedMap when goog.structs.LinkedMap +// adds peekLastKey() + +/** + * @fileoverview A LinkedMap data structure that is accessed using key/value + * pairs like an ordinary Map, but which guarantees a consistent iteration + * order over its entries. The iteration order is either insertion order (the + * default) or ordered from most recent to least recent use. By setting a fixed + * size, the LRU version of the LinkedMap makes an effective object cache. This + * data structure is similar to Java's LinkedHashMap. + * + * @author brenneman@google.com (Shawn Brenneman) + */ + + +goog.provide('ol.structs.LinkedMap'); + +goog.require('goog.structs.Map'); + + + +/** + * Class for a LinkedMap datastructure, which combines O(1) map access for + * key/value pairs with a linked list for a consistent iteration order. Sample + * usage: + * + *
+ * var m = new LinkedMap();
+ * m.set('param1', 'A');
+ * m.set('param2', 'B');
+ * m.set('param3', 'C');
+ * alert(m.getKeys()); // param1, param2, param3
+ *
+ * var c = new LinkedMap(5, true);
+ * for (var i = 0; i < 10; i++) {
+ *   c.set('entry' + i, false);
+ * }
+ * alert(c.getKeys()); // entry9, entry8, entry7, entry6, entry5
+ *
+ * c.set('entry5', true);
+ * c.set('entry1', false);
+ * alert(c.getKeys()); // entry1, entry5, entry9, entry8, entry7
+ * 
+ * + * @param {number=} opt_maxCount The maximum number of objects to store in the + * LinkedMap. If unspecified or 0, there is no maximum. + * @param {boolean=} opt_cache When set, the LinkedMap stores items in order + * from most recently used to least recently used, instead of insertion + * order. + * @constructor + */ +ol.structs.LinkedMap = function(opt_maxCount, opt_cache) { + /** + * The maximum number of entries to allow, or null if there is no limit. + * @type {?number} + * @private + */ + this.maxCount_ = opt_maxCount || null; + + /** + * @type {boolean} + * @private + */ + this.cache_ = !!opt_cache; + + this.map_ = new goog.structs.Map(); + + this.head_ = new ol.structs.LinkedMap.Node_('', undefined); + this.head_.next = this.head_.prev = this.head_; +}; + + +/** + * Finds a node and updates it to be the most recently used. + * @param {string} key The key of the node. + * @return {ol.structs.LinkedMap.Node_} The node or null if not found. + * @private + */ +ol.structs.LinkedMap.prototype.findAndMoveToTop_ = function(key) { + var node = /** @type {ol.structs.LinkedMap.Node_} */ (this.map_.get(key)); + if (node) { + if (this.cache_) { + node.remove(); + this.insert_(node); + } + } + return node; +}; + + +/** + * Retrieves the value for a given key. If this is a caching LinkedMap, the + * entry will become the most recently used. + * @param {string} key The key to retrieve the value for. + * @param {*=} opt_val A default value that will be returned if the key is + * not found, defaults to undefined. + * @return {*} The retrieved value. + */ +ol.structs.LinkedMap.prototype.get = function(key, opt_val) { + var node = this.findAndMoveToTop_(key); + return node ? node.value : opt_val; +}; + + +/** + * Retrieves the value for a given key without updating the entry to be the + * most recently used. + * @param {string} key The key to retrieve the value for. + * @param {*=} opt_val A default value that will be returned if the key is + * not found. + * @return {*} The retrieved value. + */ +ol.structs.LinkedMap.prototype.peekValue = function(key, opt_val) { + var node = this.map_.get(key); + return node ? node.value : opt_val; +}; + + +/** + * Sets a value for a given key. If this is a caching LinkedMap, this entry + * will become the most recently used. + * @param {string} key The key to retrieve the value for. + * @param {*} value A default value that will be returned if the key is + * not found. + */ +ol.structs.LinkedMap.prototype.set = function(key, value) { + var node = this.findAndMoveToTop_(key); + if (node) { + node.value = value; + } else { + node = new ol.structs.LinkedMap.Node_(key, value); + this.map_.set(key, node); + this.insert_(node); + } +}; + + +/** + * Returns the value of the first node without making any modifications. + * @return {*} The value of the first node or undefined if the map is empty. + */ +ol.structs.LinkedMap.prototype.peek = function() { + return this.head_.next.value; +}; + + +/** + * Returns the value of the last node without making any modifications. + * @return {*} The value of the last node or undefined if the map is empty. + */ +ol.structs.LinkedMap.prototype.peekLast = function() { + return this.head_.prev.value; +}; + + +/** + * Returns the key of the last node without making any modifications. + * @return {string|undefined} The key of the last node or undefined if the map + * is empty. + */ +ol.structs.LinkedMap.prototype.peekLastKey = function() { + return this.head_.prev.key; +}; + + +/** + * Removes the first node from the list and returns its value. + * @return {*} The value of the popped node, or undefined if the map was empty. + */ +ol.structs.LinkedMap.prototype.shift = function() { + return this.popNode_(this.head_.next); +}; + + +/** + * Removes the last node from the list and returns its value. + * @return {*} The value of the popped node, or undefined if the map was empty. + */ +ol.structs.LinkedMap.prototype.pop = function() { + return this.popNode_(this.head_.prev); +}; + + +/** + * Removes a value from the LinkedMap based on its key. + * @param {string} key The key to remove. + * @return {boolean} True if the entry was removed, false if the key was not + * found. + */ +ol.structs.LinkedMap.prototype.remove = function(key) { + var node = /** @type {ol.structs.LinkedMap.Node_} */ (this.map_.get(key)); + if (node) { + this.removeNode(node); + return true; + } + return false; +}; + + +/** + * Removes a node from the {@code LinkedMap}. It can be overridden to do + * further cleanup such as disposing of the node value. + * @param {!ol.structs.LinkedMap.Node_} node The node to remove. + * @protected + */ +ol.structs.LinkedMap.prototype.removeNode = function(node) { + node.remove(); + this.map_.remove(node.key); +}; + + +/** + * @return {number} The number of items currently in the LinkedMap. + */ +ol.structs.LinkedMap.prototype.getCount = function() { + return this.map_.getCount(); +}; + + +/** + * @return {boolean} True if the cache is empty, false if it contains any items. + */ +ol.structs.LinkedMap.prototype.isEmpty = function() { + return this.map_.isEmpty(); +}; + + +/** + * Sets the maximum number of entries allowed in this object, truncating any + * excess objects if necessary. + * @param {number} maxCount The new maximum number of entries to allow. + */ +ol.structs.LinkedMap.prototype.setMaxCount = function(maxCount) { + this.maxCount_ = maxCount || null; + if (this.maxCount_ != null) { + this.truncate_(this.maxCount_); + } +}; + + +/** + * @return {!Array.} The list of the keys in the appropriate order for + * this LinkedMap. + */ +ol.structs.LinkedMap.prototype.getKeys = function() { + return this.map(function(val, key) { + return key; + }); +}; + + +/** + * @return {!Array} The list of the values in the appropriate order for + * this LinkedMap. + */ +ol.structs.LinkedMap.prototype.getValues = function() { + return this.map(function(val, key) { + return val; + }); +}; + + +/** + * Tests whether a provided value is currently in the LinkedMap. This does not + * affect item ordering in cache-style LinkedMaps. + * @param {Object} value The value to check for. + * @return {boolean} Whether the value is in the LinkedMap. + */ +ol.structs.LinkedMap.prototype.contains = function(value) { + return this.some(function(el) { + return el == value; + }); +}; + + +/** + * Tests whether a provided key is currently in the LinkedMap. This does not + * affect item ordering in cache-style LinkedMaps. + * @param {string} key The key to check for. + * @return {boolean} Whether the key is in the LinkedMap. + */ +ol.structs.LinkedMap.prototype.containsKey = function(key) { + return this.map_.containsKey(key); +}; + + +/** + * Removes all entries in this object. + */ +ol.structs.LinkedMap.prototype.clear = function() { + this.truncate_(0); +}; + + +/** + * Calls a function on each item in the LinkedMap. + * + * @see goog.structs.forEach + * @param {Function} f The function to call for each item. The function takes + * three arguments: the value, the key, and the LinkedMap. + * @param {Object=} opt_obj The object context to use as "this" for the + * function. + */ +ol.structs.LinkedMap.prototype.forEach = function(f, opt_obj) { + for (var n = this.head_.next; n != this.head_; n = n.next) { + f.call(opt_obj, n.value, n.key, this); + } +}; + + +/** + * Calls a function on each item in the LinkedMap and returns the results of + * those calls in an array. + * + * @see goog.structs.map + * @param {!Function} f The function to call for each item. The function takes + * three arguments: the value, the key, and the LinkedMap. + * @param {Object=} opt_obj The object context to use as "this" for the + * function. + * @return {!Array} The results of the function calls for each item in the + * LinkedMap. + */ +ol.structs.LinkedMap.prototype.map = function(f, opt_obj) { + var rv = []; + for (var n = this.head_.next; n != this.head_; n = n.next) { + rv.push(f.call(opt_obj, n.value, n.key, this)); + } + return rv; +}; + + +/** + * Calls a function on each item in the LinkedMap and returns true if any of + * those function calls returns a true-like value. + * + * @see goog.structs.some + * @param {Function} f The function to call for each item. The function takes + * three arguments: the value, the key, and the LinkedMap, and returns a + * boolean. + * @param {Object=} opt_obj The object context to use as "this" for the + * function. + * @return {boolean} Whether f evaluates to true for at least one item in the + * LinkedMap. + */ +ol.structs.LinkedMap.prototype.some = function(f, opt_obj) { + for (var n = this.head_.next; n != this.head_; n = n.next) { + if (f.call(opt_obj, n.value, n.key, this)) { + return true; + } + } + return false; +}; + + +/** + * Calls a function on each item in the LinkedMap and returns true only if every + * function call returns a true-like value. + * + * @see goog.structs.some + * @param {Function} f The function to call for each item. The function takes + * three arguments: the value, the key, and the Cache, and returns a + * boolean. + * @param {Object=} opt_obj The object context to use as "this" for the + * function. + * @return {boolean} Whether f evaluates to true for every item in the Cache. + */ +ol.structs.LinkedMap.prototype.every = function(f, opt_obj) { + for (var n = this.head_.next; n != this.head_; n = n.next) { + if (!f.call(opt_obj, n.value, n.key, this)) { + return false; + } + } + return true; +}; + + +/** + * Appends a node to the list. LinkedMap in cache mode adds new nodes to + * the head of the list, otherwise they are appended to the tail. If there is a + * maximum size, the list will be truncated if necessary. + * + * @param {ol.structs.LinkedMap.Node_} node The item to insert. + * @private + */ +ol.structs.LinkedMap.prototype.insert_ = function(node) { + if (this.cache_) { + node.next = this.head_.next; + node.prev = this.head_; + + this.head_.next = node; + node.next.prev = node; + } else { + node.prev = this.head_.prev; + node.next = this.head_; + + this.head_.prev = node; + node.prev.next = node; + } + + if (this.maxCount_ != null) { + this.truncate_(this.maxCount_); + } +}; + + +/** + * Removes elements from the LinkedMap if the given count has been exceeded. + * In cache mode removes nodes from the tail of the list. Otherwise removes + * nodes from the head. + * @param {number} count Number of elements to keep. + * @private + */ +ol.structs.LinkedMap.prototype.truncate_ = function(count) { + for (var i = this.map_.getCount(); i > count; i--) { + this.removeNode(this.cache_ ? this.head_.prev : this.head_.next); + } +}; + + +/** + * Removes the node from the LinkedMap if it is not the head, and returns + * the node's value. + * @param {!ol.structs.LinkedMap.Node_} node The item to remove. + * @return {*} The value of the popped node. + * @private + */ +ol.structs.LinkedMap.prototype.popNode_ = function(node) { + if (this.head_ != node) { + this.removeNode(node); + } + return node.value; +}; + + + +/** + * Internal class for a doubly-linked list node containing a key/value pair. + * @param {string} key The key. + * @param {*} value The value. + * @constructor + * @private + */ +ol.structs.LinkedMap.Node_ = function(key, value) { + this.key = key; + this.value = value; +}; + + +/** + * The next node in the list. + * @type {!ol.structs.LinkedMap.Node_} + */ +ol.structs.LinkedMap.Node_.prototype.next; + + +/** + * The previous node in the list. + * @type {!ol.structs.LinkedMap.Node_} + */ +ol.structs.LinkedMap.Node_.prototype.prev; + + +/** + * Causes this node to remove itself from the list. + */ +ol.structs.LinkedMap.Node_.prototype.remove = function() { + this.prev.next = this.next; + this.next.prev = this.prev; + + delete this.prev; + delete this.next; +}; diff --git a/src/ol/tilecache.js b/src/ol/tilecache.js new file mode 100644 index 0000000000..9645fb9513 --- /dev/null +++ b/src/ol/tilecache.js @@ -0,0 +1,58 @@ +goog.provide('ol.TileCache'); + +goog.require('goog.dispose'); +goog.require('ol.Tile'); +goog.require('ol.TileRange'); +goog.require('ol.structs.LinkedMap'); + + +/** + * @define {number} Default high water mark. + */ +ol.DEFAULT_TILE_CACHE_HIGH_WATER_MARK = 512; + + + +/** + * @constructor + * @extends {ol.structs.LinkedMap} + * @param {number=} opt_highWaterMark High water mark. + */ +ol.TileCache = function(opt_highWaterMark) { + + goog.base(this, undefined, true); + + /** + * @private + * @type {number} + */ + this.highWaterMark_ = goog.isDef(opt_highWaterMark) ? + opt_highWaterMark : ol.DEFAULT_TILE_CACHE_HIGH_WATER_MARK; + +}; +goog.inherits(ol.TileCache, ol.structs.LinkedMap); + + +/** + * @return {boolean} Can expire cache. + */ +ol.TileCache.prototype.canExpireCache = function() { + return this.getCount() > this.highWaterMark_; +}; + + +/** + * @param {Object.} usedTiles Used tiles. + */ +ol.TileCache.prototype.expireCache = function(usedTiles) { + var tile, zKey; + while (this.canExpireCache()) { + tile = /** @type {ol.Tile} */ (this.peekLast()); + zKey = tile.tileCoord.z.toString(); + if (zKey in usedTiles && usedTiles[zKey].contains(tile.tileCoord)) { + break; + } else { + this.pop(); + } + } +}; diff --git a/src/ol/tilegrid/xyztilegrid.js b/src/ol/tilegrid/xyztilegrid.js index b7b16faf60..c948010ba7 100644 --- a/src/ol/tilegrid/xyztilegrid.js +++ b/src/ol/tilegrid/xyztilegrid.js @@ -48,8 +48,8 @@ ol.tilegrid.XYZ.prototype.forEachTileCoordParentTileRange = if (z < 0) { break; } - x = Math.floor(x / 2); - y = Math.floor(y / 2); + x >>= 1; + y >>= 1; tileRange = new ol.TileRange(x, y, x, y); if (callback.call(opt_obj, z, tileRange)) { break; diff --git a/src/ol/tilequeue.js b/src/ol/tilequeue.js index d73f7c0901..64984ba1a8 100644 --- a/src/ol/tilequeue.js +++ b/src/ol/tilequeue.js @@ -3,14 +3,24 @@ goog.provide('ol.TileQueue'); goog.require('goog.events'); goog.require('goog.events.EventType'); -goog.require('goog.structs.PriorityQueue'); goog.require('ol.Coordinate'); goog.require('ol.Tile'); goog.require('ol.TileState'); /** - * @typedef {function(ol.Tile, ol.Coordinate, number): (number|undefined)} + * Tile Queue. + * + * The implementation is inspired from the Closure Library's Heap + * class and Python's heapq module. + * + * http://closure-library.googlecode.com/svn/docs/closure_goog_structs_heap.js.source.html + * http://hg.python.org/cpython/file/2.7/Lib/heapq.py + */ + + +/** + * @typedef {function(ol.Tile, string, ol.Coordinate): number} */ ol.TilePriorityFunction; @@ -43,9 +53,9 @@ ol.TileQueue = function(tilePriorityFunction) { /** * @private - * @type {goog.structs.PriorityQueue} + * @type {Array.>} */ - this.queue_ = new goog.structs.PriorityQueue(); + this.heap_ = []; /** * @private @@ -57,23 +67,49 @@ ol.TileQueue = function(tilePriorityFunction) { /** - * @param {ol.Tile} tile Tile. - * @param {ol.Coordinate} tileCenter Tile center. - * @param {number} tileResolution Tile resolution. + * @const {number} */ -ol.TileQueue.prototype.enqueue = - function(tile, tileCenter, tileResolution) { +ol.TileQueue.DROP = Infinity; + + +/** + * Remove and return the highest-priority tile. O(logn). + * @private + * @return {ol.Tile} Tile. + */ +ol.TileQueue.prototype.dequeue_ = function() { + var heap = this.heap_; + goog.asserts.assert(heap.length > 0); + var tile = /** @type {ol.Tile} */ (heap[0][1]); + if (heap.length == 1) { + heap.length = 0; + } else { + heap[0] = heap.pop(); + this.siftUp_(0); + } + var tileKey = tile.getKey(); + delete this.queuedTileKeys_[tileKey]; + return tile; +}; + + +/** + * Enqueue a tile. O(logn). + * @param {ol.Tile} tile Tile. + * @param {string} tileSourceKey Tile source key. + * @param {ol.Coordinate} tileCenter Tile center. + */ +ol.TileQueue.prototype.enqueue = function(tile, tileSourceKey, tileCenter) { if (tile.getState() != ol.TileState.IDLE) { return; } var tileKey = tile.getKey(); if (!(tileKey in this.queuedTileKeys_)) { - var priority = this.tilePriorityFunction_(tile, tileCenter, tileResolution); - if (goog.isDef(priority)) { - this.queue_.enqueue(priority, arguments); + var priority = this.tilePriorityFunction_(tile, tileSourceKey, tileCenter); + if (priority != ol.TileQueue.DROP) { + this.heap_.push([priority, tile, tileSourceKey, tileCenter]); this.queuedTileKeys_[tileKey] = true; - } else { - // FIXME fire drop event? + this.siftDown_(0, this.heap_.length - 1); } } }; @@ -87,15 +123,57 @@ ol.TileQueue.prototype.handleTileChange = function() { }; +/** + * Gets the index of the left child of the node at the given index. + * @param {number} index The index of the node to get the left child for. + * @return {number} The index of the left child. + * @private + */ +ol.TileQueue.prototype.getLeftChildIndex_ = function(index) { + return index * 2 + 1; +}; + + +/** + * Gets the index of the right child of the node at the given index. + * @param {number} index The index of the node to get the right child for. + * @return {number} The index of the right child. + * @private + */ +ol.TileQueue.prototype.getRightChildIndex_ = function(index) { + return index * 2 + 2; +}; + + +/** + * Gets the index of the parent of the node at the given index. + * @param {number} index The index of the node to get the parent for. + * @return {number} The index of the parent. + * @private + */ +ol.TileQueue.prototype.getParentIndex_ = function(index) { + return (index - 1) >> 1; +}; + + +/** + * Make _heap a heap. O(n). + * @private + */ +ol.TileQueue.prototype.heapify_ = function() { + for (var i = (this.heap_.length >> 1) - 1; i >= 0; i--) { + this.siftUp_(i); + } +}; + + /** * FIXME empty description for jsdoc */ ol.TileQueue.prototype.loadMoreTiles = function() { - var tile, tileKey; - while (!this.queue_.isEmpty() && this.tilesLoading_ < this.maxTilesLoading_) { - tile = (/** @type {Array} */ (this.queue_.dequeue()))[0]; - tileKey = tile.getKey(); - delete this.queuedTileKeys_[tileKey]; + var tile; + while (this.heap_.length > 0 && this.tilesLoading_ < this.maxTilesLoading_) { + tile = /** @type {ol.Tile} */ (this.dequeue_()); goog.events.listen(tile, goog.events.EventType.CHANGE, this.handleTileChange, false, this); tile.load(); @@ -104,17 +182,75 @@ ol.TileQueue.prototype.loadMoreTiles = function() { }; +/** + * @param {number} index The index of the node to move down. + * @private + */ +ol.TileQueue.prototype.siftUp_ = function(index) { + var heap = this.heap_; + var count = heap.length; + var node = heap[index]; + var startIndex = index; + + while (index < (count >> 1)) { + var lIndex = this.getLeftChildIndex_(index); + var rIndex = this.getRightChildIndex_(index); + + var smallerChildIndex = rIndex < count && + heap[rIndex][0] < heap[lIndex][0] ? + rIndex : lIndex; + + heap[index] = heap[smallerChildIndex]; + index = smallerChildIndex; + } + + heap[index] = node; + this.siftDown_(startIndex, index); +}; + + +/** + * @param {number} startIndex The index of the root. + * @param {number} index The index of the node to move up. + * @private + */ +ol.TileQueue.prototype.siftDown_ = function(startIndex, index) { + var heap = this.heap_; + var node = heap[index]; + + while (index > startIndex) { + var parentIndex = this.getParentIndex_(index); + if (heap[parentIndex][0] > node[0]) { + heap[index] = heap[parentIndex]; + index = parentIndex; + } else { + break; + } + } + heap[index] = node; +}; + + /** * FIXME empty description for jsdoc */ ol.TileQueue.prototype.reprioritize = function() { - if (!this.queue_.isEmpty()) { - var values = /** @type {Array.} */ (this.queue_.getValues()); - this.queue_.clear(); - this.queuedTileKeys_ = {}; - var i; - for (i = 0; i < values.length; ++i) { - this.enqueue.apply(this, values[i]); + var heap = this.heap_; + var i, n = 0, node, priority, tile, tileCenter, tileKey, tileSourceKey; + for (i = 0; i < heap.length; ++i) { + node = heap[i]; + tile = /** @type {ol.Tile} */ (node[1]); + tileSourceKey = /** @type {string} */ (node[2]); + tileCenter = /** @type {ol.Coordinate} */ (node[3]); + priority = this.tilePriorityFunction_(tile, tileSourceKey, tileCenter); + if (priority == ol.TileQueue.DROP) { + tileKey = tile.getKey(); + delete this.queuedTileKeys_[tileKey]; + } else { + node[0] = priority; + heap[n++] = node; } } + heap.length = n; + this.heapify_(); }; diff --git a/src/ol/tileurlfunction.js b/src/ol/tileurlfunction.js index ce63f6c4cb..ce7168fb24 100644 --- a/src/ol/tileurlfunction.js +++ b/src/ol/tileurlfunction.js @@ -1,6 +1,7 @@ goog.provide('ol.TileUrlFunction'); goog.provide('ol.TileUrlFunctionType'); +goog.require('goog.array'); goog.require('goog.math'); goog.require('goog.uri.utils'); goog.require('ol.TileCoord'); diff --git a/src/ol/view.js b/src/ol/view.js index 27e0c9db1f..f69302fc98 100644 --- a/src/ol/view.js +++ b/src/ol/view.js @@ -5,6 +5,7 @@ goog.require('goog.array'); goog.require('ol.IView'); goog.require('ol.IView2D'); goog.require('ol.IView3D'); +goog.require('ol.Object'); /** diff --git a/src/ol/view2d.js b/src/ol/view2d.js index 0ea5977c43..ad4af94932 100644 --- a/src/ol/view2d.js +++ b/src/ol/view2d.js @@ -296,8 +296,10 @@ ol.View2D.prototype.zoom = function(map, delta, opt_anchor, opt_duration) { var currentResolution = this.getResolution(); if (goog.isDef(currentResolution) && goog.isDef(opt_duration)) { map.requestRenderFrame(); - map.addPreRenderFunction(ol.animation.createZoomFrom( - currentResolution, opt_duration)); + map.addPreRenderFunction(ol.animation.createZoomFrom({ + resolution: currentResolution, + duration: opt_duration + })); } var resolution = this.constraints_.resolution(currentResolution, delta); this.zoom_(map, resolution, opt_anchor); diff --git a/test/ol.html b/test/ol.html index b21a91519d..059554959e 100644 --- a/test/ol.html +++ b/test/ol.html @@ -93,6 +93,7 @@ + diff --git a/test/spec/ol/extent.test.js b/test/spec/ol/extent.test.js index 8763aaf0d3..d1312fad3c 100644 --- a/test/spec/ol/extent.test.js +++ b/test/spec/ol/extent.test.js @@ -55,6 +55,21 @@ describe('ol.Extent', function() { expect(destinationExtent.maxX).toRoughlyEqual(5009377.085697311, 1e-9); expect(destinationExtent.maxY).toRoughlyEqual(8399737.889818361, 1e-9); }); + + it('takes arbitrary function', function() { + var transformFn = function(coordinate) { + return new ol.Coordinate(-coordinate.x, -coordinate.y); + }; + var sourceExtent = new ol.Extent(-15, -30, 45, 60); + var destinationExtent = sourceExtent.transform(transformFn); + expect(destinationExtent).not.toBeUndefined(); + expect(destinationExtent).not.toBeNull(); + expect(destinationExtent.minX).toBe(-45); + expect(destinationExtent.minY).toBe(-60); + expect(destinationExtent.maxX).toBe(15); + expect(destinationExtent.maxY).toBe(30); + }); + }); }); diff --git a/test/spec/ol/tilequeue.test.js b/test/spec/ol/tilequeue.test.js new file mode 100644 index 0000000000..37b8d609e0 --- /dev/null +++ b/test/spec/ol/tilequeue.test.js @@ -0,0 +1,68 @@ +describe('ol.TileQueue', function() { + + // is the tile queue's array a heap? + function isHeap(tq) { + var heap = tq.heap_; + var i; + var key; + var leftKey; + var rightKey; + for (i = 0; i < (heap.length >> 1) - 1; i++) { + key = heap[i][0]; + leftKey = heap[tq.getLeftChildIndex_(i)][0]; + rightKey = heap[tq.getRightChildIndex_(i)][0]; + if (leftKey < key || rightKey < key) { + return false; + } + } + return true; + } + + function addRandomPriorityTiles(tq, num) { + var i, tile, priority; + for (i = 0; i < num; i++) { + tile = new ol.Tile(); + priority = Math.floor(Math.random() * 100); + tq.heap_.push([priority, tile, '', new ol.Coordinate(0, 0)]); + tq.queuedTileKeys_[tile.getKey()] = true; + } + } + + describe('heapify', function() { + it('does convert an arbitrary array into a heap', function() { + + var tq = new ol.TileQueue(function() {}); + addRandomPriorityTiles(tq, 100); + + tq.heapify_(); + expect(isHeap(tq)).toBeTruthy(); + }); + }); + + describe('reprioritize', function() { + it('does reprioritize the array', function() { + + var tq = new ol.TileQueue(function() {}); + addRandomPriorityTiles(tq, 100); + + tq.heapify_(); + + // now reprioritize, changing the priority of 50 tiles and removing the + // rest + + var i = 0; + tq.tilePriorityFunction_ = function() { + if ((i++) % 2 === 0) { + return ol.TileQueue.DROP; + } + return Math.floor(Math.random() * 100); + }; + + tq.reprioritize(); + expect(tq.heap_.length).toEqual(50); + expect(isHeap(tq)).toBeTruthy(); + + }); + }); +}); +