From a90a012e5d538554702ed1275f19a860ade7c712 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Apr 2015 14:08:16 +0200 Subject: [PATCH 01/11] Replay vector batches for each world --- src/ol/proj/proj.js | 3 ++ src/ol/renderer/canvas/canvaslayerrenderer.js | 8 +++- .../canvas/canvasvectorlayerrenderer.js | 43 ++++++++++++++++++- src/ol/source/vectorsource.js | 14 ++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/ol/proj/proj.js b/src/ol/proj/proj.js index 6ec9c030f8..348432e087 100644 --- a/src/ol/proj/proj.js +++ b/src/ol/proj/proj.js @@ -120,6 +120,9 @@ ol.proj.Projection = function(options) { * @type {boolean} */ this.global_ = goog.isDef(options.global) ? options.global : false; + if (this.global_) { + goog.asserts.assert(!goog.isNull(this.extent_)); + } /** * @private diff --git a/src/ol/renderer/canvas/canvaslayerrenderer.js b/src/ol/renderer/canvas/canvaslayerrenderer.js index 137485307f..3b5e8b09ff 100644 --- a/src/ol/renderer/canvas/canvaslayerrenderer.js +++ b/src/ol/renderer/canvas/canvaslayerrenderer.js @@ -192,19 +192,23 @@ ol.renderer.canvas.Layer.prototype.getImageTransform = goog.abstractMethod; /** * @param {olx.FrameState} frameState Frame state. + * @param {number=} opt_offsetX Offset on the x-axis in view coordinates. * @protected * @return {!goog.vec.Mat4.Number} Transform. */ -ol.renderer.canvas.Layer.prototype.getTransform = function(frameState) { +ol.renderer.canvas.Layer.prototype.getTransform = + function(frameState, opt_offsetX) { var viewState = frameState.viewState; var pixelRatio = frameState.pixelRatio; + var offsetX = goog.isDef(opt_offsetX) ? opt_offsetX : 0; return ol.vec.Mat4.makeTransform2D(this.transform_, pixelRatio * frameState.size[0] / 2, pixelRatio * frameState.size[1] / 2, pixelRatio / viewState.resolution, -pixelRatio / viewState.resolution, -viewState.rotation, - -viewState.center[0], -viewState.center[1]); + -viewState.center[0] + offsetX, + -viewState.center[1]); }; diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 6b913462da..dab1413b83 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -11,6 +11,7 @@ goog.require('ol.render.EventType'); goog.require('ol.render.canvas.ReplayGroup'); goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.vector'); +goog.require('ol.source.Vector'); @@ -75,6 +76,16 @@ goog.inherits(ol.renderer.canvas.VectorLayer, ol.renderer.canvas.Layer); ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, layerState, context) { + var extent = frameState.extent; + var pixelRatio = frameState.pixelRatio; + var skippedFeatureUids = frameState.skippedFeatureUids; + var viewState = frameState.viewState; + var projection = viewState.projection; + var rotation = viewState.rotation; + var projectionExtent = projection.getExtent(); + var vectorSource = this.getLayer().getSource(); + goog.asserts.assertInstanceof(vectorSource, ol.source.Vector); + var transform = this.getTransform(frameState); this.dispatchPreComposeEvent(context, frameState, transform); @@ -97,8 +108,28 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = var alpha = replayContext.globalAlpha; replayContext.globalAlpha = layerState.opacity; replayGroup.replay( - replayContext, frameState.pixelRatio, transform, - frameState.viewState.rotation, frameState.skippedFeatureUids); + replayContext, pixelRatio, transform, rotation, skippedFeatureUids); + + if (vectorSource.getWrapX() && projection.isGlobal() && + !ol.extent.containsExtent(projectionExtent, frameState.extent)) { + var startX = extent[0]; + var worldWidth = ol.extent.getWidth(projectionExtent); + var world = 0; + while (startX < projectionExtent[0]) { + transform = this.getTransform(frameState, worldWidth * (--world)); + replayGroup.replay( + replayContext, pixelRatio, transform, rotation, skippedFeatureUids); + startX += worldWidth; + } + world = 0; + startX = extent[2]; + while (startX > projectionExtent[2]) { + transform = this.getTransform(frameState, worldWidth * (++world)); + replayGroup.replay( + replayContext, pixelRatio, transform, rotation, skippedFeatureUids); + startX -= worldWidth; + } + } if (replayContext != context) { this.dispatchRenderEvent(replayContext, frameState, transform); @@ -194,6 +225,14 @@ ol.renderer.canvas.VectorLayer.prototype.prepareFrame = var extent = ol.extent.buffer(frameStateExtent, vectorLayerRenderBuffer * resolution); + var projectionExtent = viewState.projection.getExtent(); + + if (vectorSource.getWrapX() && viewState.projection.isGlobal() && + !ol.extent.containsExtent(projectionExtent, frameState.extent)) { + // do not clip when the view crosses the 0 or 180 meridians + extent[0] = projectionExtent[0]; + extent[2] = projectionExtent[2]; + } if (!this.dirty_ && this.renderedResolution_ == resolution && diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index 53b3319f64..eadeb180e8 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -146,6 +146,12 @@ ol.source.Vector = function(opt_options) { this.addFeaturesInternal(options.features); } + /** + * @type {boolean} + * @private + */ + this.wrapX_ = goog.isDef(options.wrapX) ? options.wrapX : true; + }; goog.inherits(ol.source.Vector, ol.source.Source); @@ -564,6 +570,14 @@ ol.source.Vector.prototype.getFeatureById = function(id) { }; +/** + * @return {boolean} + */ +ol.source.Vector.prototype.getWrapX = function() { + return this.wrapX_; +}; + + /** * @param {goog.events.Event} event Event. * @private From 45cc660c48e43cca754ece322851245bab6c3477 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Apr 2015 14:09:23 +0200 Subject: [PATCH 02/11] Use x of the real world in forEachFeatureAtCoordinate --- src/ol/renderer/maprenderer.js | 56 +++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index 64e32edc75..295b3fc174 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -9,6 +9,7 @@ goog.require('goog.events.EventType'); goog.require('goog.functions'); goog.require('goog.object'); goog.require('goog.vec.Mat4'); +goog.require('ol.extent'); goog.require('ol.layer.Layer'); goog.require('ol.renderer.Layer'); goog.require('ol.style.IconImageCache'); @@ -136,23 +137,44 @@ ol.renderer.Map.prototype.forEachFeatureAtCoordinate = var viewState = frameState.viewState; var viewResolution = viewState.resolution; var viewRotation = viewState.rotation; + + /** @type {Object.} */ + var features = {}; + + /** + * @param {ol.Feature} feature Feature. + * @return {?} Callback result. + */ + function forEachFeatureAtCoordinate(feature) { + goog.asserts.assert(goog.isDef(feature), 'received a feature'); + var key = goog.getUid(feature).toString(); + if (!(key in features)) { + features[key] = true; + return callback.call(thisArg, feature, null); + } + } + + var projection = viewState.projection; + + var translatedX; + if (projection.isGlobal()) { + var projectionExtent = projection.getExtent(); + var worldWidth = ol.extent.getWidth(projectionExtent); + var x = coordinate[0]; + if (x < projectionExtent[0] || x > projectionExtent[2]) { + var worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth); + translatedX = x + worldWidth * worldsAway; + } + } + if (!goog.isNull(this.replayGroup)) { - /** @type {Object.} */ - var features = {}; result = this.replayGroup.forEachFeatureAtCoordinate(coordinate, - viewResolution, viewRotation, {}, - /** - * @param {ol.Feature} feature Feature. - * @return {?} Callback result. - */ - function(feature) { - goog.asserts.assert(goog.isDef(feature), 'received a feature'); - var key = goog.getUid(feature).toString(); - if (!(key in features)) { - features[key] = true; - return callback.call(thisArg, feature, null); - } - }); + viewResolution, viewRotation, {}, forEachFeatureAtCoordinate); + if (!result && goog.isDef(translatedX)) { + result = this.replayGroup.forEachFeatureAtCoordinate( + [translatedX, coordinate[1]], + viewResolution, viewRotation, {}, forEachFeatureAtCoordinate); + } if (result) { return result; } @@ -168,6 +190,10 @@ ol.renderer.Map.prototype.forEachFeatureAtCoordinate = var layerRenderer = this.getLayerRenderer(layer); result = layerRenderer.forEachFeatureAtCoordinate( coordinate, frameState, callback, thisArg); + if (!result && goog.isDef(translatedX)) { + result = layerRenderer.forEachFeatureAtCoordinate( + [translatedX, coordinate[1]], frameState, callback, thisArg); + } if (result) { return result; } From b51aaa5055517a9b8fa8428d32ee480af8a937b4 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Apr 2015 14:43:01 +0200 Subject: [PATCH 03/11] Wrap the x-axis for the map's replay group --- src/ol/renderer/canvas/canvasmaprenderer.js | 72 +++++++++++++++------ 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index 86b1c06512..fd100e65ab 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -10,6 +10,7 @@ goog.require('ol'); goog.require('ol.RendererType'); goog.require('ol.css'); goog.require('ol.dom'); +goog.require('ol.extent'); goog.require('ol.layer.Image'); goog.require('ol.layer.Layer'); goog.require('ol.layer.Tile'); @@ -92,39 +93,60 @@ ol.renderer.canvas.Map.prototype.createLayerRenderer = function(layer) { /** * @param {ol.render.EventType} type Event type. * @param {olx.FrameState} frameState Frame state. + * @param {boolean=} opt_wrapX Wrap the x-axis. * @private */ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = - function(type, frameState) { + function(type, frameState, opt_wrapX) { var map = this.getMap(); var context = this.context_; if (map.hasListener(type)) { var extent = frameState.extent; var pixelRatio = frameState.pixelRatio; var viewState = frameState.viewState; + var projection = viewState.projection; + var projectionExtent = projection.getExtent(); var resolution = viewState.resolution; var rotation = viewState.rotation; + var repeatReplay = (opt_wrapX && projection.isGlobal() && + !ol.extent.containsExtent(projectionExtent, extent)); - ol.vec.Mat4.makeTransform2D(this.transform_, - this.canvas_.width / 2, this.canvas_.height / 2, - pixelRatio / resolution, -pixelRatio / resolution, - -rotation, - -viewState.center[0], -viewState.center[1]); + var transform = this.getTransform(frameState); var tolerance = ol.renderer.vector.getTolerance(resolution, pixelRatio); - var replayGroup = new ol.render.canvas.ReplayGroup(tolerance, extent, + var replayGroup = new ol.render.canvas.ReplayGroup(tolerance, + repeatReplay ? + [projectionExtent[0], extent[1], projectionExtent[2], extent[3]] : + extent, resolution); var vectorContext = new ol.render.canvas.Immediate(context, pixelRatio, - extent, this.transform_, rotation); + extent, transform, rotation); var composeEvent = new ol.render.Event(type, map, vectorContext, replayGroup, frameState, context, null); map.dispatchEvent(composeEvent); replayGroup.finish(); if (!replayGroup.isEmpty()) { - replayGroup.replay(context, pixelRatio, this.transform_, - rotation, {}); + replayGroup.replay(context, pixelRatio, transform, rotation, {}); + + if (repeatReplay) { + var startX = extent[0]; + var worldWidth = ol.extent.getWidth(projectionExtent); + var world = 0; + while (startX < projectionExtent[0]) { + transform = this.getTransform(frameState, worldWidth * (--world)); + replayGroup.replay(context, pixelRatio, transform, rotation, {}); + startX += worldWidth; + } + world = 0; + startX = extent[2]; + while (startX > projectionExtent[2]) { + transform = this.getTransform(frameState, worldWidth * (++world)); + replayGroup.replay(context, pixelRatio, transform, rotation, {}); + startX -= worldWidth; + } + } } vectorContext.flush(); this.replayGroup = replayGroup; @@ -133,14 +155,23 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = /** - * @param {ol.layer.Layer} layer Layer. - * @return {ol.renderer.canvas.Layer} Canvas layer renderer. + * @param {olx.FrameState} frameState Frame state. + * @param {number=} opt_offsetX Offset on the x-axis in view coordinates. + * @protected + * @return {!goog.vec.Mat4.Number} Transform. */ -ol.renderer.canvas.Map.prototype.getCanvasLayerRenderer = function(layer) { - var layerRenderer = this.getLayerRenderer(layer); - goog.asserts.assertInstanceof(layerRenderer, ol.renderer.canvas.Layer, - 'layerRenderer is an instance of ol.renderer.canvas.Layer'); - return /** @type {ol.renderer.canvas.Layer} */ (layerRenderer); +ol.renderer.canvas.Map.prototype.getTransform = + function(frameState, opt_offsetX) { + var pixelRatio = frameState.pixelRatio; + var viewState = frameState.viewState; + var resolution = viewState.resolution; + var offsetX = goog.isDef(opt_offsetX) ? opt_offsetX : 0; + return ol.vec.Mat4.makeTransform2D(this.transform_, + this.canvas_.width / 2, this.canvas_.height / 2, + pixelRatio / resolution, -pixelRatio / resolution, + -viewState.rotation, + -viewState.center[0] + offsetX, + -viewState.center[1]); }; @@ -181,6 +212,7 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { var layerStatesArray = frameState.layerStatesArray; var viewResolution = frameState.viewState.resolution; + var wrapX = false; var i, ii, layer, layerRenderer, layerState; for (i = 0, ii = layerStatesArray.length; i < ii; ++i) { layerState = layerStatesArray[i]; @@ -193,11 +225,15 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { continue; } if (layerRenderer.prepareFrame(frameState, layerState)) { + if (layer instanceof ol.layer.Vector && layer.getSource().getWrapX()) { + wrapX = true; + } layerRenderer.composeFrame(frameState, layerState, context); } } - this.dispatchComposeEvent_(ol.render.EventType.POSTCOMPOSE, frameState); + this.dispatchComposeEvent_( + ol.render.EventType.POSTCOMPOSE, frameState, wrapX); if (!this.renderedVisible_) { goog.style.setElementShown(this.canvas_, true); From d5e5628fe1f81657ddad3db495b5fdbc98e247e5 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Apr 2015 19:25:04 +0200 Subject: [PATCH 04/11] Add some tests --- .../renderer/canvas/canvasmaprenderer.test.js | 71 +++++++++++++++++++ .../canvas/canvasvectorlayerrenderer.test.js | 10 +-- 2 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 test/spec/ol/renderer/canvas/canvasmaprenderer.test.js diff --git a/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js b/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js new file mode 100644 index 0000000000..c4894f3f7a --- /dev/null +++ b/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js @@ -0,0 +1,71 @@ +goog.provide('ol.test.renderer.canvas.Map'); + +describe('ol.renderer.canvas.Map', function() { + + describe('constructor', function() { + + it('creates a new instance', function() { + var map = new ol.Map({ + target: document.createElement('div') + }); + var renderer = new ol.renderer.canvas.Map(map.viewport_, map); + expect(renderer).to.be.a(ol.renderer.canvas.Map); + }); + + }); + + describe('#renderFrame()', function() { + var layer, map, renderer; + + beforeEach(function() { + map = new ol.Map({}); + layer = new ol.layer.Vector({ + source: new ol.source.Vector({wrapX: true}) + }); + renderer = map.getRenderer(); + renderer.layerRenderers_ = {}; + var layerRenderer = new ol.renderer.canvas.Layer(layer); + layerRenderer.prepareFrame = function() { return true; }; + layerRenderer.getImage = function() { return null; }; + renderer.layerRenderers_[goog.getUid(layer)] = layerRenderer; + }); + + it('calls #dispatchComposeEvent_() with a wrapX argument', function() { + var spy = sinon.spy(renderer, 'dispatchComposeEvent_'); + var frameState = { + coordinateToPixelMatrix: map.coordinateToPixelMatrix_, + pixelToCoordinateMatrix: map.pixelToCoordinateMatrix_, + pixelRatio: 1, + size: [100, 100], + skippedFeatureUids: {}, + viewState: { + center: [0, 0], + resolution: 1, + rotation: 0 + }, + layerStates: {}, + layerStatesArray: [{ + layer: layer, + sourceState: 'ready', + visible: true, + minResolution: 1, + maxResolution: 2 + }], + postRenderFunctions: [] + }; + renderer.renderFrame(frameState); + // precompose without wrapX + expect(spy.getCall(0).args[2]).to.be(undefined); + // postcompose with wrapX + expect(spy.getCall(1).args[2]).to.be(true); + }); + }); + +}); + + +goog.require('ol.layer.Vector'); +goog.require('ol.Map'); +goog.require('ol.renderer.canvas.Layer'); +goog.require('ol.renderer.canvas.Map'); +goog.require('ol.source.Vector'); diff --git a/test/spec/ol/renderer/canvas/canvasvectorlayerrenderer.test.js b/test/spec/ol/renderer/canvas/canvasvectorlayerrenderer.test.js index 2457dae729..ca7d8f3c52 100644 --- a/test/spec/ol/renderer/canvas/canvasvectorlayerrenderer.test.js +++ b/test/spec/ol/renderer/canvas/canvasvectorlayerrenderer.test.js @@ -5,14 +5,10 @@ describe('ol.renderer.canvas.VectorLayer', function() { describe('constructor', function() { it('creates a new instance', function() { - var map = new ol.Map({ - target: document.createElement('div') - }); var layer = new ol.layer.Vector({ source: new ol.source.Vector() }); - var renderer = new ol.renderer.canvas.VectorLayer(map.getRenderer(), - layer); + var renderer = new ol.renderer.canvas.VectorLayer(layer); expect(renderer).to.be.a(ol.renderer.canvas.VectorLayer); }); @@ -62,12 +58,10 @@ describe('ol.renderer.canvas.VectorLayer', function() { var renderer; beforeEach(function() { - var map = new ol.Map({}); var layer = new ol.layer.Vector({ source: new ol.source.Vector() }); - renderer = new ol.renderer.canvas.VectorLayer( - map.getRenderer(), layer); + renderer = new ol.renderer.canvas.VectorLayer(layer); var replayGroup = {}; renderer.replayGroup_ = replayGroup; replayGroup.forEachFeatureAtCoordinate = function(coordinate, From 9a58151caa4a40fb28e89b5f52c21d2173ad52f9 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Apr 2015 21:21:16 +0200 Subject: [PATCH 05/11] Do not wrapX for editing --- examples/draw-features.js | 2 +- examples/modify-features.js | 3 ++- externs/olx.js | 13 ++++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/examples/draw-features.js b/examples/draw-features.js index 1a25d6874e..dcab9c70db 100644 --- a/examples/draw-features.js +++ b/examples/draw-features.js @@ -14,7 +14,7 @@ var raster = new ol.layer.Tile({ source: new ol.source.MapQuest({layer: 'sat'}) }); -var source = new ol.source.Vector(); +var source = new ol.source.Vector({wrapX: false}); var vector = new ol.layer.Vector({ source: source, diff --git a/examples/modify-features.js b/examples/modify-features.js index 90709d6a8b..25c6ee18dc 100644 --- a/examples/modify-features.js +++ b/examples/modify-features.js @@ -19,7 +19,8 @@ var raster = new ol.layer.Tile({ var vector = new ol.layer.Vector({ source: new ol.source.Vector({ url: 'data/geojson/countries.geojson', - format: new ol.format.GeoJSON() + format: new ol.format.GeoJSON(), + wrapX: false }) }); diff --git a/externs/olx.js b/externs/olx.js index 674b03b8ee..0856844d8f 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4877,7 +4877,8 @@ olx.source.TileWMSOptions.prototype.wrapX; * loader: (ol.FeatureLoader|undefined), * logo: (string|olx.LogoOptions|undefined), * strategy: (ol.LoadingStrategy|undefined), - * url: (string|undefined)}} + * url: (string|undefined), + * wrapX: (boolean|undefined)}} * @api */ olx.source.VectorOptions; @@ -4946,6 +4947,16 @@ olx.source.VectorOptions.prototype.strategy; olx.source.VectorOptions.prototype.url; +/** + * Wrap the world horizontally. Default is `true`. For vector editing across the + * 0° and 180° meridians to work properly, this should be set to `false`. The + * resulting geometry coordinates will then exceed the world bounds. + * @type {boolean|undefined} + * @api + */ +olx.source.VectorOptions.prototype.wrapX; + + /** * @typedef {{attributions: (Array.|undefined), * crossOrigin: (string|null|undefined), From 6a2aa833b4c851faae816054e1ffa6107bff9123 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 19 Apr 2015 08:45:32 +0200 Subject: [PATCH 06/11] Improve tests for ol.renderer.canvas.VectorLayer --- .../canvas/canvasvectorlayerrenderer.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/spec/ol/renderer/canvas/canvasvectorlayerrenderer.test.js b/test/spec/ol/renderer/canvas/canvasvectorlayerrenderer.test.js index ca7d8f3c52..82636f5730 100644 --- a/test/spec/ol/renderer/canvas/canvasvectorlayerrenderer.test.js +++ b/test/spec/ol/renderer/canvas/canvasvectorlayerrenderer.test.js @@ -55,10 +55,10 @@ describe('ol.renderer.canvas.VectorLayer', function() { }); describe('#forEachFeatureAtCoordinate', function() { - var renderer; + var layer, renderer; beforeEach(function() { - var layer = new ol.layer.Vector({ + layer = new ol.layer.Vector({ source: new ol.source.Vector() }); renderer = new ol.renderer.canvas.VectorLayer(layer); @@ -66,14 +66,13 @@ describe('ol.renderer.canvas.VectorLayer', function() { renderer.replayGroup_ = replayGroup; replayGroup.forEachFeatureAtCoordinate = function(coordinate, resolution, rotation, skippedFeaturesUids, callback) { - var geometry = new ol.geom.Point([0, 0]); var feature = new ol.Feature(); - callback(geometry, feature); - callback(geometry, feature); + callback(feature); + callback(feature); }; }); - it('calls callback once per feature', function() { + it('calls callback once per feature with a layer as 2nd arg', function() { var spy = sinon.spy(); var coordinate = [0, 0]; var frameState = { @@ -86,6 +85,7 @@ describe('ol.renderer.canvas.VectorLayer', function() { renderer.forEachFeatureAtCoordinate( coordinate, frameState, spy, undefined); expect(spy.callCount).to.be(1); + expect(spy.getCall(0).args[1]).to.equal(layer); }); }); From 8fd4e2c7c55789e641bd40a0b9d1c73c0cb074e4 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 19 Apr 2015 08:49:17 +0200 Subject: [PATCH 07/11] Improve docs, comments and code readability --- externs/olx.js | 2 +- src/ol/renderer/canvas/canvasmaprenderer.js | 6 ++++-- src/ol/renderer/canvas/canvasvectorlayerrenderer.js | 8 +++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index 0856844d8f..6b7c541696 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4949,7 +4949,7 @@ olx.source.VectorOptions.prototype.url; /** * Wrap the world horizontally. Default is `true`. For vector editing across the - * 0° and 180° meridians to work properly, this should be set to `false`. The + * -180° and 180° meridians to work properly, this should be set to `false`. The * resulting geometry coordinates will then exceed the world bounds. * @type {boolean|undefined} * @api diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index fd100e65ab..86531201e6 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -135,14 +135,16 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = var worldWidth = ol.extent.getWidth(projectionExtent); var world = 0; while (startX < projectionExtent[0]) { - transform = this.getTransform(frameState, worldWidth * (--world)); + --world; + transform = this.getTransform(frameState, worldWidth * world); replayGroup.replay(context, pixelRatio, transform, rotation, {}); startX += worldWidth; } world = 0; startX = extent[2]; while (startX > projectionExtent[2]) { - transform = this.getTransform(frameState, worldWidth * (++world)); + ++world; + transform = this.getTransform(frameState, worldWidth * ++world); replayGroup.replay(context, pixelRatio, transform, rotation, {}); startX -= worldWidth; } diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index dab1413b83..7e1b68093c 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -116,7 +116,8 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = var worldWidth = ol.extent.getWidth(projectionExtent); var world = 0; while (startX < projectionExtent[0]) { - transform = this.getTransform(frameState, worldWidth * (--world)); + --world; + transform = this.getTransform(frameState, worldWidth * world); replayGroup.replay( replayContext, pixelRatio, transform, rotation, skippedFeatureUids); startX += worldWidth; @@ -124,7 +125,8 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = world = 0; startX = extent[2]; while (startX > projectionExtent[2]) { - transform = this.getTransform(frameState, worldWidth * (++world)); + ++world; + transform = this.getTransform(frameState, worldWidth * world); replayGroup.replay( replayContext, pixelRatio, transform, rotation, skippedFeatureUids); startX -= worldWidth; @@ -229,7 +231,7 @@ ol.renderer.canvas.VectorLayer.prototype.prepareFrame = if (vectorSource.getWrapX() && viewState.projection.isGlobal() && !ol.extent.containsExtent(projectionExtent, frameState.extent)) { - // do not clip when the view crosses the 0 or 180 meridians + // do not clip when the view crosses the -180° or 180° meridians extent[0] = projectionExtent[0]; extent[2] = projectionExtent[2]; } From 4f8dca92ba819e8fcfdd0670de9f72950018688e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 21 Apr 2015 08:21:31 +0200 Subject: [PATCH 08/11] Ensure functions are always called with the same number of args --- src/ol/renderer/canvas/canvaslayerrenderer.js | 7 +++-- src/ol/renderer/canvas/canvasmaprenderer.js | 26 ++++++++++--------- .../canvas/canvasvectorlayerrenderer.js | 2 +- .../renderer/canvas/canvasmaprenderer.test.js | 2 +- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/ol/renderer/canvas/canvaslayerrenderer.js b/src/ol/renderer/canvas/canvaslayerrenderer.js index 3b5e8b09ff..ac8b9a112a 100644 --- a/src/ol/renderer/canvas/canvaslayerrenderer.js +++ b/src/ol/renderer/canvas/canvaslayerrenderer.js @@ -127,7 +127,7 @@ ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = var layer = this.getLayer(); if (layer.hasListener(type)) { var transform = goog.isDef(opt_transform) ? - opt_transform : this.getTransform(frameState); + opt_transform : this.getTransform(frameState, 0); var render = new ol.render.canvas.Immediate( context, frameState.pixelRatio, frameState.extent, transform, frameState.viewState.rotation); @@ -192,15 +192,14 @@ ol.renderer.canvas.Layer.prototype.getImageTransform = goog.abstractMethod; /** * @param {olx.FrameState} frameState Frame state. - * @param {number=} opt_offsetX Offset on the x-axis in view coordinates. + * @param {number} offsetX Offset on the x-axis in view coordinates. * @protected * @return {!goog.vec.Mat4.Number} Transform. */ ol.renderer.canvas.Layer.prototype.getTransform = - function(frameState, opt_offsetX) { + function(frameState, offsetX) { var viewState = frameState.viewState; var pixelRatio = frameState.pixelRatio; - var offsetX = goog.isDef(opt_offsetX) ? opt_offsetX : 0; return ol.vec.Mat4.makeTransform2D(this.transform_, pixelRatio * frameState.size[0] / 2, pixelRatio * frameState.size[1] / 2, diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index 86531201e6..17c1d4fafe 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -93,11 +93,11 @@ ol.renderer.canvas.Map.prototype.createLayerRenderer = function(layer) { /** * @param {ol.render.EventType} type Event type. * @param {olx.FrameState} frameState Frame state. - * @param {boolean=} opt_wrapX Wrap the x-axis. + * @param {boolean} wrapX Wrap the x-axis. * @private */ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = - function(type, frameState, opt_wrapX) { + function(type, frameState, wrapX) { var map = this.getMap(); var context = this.context_; if (map.hasListener(type)) { @@ -108,10 +108,11 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = var projectionExtent = projection.getExtent(); var resolution = viewState.resolution; var rotation = viewState.rotation; - var repeatReplay = (opt_wrapX && projection.isGlobal() && + var repeatReplay = (wrapX && projection.isGlobal() && !ol.extent.containsExtent(projectionExtent, extent)); + var skippedFeaturesHash = {}; - var transform = this.getTransform(frameState); + var transform = this.getTransform(frameState, 0); var tolerance = ol.renderer.vector.getTolerance(resolution, pixelRatio); var replayGroup = new ol.render.canvas.ReplayGroup(tolerance, @@ -128,7 +129,8 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = replayGroup.finish(); if (!replayGroup.isEmpty()) { - replayGroup.replay(context, pixelRatio, transform, rotation, {}); + replayGroup.replay(context, pixelRatio, transform, rotation, + skippedFeaturesHash); if (repeatReplay) { var startX = extent[0]; @@ -137,7 +139,8 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = while (startX < projectionExtent[0]) { --world; transform = this.getTransform(frameState, worldWidth * world); - replayGroup.replay(context, pixelRatio, transform, rotation, {}); + replayGroup.replay(context, pixelRatio, transform, rotation, + skippedFeaturesHash); startX += worldWidth; } world = 0; @@ -145,7 +148,8 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = while (startX > projectionExtent[2]) { ++world; transform = this.getTransform(frameState, worldWidth * ++world); - replayGroup.replay(context, pixelRatio, transform, rotation, {}); + replayGroup.replay(context, pixelRatio, transform, rotation, + skippedFeaturesHash); startX -= worldWidth; } } @@ -158,16 +162,14 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = /** * @param {olx.FrameState} frameState Frame state. - * @param {number=} opt_offsetX Offset on the x-axis in view coordinates. + * @param {number} offsetX Offset on the x-axis in view coordinates. * @protected * @return {!goog.vec.Mat4.Number} Transform. */ -ol.renderer.canvas.Map.prototype.getTransform = - function(frameState, opt_offsetX) { +ol.renderer.canvas.Map.prototype.getTransform = function(frameState, offsetX) { var pixelRatio = frameState.pixelRatio; var viewState = frameState.viewState; var resolution = viewState.resolution; - var offsetX = goog.isDef(opt_offsetX) ? opt_offsetX : 0; return ol.vec.Mat4.makeTransform2D(this.transform_, this.canvas_.width / 2, this.canvas_.height / 2, pixelRatio / resolution, -pixelRatio / resolution, @@ -210,7 +212,7 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { this.calculateMatrices2D(frameState); - this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, frameState); + this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, frameState, false); var layerStatesArray = frameState.layerStatesArray; var viewResolution = frameState.viewState.resolution; diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 7e1b68093c..0479523352 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -86,7 +86,7 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = var vectorSource = this.getLayer().getSource(); goog.asserts.assertInstanceof(vectorSource, ol.source.Vector); - var transform = this.getTransform(frameState); + var transform = this.getTransform(frameState, 0); this.dispatchPreComposeEvent(context, frameState, transform); diff --git a/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js b/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js index c4894f3f7a..061ee1b5e6 100644 --- a/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js +++ b/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js @@ -55,7 +55,7 @@ describe('ol.renderer.canvas.Map', function() { }; renderer.renderFrame(frameState); // precompose without wrapX - expect(spy.getCall(0).args[2]).to.be(undefined); + expect(spy.getCall(0).args[2]).to.be(false); // postcompose with wrapX expect(spy.getCall(1).args[2]).to.be(true); }); From 40feabc3c8471f3bf6551e600917482fbdbb7666 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 21 Apr 2015 08:47:36 +0200 Subject: [PATCH 09/11] Do not require an extent for global projections --- src/ol/proj/proj.js | 20 ++++++++-- src/ol/renderer/canvas/canvasmaprenderer.js | 2 +- .../canvas/canvasvectorlayerrenderer.js | 4 +- src/ol/renderer/maprenderer.js | 2 +- test/spec/ol/proj/proj.test.js | 40 +++++++++++++++++++ 5 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/ol/proj/proj.js b/src/ol/proj/proj.js index 348432e087..ddcc57484f 100644 --- a/src/ol/proj/proj.js +++ b/src/ol/proj/proj.js @@ -120,9 +120,13 @@ ol.proj.Projection = function(options) { * @type {boolean} */ this.global_ = goog.isDef(options.global) ? options.global : false; - if (this.global_) { - goog.asserts.assert(!goog.isNull(this.extent_)); - } + + + /** + * @private + * @type {boolean} + */ + this.canWrapX_ = this.global_ && !goog.isNull(this.extent_); /** * @private @@ -175,6 +179,14 @@ ol.proj.Projection = function(options) { }; +/** + * @return {boolean} The projection is suitable for wrapping the x-axis + */ +ol.proj.Projection.prototype.canWrapX = function() { + return this.canWrapX_; +}; + + /** * Get the code for this projection, e.g. 'EPSG:4326'. * @return {string} Code. @@ -258,6 +270,7 @@ ol.proj.Projection.prototype.isGlobal = function() { */ ol.proj.Projection.prototype.setGlobal = function(global) { this.global_ = global; + this.canWrapX_ = global && !goog.isNull(this.extent_); }; @@ -284,6 +297,7 @@ ol.proj.Projection.prototype.setDefaultTileGrid = function(tileGrid) { */ ol.proj.Projection.prototype.setExtent = function(extent) { this.extent_ = extent; + this.canWrapX_ = this.global_ && !goog.isNull(extent); }; diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index 17c1d4fafe..3a15b7f199 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -108,7 +108,7 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = var projectionExtent = projection.getExtent(); var resolution = viewState.resolution; var rotation = viewState.rotation; - var repeatReplay = (wrapX && projection.isGlobal() && + var repeatReplay = (wrapX && projection.canWrapX() && !ol.extent.containsExtent(projectionExtent, extent)); var skippedFeaturesHash = {}; diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 0479523352..5ecbec9901 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -110,7 +110,7 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = replayGroup.replay( replayContext, pixelRatio, transform, rotation, skippedFeatureUids); - if (vectorSource.getWrapX() && projection.isGlobal() && + if (vectorSource.getWrapX() && projection.canWrapX() && !ol.extent.containsExtent(projectionExtent, frameState.extent)) { var startX = extent[0]; var worldWidth = ol.extent.getWidth(projectionExtent); @@ -229,7 +229,7 @@ ol.renderer.canvas.VectorLayer.prototype.prepareFrame = vectorLayerRenderBuffer * resolution); var projectionExtent = viewState.projection.getExtent(); - if (vectorSource.getWrapX() && viewState.projection.isGlobal() && + if (vectorSource.getWrapX() && viewState.projection.canWrapX() && !ol.extent.containsExtent(projectionExtent, frameState.extent)) { // do not clip when the view crosses the -180° or 180° meridians extent[0] = projectionExtent[0]; diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index 295b3fc174..26145381ca 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -157,7 +157,7 @@ ol.renderer.Map.prototype.forEachFeatureAtCoordinate = var projection = viewState.projection; var translatedX; - if (projection.isGlobal()) { + if (projection.canWrapX()) { var projectionExtent = projection.getExtent(); var worldWidth = ol.extent.getWidth(projectionExtent); var x = coordinate[0]; diff --git a/test/spec/ol/proj/proj.test.js b/test/spec/ol/proj/proj.test.js index 1714236b0d..de7ba4935a 100644 --- a/test/spec/ol/proj/proj.test.js +++ b/test/spec/ol/proj/proj.test.js @@ -138,6 +138,46 @@ describe('ol.proj', function() { }); }); + describe('canWrapX()', function() { + + it('requires an extent for allowing wrapX', function() { + var proj = new ol.proj.Projection({ + code: 'foo', + global: true + }); + expect(proj.canWrapX()).to.be(false); + proj.setExtent([1, 2, 3, 4]); + expect(proj.canWrapX()).to.be(true); + proj = new ol.proj.Projection({ + code: 'foo', + global: true, + extent: [1, 2, 3, 4] + }); + expect(proj.canWrapX()).to.be(true); + proj.setExtent(null); + expect(proj.canWrapX()).to.be(false); + }); + + it('requires global to be true for allowing wrapX', function() { + var proj = new ol.proj.Projection({ + code: 'foo', + extent: [1, 2, 3, 4] + }); + expect(proj.canWrapX()).to.be(false); + proj.setGlobal(true); + expect(proj.canWrapX()).to.be(true); + proj = new ol.proj.Projection({ + code: 'foo', + global: true, + extent: [1, 2, 3, 4] + }); + expect(proj.canWrapX()).to.be(true); + proj.setGlobal(false); + expect(proj.canWrapX()).to.be(false); + }); + + }); + describe('transformExtent()', function() { it('transforms an extent given projection identifiers', function() { From 23ed120361bd7f53293c888c118297d267ce5b28 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 21 Apr 2015 09:56:03 +0200 Subject: [PATCH 10/11] Clarify when we make feature overlays wrap the x-axis --- src/ol/renderer/canvas/canvasmaprenderer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index 3a15b7f199..6fccd6d922 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -229,6 +229,8 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { continue; } if (layerRenderer.prepareFrame(frameState, layerState)) { + // As soon as a vector layer on the map has wrapX set to true, we make + // feature overlays wrap the x-axis too. if (layer instanceof ol.layer.Vector && layer.getSource().getWrapX()) { wrapX = true; } From 513677fecdbe5dda5ecdc90177e6243dc3f6a0f5 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Wed, 22 Apr 2015 09:11:19 +0200 Subject: [PATCH 11/11] Render map replay group on the correct world instead of wrapping it By using the frameState's focus, we can adjust extent and transform and render it for the world of interest instead of wrapping it and rendering for every visible world. --- src/ol/renderer/canvas/canvasmaprenderer.js | 67 +++++++------------ .../canvas/canvasvectorlayerrenderer.js | 36 +++++++--- src/ol/renderer/maprenderer.js | 19 ++---- src/ol/source/source.js | 17 ++++- src/ol/source/tilesource.js | 22 +++--- src/ol/source/vectorsource.js | 17 +---- .../renderer/canvas/canvasmaprenderer.test.js | 31 +++++++-- 7 files changed, 108 insertions(+), 101 deletions(-) diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index 6fccd6d922..6a1c38e691 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -93,11 +93,10 @@ ol.renderer.canvas.Map.prototype.createLayerRenderer = function(layer) { /** * @param {ol.render.EventType} type Event type. * @param {olx.FrameState} frameState Frame state. - * @param {boolean} wrapX Wrap the x-axis. * @private */ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = - function(type, frameState, wrapX) { + function(type, frameState) { var map = this.getMap(); var context = this.context_; if (map.hasListener(type)) { @@ -105,21 +104,29 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = var pixelRatio = frameState.pixelRatio; var viewState = frameState.viewState; var projection = viewState.projection; - var projectionExtent = projection.getExtent(); var resolution = viewState.resolution; var rotation = viewState.rotation; - var repeatReplay = (wrapX && projection.canWrapX() && - !ol.extent.containsExtent(projectionExtent, extent)); - var skippedFeaturesHash = {}; - var transform = this.getTransform(frameState, 0); + var offsetX = 0; + if (projection.canWrapX()) { + var projectionExtent = projection.getExtent(); + var worldWidth = ol.extent.getWidth(projectionExtent); + var x = frameState.focus[0]; + if (x < projectionExtent[0] || x > projectionExtent[2]) { + var worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth); + offsetX = worldWidth * worldsAway; + extent = [ + extent[0] + offsetX, extent[1], + extent[2] + offsetX, extent[3] + ]; + } + } + + var transform = this.getTransform(frameState, offsetX); var tolerance = ol.renderer.vector.getTolerance(resolution, pixelRatio); - var replayGroup = new ol.render.canvas.ReplayGroup(tolerance, - repeatReplay ? - [projectionExtent[0], extent[1], projectionExtent[2], extent[3]] : - extent, - resolution); + var replayGroup = new ol.render.canvas.ReplayGroup( + tolerance, extent, resolution); var vectorContext = new ol.render.canvas.Immediate(context, pixelRatio, extent, transform, rotation); @@ -129,30 +136,8 @@ ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = replayGroup.finish(); if (!replayGroup.isEmpty()) { - replayGroup.replay(context, pixelRatio, transform, rotation, - skippedFeaturesHash); + replayGroup.replay(context, pixelRatio, transform, rotation, {}); - if (repeatReplay) { - var startX = extent[0]; - var worldWidth = ol.extent.getWidth(projectionExtent); - var world = 0; - while (startX < projectionExtent[0]) { - --world; - transform = this.getTransform(frameState, worldWidth * world); - replayGroup.replay(context, pixelRatio, transform, rotation, - skippedFeaturesHash); - startX += worldWidth; - } - world = 0; - startX = extent[2]; - while (startX > projectionExtent[2]) { - ++world; - transform = this.getTransform(frameState, worldWidth * ++world); - replayGroup.replay(context, pixelRatio, transform, rotation, - skippedFeaturesHash); - startX -= worldWidth; - } - } } vectorContext.flush(); this.replayGroup = replayGroup; @@ -174,7 +159,7 @@ ol.renderer.canvas.Map.prototype.getTransform = function(frameState, offsetX) { this.canvas_.width / 2, this.canvas_.height / 2, pixelRatio / resolution, -pixelRatio / resolution, -viewState.rotation, - -viewState.center[0] + offsetX, + -viewState.center[0] - offsetX, -viewState.center[1]); }; @@ -212,11 +197,10 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { this.calculateMatrices2D(frameState); - this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, frameState, false); + this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, frameState); var layerStatesArray = frameState.layerStatesArray; var viewResolution = frameState.viewState.resolution; - var wrapX = false; var i, ii, layer, layerRenderer, layerState; for (i = 0, ii = layerStatesArray.length; i < ii; ++i) { layerState = layerStatesArray[i]; @@ -229,17 +213,12 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { continue; } if (layerRenderer.prepareFrame(frameState, layerState)) { - // As soon as a vector layer on the map has wrapX set to true, we make - // feature overlays wrap the x-axis too. - if (layer instanceof ol.layer.Vector && layer.getSource().getWrapX()) { - wrapX = true; - } layerRenderer.composeFrame(frameState, layerState, context); } } this.dispatchComposeEvent_( - ol.render.EventType.POSTCOMPOSE, frameState, wrapX); + ol.render.EventType.POSTCOMPOSE, frameState); if (!this.renderedVisible_) { goog.style.setElementShown(this.canvas_, true); diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 5ecbec9901..45a83ea653 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -77,6 +77,7 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, layerState, context) { var extent = frameState.extent; + var focus = frameState.focus; var pixelRatio = frameState.pixelRatio; var skippedFeatureUids = frameState.skippedFeatureUids; var viewState = frameState.viewState; @@ -107,30 +108,47 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = // see http://jsperf.com/context-save-restore-versus-variable var alpha = replayContext.globalAlpha; replayContext.globalAlpha = layerState.opacity; - replayGroup.replay( - replayContext, pixelRatio, transform, rotation, skippedFeatureUids); + var noSkip = {}; + var focusX = focus[0]; if (vectorSource.getWrapX() && projection.canWrapX() && - !ol.extent.containsExtent(projectionExtent, frameState.extent)) { + !ol.extent.containsExtent(projectionExtent, extent)) { + var projLeft = projectionExtent[0]; + var projRight = projectionExtent[2]; + // A feature from skippedFeatureUids will only be skipped in the world + // that has the frameState's focus, because this is where a feature + // overlay for highlighting or selection would render the skipped + // feature. + replayGroup.replay(replayContext, pixelRatio, transform, rotation, + projLeft <= focusX && focusX <= projRight ? + skippedFeatureUids : noSkip); var startX = extent[0]; var worldWidth = ol.extent.getWidth(projectionExtent); var world = 0; + var offsetX; while (startX < projectionExtent[0]) { --world; - transform = this.getTransform(frameState, worldWidth * world); - replayGroup.replay( - replayContext, pixelRatio, transform, rotation, skippedFeatureUids); + offsetX = worldWidth * world; + transform = this.getTransform(frameState, offsetX); + replayGroup.replay(replayContext, pixelRatio, transform, rotation, + projLeft + offsetX <= focusX && focusX <= projRight + offsetX ? + skippedFeatureUids : noSkip); startX += worldWidth; } world = 0; startX = extent[2]; while (startX > projectionExtent[2]) { ++world; - transform = this.getTransform(frameState, worldWidth * world); - replayGroup.replay( - replayContext, pixelRatio, transform, rotation, skippedFeatureUids); + offsetX = worldWidth * world; + transform = this.getTransform(frameState, offsetX); + replayGroup.replay(replayContext, pixelRatio, transform, rotation, + projLeft + offsetX <= focusX && focusX <= projRight + offsetX ? + skippedFeatureUids : noSkip); startX -= worldWidth; } + } else { + replayGroup.replay( + replayContext, pixelRatio, transform, rotation, skippedFeatureUids); } if (replayContext != context) { diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index 26145381ca..666ae642b0 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -41,6 +41,7 @@ ol.renderer.Map = function(container, map) { goog.base(this); + /** * @private * @type {ol.Map} @@ -156,25 +157,20 @@ ol.renderer.Map.prototype.forEachFeatureAtCoordinate = var projection = viewState.projection; - var translatedX; + var translatedCoordinate = coordinate; if (projection.canWrapX()) { var projectionExtent = projection.getExtent(); var worldWidth = ol.extent.getWidth(projectionExtent); var x = coordinate[0]; if (x < projectionExtent[0] || x > projectionExtent[2]) { var worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth); - translatedX = x + worldWidth * worldsAway; + translatedCoordinate = [x + worldWidth * worldsAway, coordinate[1]]; } } if (!goog.isNull(this.replayGroup)) { - result = this.replayGroup.forEachFeatureAtCoordinate(coordinate, + result = this.replayGroup.forEachFeatureAtCoordinate(translatedCoordinate, viewResolution, viewRotation, {}, forEachFeatureAtCoordinate); - if (!result && goog.isDef(translatedX)) { - result = this.replayGroup.forEachFeatureAtCoordinate( - [translatedX, coordinate[1]], - viewResolution, viewRotation, {}, forEachFeatureAtCoordinate); - } if (result) { return result; } @@ -189,11 +185,8 @@ ol.renderer.Map.prototype.forEachFeatureAtCoordinate = layerFilter.call(thisArg2, layer)) { var layerRenderer = this.getLayerRenderer(layer); result = layerRenderer.forEachFeatureAtCoordinate( - coordinate, frameState, callback, thisArg); - if (!result && goog.isDef(translatedX)) { - result = layerRenderer.forEachFeatureAtCoordinate( - [translatedX, coordinate[1]], frameState, callback, thisArg); - } + layer.getSource().getWrapX() ? translatedCoordinate : coordinate, + frameState, callback, thisArg); if (result) { return result; } diff --git a/src/ol/source/source.js b/src/ol/source/source.js index 75a02f5cd3..e215cb40e9 100644 --- a/src/ol/source/source.js +++ b/src/ol/source/source.js @@ -24,7 +24,8 @@ ol.source.State = { * @typedef {{attributions: (Array.|undefined), * logo: (string|olx.LogoOptions|undefined), * projection: ol.proj.ProjectionLike, - * state: (ol.source.State|undefined)}} + * state: (ol.source.State|undefined), + * wrapX: (boolean|undefined)}} */ ol.source.SourceOptions; @@ -72,6 +73,12 @@ ol.source.Source = function(options) { this.state_ = goog.isDef(options.state) ? options.state : ol.source.State.READY; + /** + * @private + * @type {boolean|undefined} + */ + this.wrapX_ = options.wrapX; + }; goog.inherits(ol.source.Source, ol.Object); @@ -135,6 +142,14 @@ ol.source.Source.prototype.getState = function() { }; +/** + * @return {boolean|undefined} Wrap X. + */ +ol.source.Source.prototype.getWrapX = function() { + return this.wrapX_; +}; + + /** * Set the attributions of the source. * @param {Array.} attributions Attributions. diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index abb7ba7730..e81a4261b3 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -47,7 +47,8 @@ ol.source.Tile = function(options) { extent: options.extent, logo: options.logo, projection: options.projection, - state: options.state + state: options.state, + wrapX: options.wrapX }); /** @@ -81,12 +82,6 @@ ol.source.Tile = function(options) { */ this.tmpSize = [0, 0]; - /** - * @private - * @type {boolean|undefined} - */ - this.wrapX_ = options.wrapX; - }; goog.inherits(ol.source.Tile, ol.source.Source); @@ -221,10 +216,10 @@ ol.source.Tile.prototype.getTilePixelSize = /** - * Handles x-axis wrapping. When `this.wrapX_` is undefined or the projection - * is not a global projection, `tileCoord` will be returned unaltered. When - * `this.wrapX_` is true, the tile coordinate will be wrapped horizontally. - * When `this.wrapX_` is `false`, `null` will be returned for tiles that are + * Handles x-axis wrapping. When `wrapX` is `undefined` or the projection is not + * a global projection, `tileCoord` will be returned unaltered. When `wrapX` is + * `true`, the tile coordinate will be wrapped horizontally. + * When `wrapX` is `false`, `null` will be returned for tiles that are * outside the projection extent. * @param {ol.TileCoord} tileCoord Tile coordinate. * @param {ol.proj.Projection=} opt_projection Projection. @@ -235,8 +230,9 @@ ol.source.Tile.prototype.getWrapXTileCoord = var projection = goog.isDef(opt_projection) ? opt_projection : this.getProjection(); var tileGrid = this.getTileGridForProjection(projection); - if (goog.isDef(this.wrapX_) && tileGrid.isGlobal(tileCoord[0], projection)) { - return this.wrapX_ ? + var wrapX = this.getWrapX(); + if (goog.isDef(wrapX) && tileGrid.isGlobal(tileCoord[0], projection)) { + return wrapX ? ol.tilecoord.wrapX(tileCoord, tileGrid, projection) : ol.tilecoord.clipX(tileCoord, tileGrid, projection); } else { diff --git a/src/ol/source/vectorsource.js b/src/ol/source/vectorsource.js index eadeb180e8..42f945f9d7 100644 --- a/src/ol/source/vectorsource.js +++ b/src/ol/source/vectorsource.js @@ -79,7 +79,8 @@ ol.source.Vector = function(opt_options) { attributions: options.attributions, logo: options.logo, projection: undefined, - state: ol.source.State.READY + state: ol.source.State.READY, + wrapX: goog.isDef(options.wrapX) ? options.wrapX : true }); /** @@ -146,12 +147,6 @@ ol.source.Vector = function(opt_options) { this.addFeaturesInternal(options.features); } - /** - * @type {boolean} - * @private - */ - this.wrapX_ = goog.isDef(options.wrapX) ? options.wrapX : true; - }; goog.inherits(ol.source.Vector, ol.source.Source); @@ -570,14 +565,6 @@ ol.source.Vector.prototype.getFeatureById = function(id) { }; -/** - * @return {boolean} - */ -ol.source.Vector.prototype.getWrapX = function() { - return this.wrapX_; -}; - - /** * @param {goog.events.Event} event Event. * @private diff --git a/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js b/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js index 061ee1b5e6..f5209f1ce6 100644 --- a/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js +++ b/test/spec/ol/renderer/canvas/canvasmaprenderer.test.js @@ -19,6 +19,7 @@ describe('ol.renderer.canvas.Map', function() { beforeEach(function() { map = new ol.Map({}); + map.on('postcompose', function() {}); layer = new ol.layer.Vector({ source: new ol.source.Vector({wrapX: true}) }); @@ -30,16 +31,23 @@ describe('ol.renderer.canvas.Map', function() { renderer.layerRenderers_[goog.getUid(layer)] = layerRenderer; }); - it('calls #dispatchComposeEvent_() with a wrapX argument', function() { - var spy = sinon.spy(renderer, 'dispatchComposeEvent_'); + it('uses correct extent and offset on wrapped worlds', function() { + var spy = sinon.spy(renderer, 'getTransform'); + var proj = new ol.proj.Projection({ + code: 'foo', + extent: [-180, -90, 180, 90], + global: true + }); var frameState = { coordinateToPixelMatrix: map.coordinateToPixelMatrix_, pixelToCoordinateMatrix: map.pixelToCoordinateMatrix_, pixelRatio: 1, size: [100, 100], skippedFeatureUids: {}, + extent: proj.getExtent(), viewState: { center: [0, 0], + projection: proj, resolution: 1, rotation: 0 }, @@ -53,11 +61,21 @@ describe('ol.renderer.canvas.Map', function() { }], postRenderFunctions: [] }; + frameState.focus = [0, 0]; + // focus is on real world renderer.renderFrame(frameState); - // precompose without wrapX - expect(spy.getCall(0).args[2]).to.be(false); - // postcompose with wrapX - expect(spy.getCall(1).args[2]).to.be(true); + expect(spy.getCall(0).args[1]).to.be(0); + expect(renderer.replayGroup.maxExtent_).to.eql([-180, -90, 180, 90]); + frameState.focus = [-200, 0]; + // focus is one world left of the real world + renderer.renderFrame(frameState); + expect(spy.getCall(1).args[1]).to.be(360); + expect(renderer.replayGroup.maxExtent_).to.eql([180, -90, 540, 90]); + frameState.focus = [200, 0]; + // focus is one world right of the real world + renderer.renderFrame(frameState); + expect(spy.getCall(2).args[1]).to.be(-360); + expect(renderer.replayGroup.maxExtent_).to.eql([-540, -90, -180, 90]); }); }); @@ -66,6 +84,7 @@ describe('ol.renderer.canvas.Map', function() { goog.require('ol.layer.Vector'); goog.require('ol.Map'); +goog.require('ol.proj.Projection'); goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.canvas.Map'); goog.require('ol.source.Vector');