From 44a64ba45104cb776f1d1982258d1579e7c2ab1d Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Tue, 9 Jun 2015 15:43:40 +0200 Subject: [PATCH 01/80] Add ol.math.solveLinearSystem --- src/ol/math.js | 63 +++++++++++++++++++++++++++++++++++++++ test/spec/ol/math.test.js | 39 ++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/ol/math.js b/src/ol/math.js index c0244bda01..2ca041d2b3 100644 --- a/src/ol/math.js +++ b/src/ol/math.js @@ -78,6 +78,69 @@ ol.math.squaredDistance = function(x1, y1, x2, y2) { }; +/** + * Solves system of linear equations using Gaussian elimination method. + * + * @param {Array.>} A Augmented matrix (n x n + 1 column) + * in row-major order. + * @return {Array.} The resulting vector. + */ +ol.math.solveLinearSystem = function(A) { + var n = A.length; + + if (goog.asserts.ENABLE_ASSERTS) { + for (var row = 0; row < n; row++) { + goog.asserts.assert(A[row].length == n + 1, + 'every row should have correct number of columns'); + } + } + + for (var i = 0; i < n; i++) { + // Find max in the i-th column (ignoring i - 1 first rows) + var maxRow = i; + var maxEl = Math.abs(A[i][i]); + for (var r = i + 1; r < n; r++) { + var absValue = Math.abs(A[r][i]); + if (absValue > maxEl) { + maxEl = absValue; + maxRow = r; + } + } + + if (maxEl === 0) { + return null; // matrix is singular + } + + // Swap max row with i-th (current) row + var tmp = A[maxRow]; + A[maxRow] = A[i]; + A[i] = tmp; + + // Subtract the i-th row to make all the remaining rows 0 in the i-th column + for (var j = i + 1; j < n; j++) { + var coef = -A[j][i] / A[i][i]; + for (var k = i; k < n + 1; k++) { + if (i == k) { + A[j][k] = 0; + } else { + A[j][k] += coef * A[i][k]; + } + } + } + } + + // Solve Ax=b for upper triangular matrix A + var x = new Array(n); + for (var l = n - 1; l >= 0; l--) { + x[l] = A[l][n] / A[l][l]; + for (var m = l - 1; m >= 0; m--) { + A[m][n] -= A[m][l] * x[l]; + } + } + return x; +}; + + /** * Converts radians to to degrees. * diff --git a/test/spec/ol/math.test.js b/test/spec/ol/math.test.js index f299028b8d..ba781f75da 100644 --- a/test/spec/ol/math.test.js +++ b/test/spec/ol/math.test.js @@ -91,6 +91,45 @@ describe('ol.math.roundUpToPowerOfTwo', function() { }); +describe('ol.math.solveLinearSystem', function() { + it('calculates correctly', function() { + var result = ol.math.solveLinearSystem([ + [2, 1, 3, 1], + [2, 6, 8, 3], + [6, 8, 18, 5] + ]); + expect(result[0]).to.roughlyEqual(0.3, 1e-9); + expect(result[1]).to.roughlyEqual(0.4, 1e-9); + expect(result[2]).to.roughlyEqual(0, 1e-9); + }); + it('can handle singular matrix', function() { + var result = ol.math.solveLinearSystem([ + [2, 1, 3, 1], + [2, 6, 8, 3], + [2, 1, 3, 1] + ]); + expect(result).to.be(null); + }); + it('raises an exception when the matrix is malformed', function() { + expect(function() { + ol.math.solveLinearSystem([ + [2, 1, 3, 1], + [2, 6, 8, 3], + [6, 8, 18] + ]); + }).to.throwException(); + + expect(function() { + ol.math.solveLinearSystem([ + [2, 1, 3, 1], + [2, 6, 8, 3], + [6, 8, 18, 5, 0] + ]); + }).to.throwException(); + }); +}); + + describe('ol.math.toDegrees', function() { it('returns the correct value at -π', function() { expect(ol.math.toDegrees(-Math.PI)).to.be(-180); From 1222287f22892f5166642a7d1b0a4108fe9dc815 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 25 May 2015 10:55:14 +0200 Subject: [PATCH 02/80] Add ol.reproj --- src/ol/reproj/image.js | 181 ++++++++++++++++++++++ src/ol/reproj/reproj.js | 119 +++++++++++++++ src/ol/reproj/tile.js | 265 +++++++++++++++++++++++++++++++++ src/ol/reproj/triangulation.js | 104 +++++++++++++ 4 files changed, 669 insertions(+) create mode 100644 src/ol/reproj/image.js create mode 100644 src/ol/reproj/reproj.js create mode 100644 src/ol/reproj/tile.js create mode 100644 src/ol/reproj/triangulation.js diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js new file mode 100644 index 0000000000..ed4a67f0bd --- /dev/null +++ b/src/ol/reproj/image.js @@ -0,0 +1,181 @@ +goog.provide('ol.reproj.Image'); + +goog.require('goog.asserts'); +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('ol.ImageBase'); +goog.require('ol.ImageState'); +goog.require('ol.dom'); +goog.require('ol.extent'); +goog.require('ol.proj'); +goog.require('ol.reproj'); +goog.require('ol.reproj.triangulation'); + + + +/** + * @constructor + * @extends {ol.ImageBase} + * @param {ol.proj.Projection} sourceProj + * @param {ol.proj.Projection} targetProj + * @param {ol.Extent} targetExtent + * @param {number} targetResolution + * @param {number} pixelRatio + * @param {function(ol.Extent, number, number, ol.proj.Projection) : + * ol.ImageBase} getImageFunction + */ +ol.reproj.Image = function(sourceProj, targetProj, + targetExtent, targetResolution, pixelRatio, getImageFunction) { + + var width = ol.extent.getWidth(targetExtent) / targetResolution; + var height = ol.extent.getHeight(targetExtent) / targetResolution; + + /** + * @private + * @type {Canvas2DRenderingContext} + */ + this.context_ = ol.dom.createCanvasContext2D(width, height); + this.context_.imageSmoothingEnabled = true; + + if (goog.DEBUG) { + this.context_.fillStyle = 'rgba(255,0,0,0.1)'; + this.context_.fillRect(0, 0, width, height); + } + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = this.context_.canvas; + + var transformInv = ol.proj.getTransform(targetProj, sourceProj); + + /** + * @private + * @type {!ol.reproj.Triangulation} + */ + this.triangles_ = ol.reproj.triangulation.createForExtent( + targetExtent, transformInv); + + /** + * @private + * @type {number} + */ + this.targetResolution_ = targetResolution; + + /** + * @private + * @type {!ol.Extent} + */ + this.targetExtent_ = targetExtent; + + var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangles_); + + var idealSourceResolution = + targetProj.getPointResolution(targetResolution, + ol.extent.getCenter(targetExtent)) * + targetProj.getMetersPerUnit() / sourceProj.getMetersPerUnit(); + + /** + * @private + * @type {ol.ImageBase} + */ + this.srcImage_ = getImageFunction(srcExtent, idealSourceResolution, + pixelRatio, sourceProj); + + /** + * @private + * @type {goog.events.Key} + */ + this.sourceListenerKey_ = null; + + + var state = ol.ImageState.LOADED; + var attributions = []; + + if (!goog.isNull(this.srcImage_)) { + state = ol.ImageState.IDLE; + attributions = this.srcImage_.getAttributions(); + } + + goog.base(this, targetExtent, targetResolution, pixelRatio, + state, attributions); +}; +goog.inherits(ol.reproj.Image, ol.ImageBase); + + +/** + * @inheritDoc + */ +ol.reproj.Image.prototype.disposeInternal = function() { + if (this.state == ol.ImageState.LOADING) { + this.unlistenSource_(); + } + goog.base(this, 'disposeInternal'); +}; + + +/** + * @inheritDoc + */ +ol.reproj.Image.prototype.getImage = function(opt_context) { + return this.canvas_; +}; + + +/** + * @private + */ +ol.reproj.Image.prototype.reproject_ = function() { + var srcState = this.srcImage_.getState(); + if (srcState == ol.ImageState.LOADED) { + // render the reprojected content + ol.reproj.renderTriangles(this.context_, this.srcImage_.getResolution(), + this.targetResolution_, this.targetExtent_, + this.triangles_, [{ + extent: this.srcImage_.getExtent(), + image: this.srcImage_.getImage() + }]); + } + this.state = srcState; + this.changed(); +}; + + +/** + * @inheritDoc + */ +ol.reproj.Image.prototype.load = function() { + if (this.state == ol.ImageState.IDLE) { + this.state = ol.ImageState.LOADING; + this.changed(); + + var srcState = this.srcImage_.getState(); + if (srcState == ol.ImageState.LOADED || + srcState == ol.ImageState.ERROR) { + this.reproject_(); + } else { + this.sourceListenerKey_ = this.srcImage_.listen( + goog.events.EventType.CHANGE, function(e) { + var srcState = this.srcImage_.getState(); + if (srcState == ol.ImageState.LOADED || + srcState == ol.ImageState.ERROR) { + this.unlistenSource_(); + this.reproject_(); + } + }, false, this); + this.srcImage_.load(); + } + } +}; + + +/** + * @private + */ +ol.reproj.Image.prototype.unlistenSource_ = function() { + goog.asserts.assert(!goog.isNull(this.sourceListenerKey_), + 'this.sourceListenerKey_ should not be null'); + goog.events.unlistenByKey(this.sourceListenerKey_); + this.sourceListenerKey_ = null; +}; diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js new file mode 100644 index 0000000000..b82f511444 --- /dev/null +++ b/src/ol/reproj/reproj.js @@ -0,0 +1,119 @@ +goog.provide('ol.reproj'); + +goog.require('goog.array'); +goog.require('ol.extent'); +goog.require('ol.math'); + + +/** + * Renders the source into the canvas based on the triangulation. + * @param {CanvasRenderingContext2D} context + * @param {number} sourceResolution + * @param {number} targetResolution + * @param {ol.Extent} targetExtent + * @param {ol.reproj.Triangulation} triangulation + * @param {Array.<{extent: ol.Extent, + * image: (HTMLCanvasElement|Image)}>} sources + */ +ol.reproj.renderTriangles = function(context, + sourceResolution, targetResolution, targetExtent, triangulation, sources) { + goog.array.forEach(triangulation, function(tri, i, arr) { + context.save(); + + var targetTL = ol.extent.getTopLeft(targetExtent); + + /* Calculate affine transform (src -> dst) + * Resulting matrix can be used to transform coordinate + * from `sourceProjection` to destination pixels. + * + * To optimize number of context calls and increase numerical stability, + * we also do the following operations: + * trans(-topLeftExtentCorner), scale(1 / targetResolution), scale(1, -1) + * here before solving the linear system. + * + * Src points: xi, yi + * Dst points: ui, vi + * Affine coefficients: aij + * + * | x0 y0 1 0 0 0 | |a00| |u0| + * | x1 y1 1 0 0 0 | |a01| |u1| + * | x2 y2 1 0 0 0 | x |a02| = |u2| + * | 0 0 0 x0 y0 1 | |a10| |v0| + * | 0 0 0 x1 y1 1 | |a11| |v1| + * | 0 0 0 x2 y2 1 | |a12| |v2| + */ + var x0 = tri[0][0][0], y0 = tri[0][0][1], + x1 = tri[1][0][0], y1 = tri[1][0][1], + x2 = tri[2][0][0], y2 = tri[2][0][1]; + var u0 = tri[0][1][0] - targetTL[0], v0 = -(tri[0][1][1] - targetTL[1]), + u1 = tri[1][1][0] - targetTL[0], v1 = -(tri[1][1][1] - targetTL[1]), + u2 = tri[2][1][0] - targetTL[0], v2 = -(tri[2][1][1] - targetTL[1]); + var augmentedMatrix = [ + [x0, y0, 1, 0, 0, 0, u0 / targetResolution], + [x1, y1, 1, 0, 0, 0, u1 / targetResolution], + [x2, y2, 1, 0, 0, 0, u2 / targetResolution], + [0, 0, 0, x0, y0, 1, v0 / targetResolution], + [0, 0, 0, x1, y1, 1, v1 / targetResolution], + [0, 0, 0, x2, y2, 1, v2 / targetResolution] + ]; + var coefs = ol.math.solveLinearSystem(augmentedMatrix); + if (goog.isNull(coefs)) { + return; + } + + context.setTransform(coefs[0], coefs[3], coefs[1], + coefs[4], coefs[2], coefs[5]); + + var pixelSize = sourceResolution; + var centroid = [(x0 + x1 + x2) / 3, (y0 + y1 + y2) / 3]; + + // moves the `point` farther away from the `anchor` + var increasePointDistance = function(point, anchor, increment) { + var dir = [point[0] - anchor[0], point[1] - anchor[1]]; + var distance = Math.sqrt(dir[0] * dir[0] + dir[1] * dir[1]); + var scaleFactor = (distance + increment) / distance; + return [anchor[0] + scaleFactor * dir[0], + anchor[1] + scaleFactor * dir[1]]; + }; + + // enlarge the triangle so that the clip paths of individual triangles + // slightly (1px) overlap to prevent transparency errors on triangle edges + var p0 = increasePointDistance([x0, y0], centroid, pixelSize); + var p1 = increasePointDistance([x1, y1], centroid, pixelSize); + var p2 = increasePointDistance([x2, y2], centroid, pixelSize); + + context.beginPath(); + context.moveTo(p0[0], p0[1]); + context.lineTo(p1[0], p1[1]); + context.lineTo(p2[0], p2[1]); + context.closePath(); + context.clip(); + + goog.array.forEach(sources, function(src, i, arr) { + context.save(); + var tlSrcFromData = ol.extent.getTopLeft(src.extent); + context.translate(tlSrcFromData[0], tlSrcFromData[1]); + context.scale(sourceResolution, -sourceResolution); + + // the image has to be scaled by half a pixel in every direction + // in order to prevent artifacts between the original tiles + // that are introduced by the canvas antialiasing. + context.drawImage(src.image, -0.5, -0.5, + src.image.width + 1, src.image.height + 1); + context.restore(); + }); + + if (goog.DEBUG) { + context.strokeStyle = 'black'; + context.lineWidth = 2 * pixelSize; + context.beginPath(); + context.moveTo(x0, y0); + context.lineTo(x1, y1); + context.lineTo(x2, y2); + context.closePath(); + context.stroke(); + } + + context.restore(); + }); +}; diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js new file mode 100644 index 0000000000..b63bb62bee --- /dev/null +++ b/src/ol/reproj/tile.js @@ -0,0 +1,265 @@ +goog.provide('ol.reproj.Tile'); + +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('goog.object'); +goog.require('ol.Tile'); +goog.require('ol.TileState'); +goog.require('ol.dom'); +goog.require('ol.extent'); +goog.require('ol.proj'); +goog.require('ol.reproj'); +goog.require('ol.reproj.triangulation'); + + + +/** + * @constructor + * @extends {ol.Tile} + * @param {ol.proj.Projection} sourceProj + * @param {ol.tilegrid.TileGrid} sourceTileGrid + * @param {ol.proj.Projection} targetProj + * @param {ol.tilegrid.TileGrid} targetTileGrid + * @param {number} z + * @param {number} x + * @param {number} y + * @param {number} pixelRatio + * @param {function(number, number, number, number) : ol.Tile} getTileFunction + */ +ol.reproj.Tile = function(sourceProj, sourceTileGrid, + targetProj, targetTileGrid, z, x, y, pixelRatio, getTileFunction) { + goog.base(this, [z, x, y], ol.TileState.IDLE); + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = null; + + /** + * @private + * @type {Object.} + */ + this.canvasByContext_ = {}; + + /** + * @private + * @type {ol.tilegrid.TileGrid} + */ + this.sourceTileGrid_ = sourceTileGrid; + + /** + * @private + * @type {ol.tilegrid.TileGrid} + */ + this.targetTileGrid_ = targetTileGrid; + + var targetExtent = targetTileGrid.getTileCoordExtent(this.getTileCoord()); + var targetResolution = targetTileGrid.getResolution(z); + var transformInv = ol.proj.getTransform(targetProj, sourceProj); + + /** + * @private + * @type {!ol.reproj.Triangulation} + */ + this.triangles_ = ol.reproj.triangulation.createForExtent( + targetExtent, transformInv); + + /** + * @private + * @type {!Array.} + */ + this.srcTiles_ = []; + + /** + * @private + * @type {Array.} + */ + this.sourcesListenerKeys_ = null; + + var idealSourceResolution = + targetProj.getPointResolution(targetResolution, + ol.extent.getCenter(targetExtent)) * + targetProj.getMetersPerUnit() / sourceProj.getMetersPerUnit(); + + /** + * @private + * @type {number} + */ + this.srcZ_ = sourceTileGrid.getZForResolution(idealSourceResolution); + var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangles_); + + if (!ol.extent.intersects(sourceTileGrid.getExtent(), srcExtent)) { + this.state = ol.TileState.EMPTY; + } else { + var srcRange = sourceTileGrid.getTileRangeForExtentAndZ( + srcExtent, this.srcZ_); + + var srcFullRange = sourceTileGrid.getFullTileRange(this.srcZ_); + srcRange.minX = Math.max(srcRange.minX, srcFullRange.minX); + srcRange.maxX = Math.min(srcRange.maxX, srcFullRange.maxX); + srcRange.minY = Math.max(srcRange.minY, srcFullRange.minY); + srcRange.maxY = Math.min(srcRange.maxY, srcFullRange.maxY); + + for (var srcX = srcRange.minX; srcX <= srcRange.maxX; srcX++) { + for (var srcY = srcRange.minY; srcY <= srcRange.maxY; srcY++) { + var tile = getTileFunction(this.srcZ_, srcX, srcY, pixelRatio); + if (tile) { + this.srcTiles_.push(tile); + } + } + } + + if (this.srcTiles_.length === 0) { + this.state = ol.TileState.EMPTY; + } + } +}; +goog.inherits(ol.reproj.Tile, ol.Tile); + + +/** + * @inheritDoc + */ +ol.reproj.Tile.prototype.disposeInternal = function() { + if (this.state == ol.TileState.LOADING) { + this.unlistenSources_(); + } + goog.base(this, 'disposeInternal'); +}; + + +/** + * @inheritDoc + */ +ol.reproj.Tile.prototype.getImage = function(opt_context) { + if (goog.isDef(opt_context)) { + var image; + var key = goog.getUid(opt_context); + if (key in this.canvasByContext_) { + return this.canvasByContext_[key]; + } else if (goog.object.isEmpty(this.canvasByContext_)) { + image = this.canvas_; + } else { + image = /** @type {HTMLCanvasElement} */ (this.canvas_.cloneNode(false)); + } + this.canvasByContext_[key] = image; + return image; + } else { + return this.canvas_; + } +}; + + +/** + * @private + */ +ol.reproj.Tile.prototype.reproject_ = function() { + var sources = []; + goog.array.forEach(this.srcTiles_, function(tile, i, arr) { + if (tile && tile.getState() == ol.TileState.LOADED) { + sources.push({ + extent: this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord), + image: tile.getImage() + }); + } + }, this); + + // create the canvas + var tileCoord = this.getTileCoord(); + var z = tileCoord[0]; + var size = this.targetTileGrid_.getTileSize(z); + var targetResolution = this.targetTileGrid_.getResolution(z); + var srcResolution = this.sourceTileGrid_.getResolution(this.srcZ_); + + var width = goog.isNumber(size) ? size : size[0]; + var height = goog.isNumber(size) ? size : size[1]; + var context = ol.dom.createCanvasContext2D(width, height); + context.imageSmoothingEnabled = true; + + if (goog.DEBUG) { + context.fillStyle = + sources.length === 0 ? 'rgba(255,0,0,.8)' : + (sources.length == 1 ? 'rgba(0,255,0,.3)' : 'rgba(0,0,255,.1)'); + context.fillRect(0, 0, width, height); + } + + if (sources.length > 0) { + var targetExtent = this.targetTileGrid_.getTileCoordExtent(tileCoord); + ol.reproj.renderTriangles(context, srcResolution, targetResolution, + targetExtent, this.triangles_, sources); + } + + this.canvas_ = context.canvas; + this.state = ol.TileState.LOADED; + this.changed(); +}; + + +/** + * @inheritDoc + */ +ol.reproj.Tile.prototype.load = function() { + if (this.state == ol.TileState.IDLE) { + this.state = ol.TileState.LOADING; + this.changed(); + + var leftToLoad = 0; + var onSingleSourceLoaded = goog.bind(function() { + leftToLoad--; + goog.asserts.assert(leftToLoad >= 0, 'leftToLoad should not be negative'); + if (leftToLoad <= 0) { + this.unlistenSources_(); + this.reproject_(); + } + }, this); + + goog.asserts.assert(goog.isNull(this.sourcesListenerKeys_), + 'this.sourcesListenerKeys_ should be null'); + + this.sourcesListenerKeys_ = []; + goog.array.forEach(this.srcTiles_, function(tile, i, arr) { + var state = tile.getState(); + if (state == ol.TileState.IDLE || state == ol.TileState.LOADING) { + leftToLoad++; + + var sourceListenKey; + sourceListenKey = tile.listen(goog.events.EventType.CHANGE, + function(e) { + var state = tile.getState(); + if (state == ol.TileState.LOADED || + state == ol.TileState.ERROR || + state == ol.TileState.EMPTY) { + onSingleSourceLoaded(); + goog.events.unlistenByKey(sourceListenKey); + } + }); + this.sourcesListenerKeys_.push(sourceListenKey); + } + }, this); + + goog.array.forEach(this.srcTiles_, function(tile, i, arr) { + var state = tile.getState(); + if (state == ol.TileState.IDLE) { + tile.load(); + } + }); + + if (leftToLoad === 0) { + this.reproject_(); + } + } +}; + + +/** + * @private + */ +ol.reproj.Tile.prototype.unlistenSources_ = function() { + goog.asserts.assert(!goog.isNull(this.sourcesListenerKeys_), + 'this.sourcesListenerKeys_ should not be null'); + goog.array.forEach(this.sourcesListenerKeys_, goog.events.unlistenByKey); + this.sourcesListenerKeys_ = null; +}; diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js new file mode 100644 index 0000000000..21899cb8b3 --- /dev/null +++ b/src/ol/reproj/triangulation.js @@ -0,0 +1,104 @@ +goog.provide('ol.reproj.Triangulation'); +goog.provide('ol.reproj.triangulation'); + +goog.require('goog.array'); +goog.require('goog.math'); +goog.require('ol.extent'); + + +/** + * Array of triangles, + * each triangles is Array (length=3) of + * projected point pairs (length=2; [src, dst]), + * each point is ol.Coordinate. + * @typedef {Array.>>} + */ +ol.reproj.Triangulation; + + +/** + * Triangulates given extent and reprojects vertices. + * TODO: improved triangulation, better error handling of some trans fails + * @param {ol.Extent} extent + * @param {ol.TransformFunction} transformInv Inverse transform (dst -> src). + * @param {number=} opt_subdiv Subdivision factor (default 4). + * @return {ol.reproj.Triangulation} + */ +ol.reproj.triangulation.createForExtent = function(extent, transformInv, + opt_subdiv) { + var triangulation = []; + + var tlDst = ol.extent.getTopLeft(extent); + var brDst = ol.extent.getBottomRight(extent); + + var projected = {0: {}}; // cache of already transformed values + var subdiv = opt_subdiv || 4; + for (var y = 0; y < subdiv; y++) { + projected[y + 1] = {}; // prepare cache for the next line + for (var x = 0; x < subdiv; x++) { + // do 2 triangle: [(x, y), (x + 1, y + 1), (x, y + 1)] + // [(x, y), (x + 1, y), (x + 1, y + 1)] + + var x0y0dst = [ + goog.math.lerp(tlDst[0], brDst[0], x / subdiv), + goog.math.lerp(tlDst[1], brDst[1], y / subdiv) + ]; + var x1y0dst = [ + goog.math.lerp(tlDst[0], brDst[0], (x + 1) / subdiv), + goog.math.lerp(tlDst[1], brDst[1], y / subdiv) + ]; + var x0y1dst = [ + goog.math.lerp(tlDst[0], brDst[0], x / subdiv), + goog.math.lerp(tlDst[1], brDst[1], (y + 1) / subdiv) + ]; + var x1y1dst = [ + goog.math.lerp(tlDst[0], brDst[0], (x + 1) / subdiv), + goog.math.lerp(tlDst[1], brDst[1], (y + 1) / subdiv) + ]; + + if (!goog.isDef(projected[y][x])) { + projected[y][x] = transformInv(x0y0dst); + } + if (!goog.isDef(projected[y][x + 1])) { + projected[y][x + 1] = transformInv(x1y0dst); + } + if (!goog.isDef(projected[y + 1][x])) { + projected[y + 1][x] = transformInv(x0y1dst); + } + if (!goog.isDef(projected[y + 1][x + 1])) { + projected[y + 1][x + 1] = transformInv(x1y1dst); + } + + triangulation.push( + [ + [projected[y][x], x0y0dst], + [projected[y + 1][x + 1], x1y1dst], + [projected[y + 1][x], x0y1dst] + ], [ + [projected[y][x], x0y0dst], + [projected[y][x + 1], x1y0dst], + [projected[y + 1][x + 1], x1y1dst] + ] + ); + } + } + + return triangulation; +}; + + +/** + * @param {ol.reproj.Triangulation} triangulation + * @return {ol.Extent} + */ +ol.reproj.triangulation.getSourceExtent = function(triangulation) { + var extent = ol.extent.createEmpty(); + + goog.array.forEach(triangulation, function(triangle, i, arr) { + ol.extent.extendCoordinate(extent, triangle[0][0]); + ol.extent.extendCoordinate(extent, triangle[1][0]); + ol.extent.extendCoordinate(extent, triangle[2][0]); + }); + + return extent; +}; From c425b9c0e6f70accc72b3f088fa7d9abd9ef986e Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 28 May 2015 15:52:04 +0200 Subject: [PATCH 03/80] Verify triangulation against source/target extents If we do this here, we can avoid some computations on triangles that will be unused in the future anyway + reduce problems with non-global projections without specified extents. --- src/ol/reproj/triangulation.js | 76 ++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 21899cb8b3..d992c77294 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -16,25 +16,67 @@ goog.require('ol.extent'); ol.reproj.Triangulation; +/** + * Adds triangle to the triangulation (and reprojects the vertices) if valid. + * @private + * @param {ol.reproj.Triangulation} triangulation + * @param {ol.Coordinate} a + * @param {ol.Coordinate} b + * @param {ol.Coordinate} c + * @param {ol.TransformFunction} transformInv Inverse transform (dst -> src). + * @param {ol.Extent=} opt_maxTargetExtent + * @param {ol.Extent=} opt_maxSourceExtent + */ +ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, + transformInv, opt_maxTargetExtent, opt_maxSourceExtent) { + if (goog.isDefAndNotNull(opt_maxTargetExtent)) { + if (!ol.extent.containsCoordinate(opt_maxTargetExtent, a) && + !ol.extent.containsCoordinate(opt_maxTargetExtent, b) && + !ol.extent.containsCoordinate(opt_maxTargetExtent, c)) { + // whole triangle outside target projection extent -> ignore + return; + } + // clamp the vertices to the extent edges before transforming + a = ol.extent.closestCoordinate(opt_maxTargetExtent, a); + b = ol.extent.closestCoordinate(opt_maxTargetExtent, b); + c = ol.extent.closestCoordinate(opt_maxTargetExtent, c); + } + var aSrc = transformInv(a); + var bSrc = transformInv(b); + var cSrc = transformInv(c); + if (goog.isDefAndNotNull(opt_maxSourceExtent)) { + var srcTriangleExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc]); + if (!ol.extent.intersects(srcTriangleExtent, opt_maxSourceExtent)) { + // whole triangle outside source projection extent -> ignore + return; + } + } + triangulation.push([[aSrc, a], [bSrc, b], [cSrc, c]]); +}; + + /** * Triangulates given extent and reprojects vertices. * TODO: improved triangulation, better error handling of some trans fails * @param {ol.Extent} extent * @param {ol.TransformFunction} transformInv Inverse transform (dst -> src). + * @param {ol.Extent=} opt_maxTargetExtent + * @param {ol.Extent=} opt_maxSourceExtent * @param {number=} opt_subdiv Subdivision factor (default 4). * @return {ol.reproj.Triangulation} */ ol.reproj.triangulation.createForExtent = function(extent, transformInv, + opt_maxTargetExtent, + opt_maxSourceExtent, opt_subdiv) { + var triangulation = []; var tlDst = ol.extent.getTopLeft(extent); var brDst = ol.extent.getBottomRight(extent); - var projected = {0: {}}; // cache of already transformed values var subdiv = opt_subdiv || 4; for (var y = 0; y < subdiv; y++) { - projected[y + 1] = {}; // prepare cache for the next line for (var x = 0; x < subdiv; x++) { // do 2 triangle: [(x, y), (x + 1, y + 1), (x, y + 1)] // [(x, y), (x + 1, y), (x + 1, y + 1)] @@ -56,30 +98,12 @@ ol.reproj.triangulation.createForExtent = function(extent, transformInv, goog.math.lerp(tlDst[1], brDst[1], (y + 1) / subdiv) ]; - if (!goog.isDef(projected[y][x])) { - projected[y][x] = transformInv(x0y0dst); - } - if (!goog.isDef(projected[y][x + 1])) { - projected[y][x + 1] = transformInv(x1y0dst); - } - if (!goog.isDef(projected[y + 1][x])) { - projected[y + 1][x] = transformInv(x0y1dst); - } - if (!goog.isDef(projected[y + 1][x + 1])) { - projected[y + 1][x + 1] = transformInv(x1y1dst); - } - - triangulation.push( - [ - [projected[y][x], x0y0dst], - [projected[y + 1][x + 1], x1y1dst], - [projected[y + 1][x], x0y1dst] - ], [ - [projected[y][x], x0y0dst], - [projected[y][x + 1], x1y0dst], - [projected[y + 1][x + 1], x1y1dst] - ] - ); + ol.reproj.triangulation.addTriangleIfValid_( + triangulation, x0y0dst, x1y1dst, x0y1dst, + transformInv, opt_maxTargetExtent, opt_maxSourceExtent); + ol.reproj.triangulation.addTriangleIfValid_( + triangulation, x0y0dst, x1y0dst, x1y1dst, + transformInv, opt_maxTargetExtent, opt_maxSourceExtent); } } From 2cc2027353a953bfe77a998d56e40f3060bf449c Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Tue, 9 Jun 2015 15:47:58 +0200 Subject: [PATCH 04/80] More precise and robust way of calculating source resolution --- src/ol/reproj/reproj.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index b82f511444..25da89ad1e 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -1,8 +1,48 @@ goog.provide('ol.reproj'); goog.require('goog.array'); +goog.require('goog.math'); goog.require('ol.extent'); goog.require('ol.math'); +goog.require('ol.proj'); + + +/** + * Calculates ideal resolution to use from the source in order to achieve + * pixel mapping as close as possible to 1:1 during reprojection. + * The resolution is calculated regardless on what resolutions + * are actually available in the dataset (TileGrid, Image, ...). + * + * @param {ol.proj.Projection} sourceProj + * @param {ol.proj.Projection} targetProj + * @param {ol.Coordinate} targetCenter + * @param {number} targetResolution + * @return {number} The best resolution to use. Can be +-Infinity, NaN or 0. + */ +ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, + targetCenter, targetResolution) { + + var sourceCenter = ol.proj.transform(targetCenter, targetProj, sourceProj); + + // calculate the ideal resolution of the source data + var sourceResolution = + targetProj.getPointResolution(targetResolution, targetCenter) * + targetProj.getMetersPerUnit() / sourceProj.getMetersPerUnit(); + + // based on the projection properties, the point resolution at the specified + // coordinates may be slightly different. We need to reverse-compensate this + // in order to achieve optimal results. + + var compensationFactor = + sourceProj.getPointResolution(sourceResolution, sourceCenter) / + sourceResolution; + + if (goog.math.isFiniteNumber(compensationFactor) && compensationFactor > 0) { + sourceResolution /= compensationFactor; + } + + return sourceResolution; +}; /** From fcffce46b44f156bdb8a350c30cfe26560164082 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 28 May 2015 15:57:59 +0200 Subject: [PATCH 05/80] More robust way of handling non-global projections If the target projection has specified extent, it is respected. Also adds various checks to optimize performance and/or prevent potential errors. --- src/ol/reproj/image.js | 14 +++++----- src/ol/reproj/tile.js | 58 +++++++++++++++++++++++++++++++++--------- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index ed4a67f0bd..0a75c9b174 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -49,13 +49,16 @@ ol.reproj.Image = function(sourceProj, targetProj, this.canvas_ = this.context_.canvas; var transformInv = ol.proj.getTransform(targetProj, sourceProj); + var maxTargetExtent = targetProj.getExtent(); + var maxSourceExtent = sourceProj.getExtent(); /** * @private * @type {!ol.reproj.Triangulation} */ this.triangles_ = ol.reproj.triangulation.createForExtent( - targetExtent, transformInv); + targetExtent, transformInv, + maxTargetExtent, maxSourceExtent); /** * @private @@ -71,16 +74,15 @@ ol.reproj.Image = function(sourceProj, targetProj, var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangles_); - var idealSourceResolution = - targetProj.getPointResolution(targetResolution, - ol.extent.getCenter(targetExtent)) * - targetProj.getMetersPerUnit() / sourceProj.getMetersPerUnit(); + var targetCenter = ol.extent.getCenter(targetExtent); + var sourceResolution = ol.reproj.calculateSourceResolution( + sourceProj, targetProj, targetCenter, targetResolution); /** * @private * @type {ol.ImageBase} */ - this.srcImage_ = getImageFunction(srcExtent, idealSourceResolution, + this.srcImage_ = getImageFunction(srcExtent, sourceResolution, pixelRatio, sourceProj); /** diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index b63bb62bee..8ba73b0e47 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -4,6 +4,7 @@ goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.events'); goog.require('goog.events.EventType'); +goog.require('goog.math'); goog.require('goog.object'); goog.require('ol.Tile'); goog.require('ol.TileState'); @@ -56,16 +57,11 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, */ this.targetTileGrid_ = targetTileGrid; - var targetExtent = targetTileGrid.getTileCoordExtent(this.getTileCoord()); - var targetResolution = targetTileGrid.getResolution(z); - var transformInv = ol.proj.getTransform(targetProj, sourceProj); - /** * @private * @type {!ol.reproj.Triangulation} */ - this.triangles_ = ol.reproj.triangulation.createForExtent( - targetExtent, transformInv); + this.triangles_ = []; /** * @private @@ -79,18 +75,48 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, */ this.sourcesListenerKeys_ = null; - var idealSourceResolution = - targetProj.getPointResolution(targetResolution, - ol.extent.getCenter(targetExtent)) * - targetProj.getMetersPerUnit() / sourceProj.getMetersPerUnit(); - /** * @private * @type {number} */ - this.srcZ_ = sourceTileGrid.getZForResolution(idealSourceResolution); + this.srcZ_ = 0; + + + var targetExtent = targetTileGrid.getTileCoordExtent(this.getTileCoord()); + var maxTargetExtent = this.targetTileGrid_.getExtent(); + var maxSourceExtent = this.sourceTileGrid_.getExtent(); + + if (!ol.extent.intersects(maxTargetExtent, targetExtent)) { + // Tile is completely outside range -> EMPTY + // TODO: is it actually correct that the source even creates the tile ? + this.state = ol.TileState.EMPTY; + //return; + } + + var transformInv = ol.proj.getTransform(targetProj, sourceProj); + this.triangles_ = ol.reproj.triangulation.createForExtent( + targetExtent, transformInv, maxTargetExtent, maxSourceExtent); + + var targetCenter = ol.extent.getCenter(targetExtent); + var targetResolution = targetTileGrid.getResolution(z); + var sourceResolution = ol.reproj.calculateSourceResolution( + sourceProj, targetProj, targetCenter, targetResolution); + + if (!goog.math.isFiniteNumber(sourceResolution) || sourceResolution <= 0) { + // invalid sourceResolution -> EMPTY + // probably edges of the projections when no extent is defined + this.state = ol.TileState.EMPTY; + return; + } + + this.srcZ_ = sourceTileGrid.getZForResolution(sourceResolution); var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangles_); + var sourceProjExtent = sourceProj.getExtent(); + if (sourceProjExtent) { + srcExtent = ol.extent.getIntersection(srcExtent, sourceProjExtent); + } + if (!ol.extent.intersects(sourceTileGrid.getExtent(), srcExtent)) { this.state = ol.TileState.EMPTY; } else { @@ -103,6 +129,14 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, srcRange.minY = Math.max(srcRange.minY, srcFullRange.minY); srcRange.maxY = Math.min(srcRange.maxY, srcFullRange.maxY); + if (srcRange.getWidth() * srcRange.getHeight() > 100) { + // Too many source tiles are needed -- something probably went wrong + // This sometimes happens for certain non-global projections + // if no extent is specified. + // TODO: detect somehow better? or at least make this a define + this.state = ol.TileState.ERROR; + return; + } for (var srcX = srcRange.minX; srcX <= srcRange.maxX; srcX++) { for (var srcY = srcRange.minY; srcY <= srcRange.maxY; srcY++) { var tile = getTileFunction(this.srcZ_, srcX, srcY, pixelRatio); From ebc3f24671d0259949ac797c76b54244615bd1fc Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 1 Jun 2015 17:48:17 +0200 Subject: [PATCH 06/80] Clamp the triangulation vertices if partially outside source extent --- src/ol/reproj/image.js | 3 +-- src/ol/reproj/tile.js | 4 ++-- src/ol/reproj/triangulation.js | 34 ++++++++++++++++++++++++++-------- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index 0a75c9b174..17d03e7b89 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -48,7 +48,6 @@ ol.reproj.Image = function(sourceProj, targetProj, */ this.canvas_ = this.context_.canvas; - var transformInv = ol.proj.getTransform(targetProj, sourceProj); var maxTargetExtent = targetProj.getExtent(); var maxSourceExtent = sourceProj.getExtent(); @@ -57,7 +56,7 @@ ol.reproj.Image = function(sourceProj, targetProj, * @type {!ol.reproj.Triangulation} */ this.triangles_ = ol.reproj.triangulation.createForExtent( - targetExtent, transformInv, + targetExtent, sourceProj, targetProj, maxTargetExtent, maxSourceExtent); /** diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index 8ba73b0e47..efe4c41d17 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -93,9 +93,9 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, //return; } - var transformInv = ol.proj.getTransform(targetProj, sourceProj); this.triangles_ = ol.reproj.triangulation.createForExtent( - targetExtent, transformInv, maxTargetExtent, maxSourceExtent); + targetExtent, sourceProj, targetProj, + maxTargetExtent, maxSourceExtent); var targetCenter = ol.extent.getCenter(targetExtent); var targetResolution = targetTileGrid.getResolution(z); diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index d992c77294..aaecb83684 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -4,6 +4,7 @@ goog.provide('ol.reproj.triangulation'); goog.require('goog.array'); goog.require('goog.math'); goog.require('ol.extent'); +goog.require('ol.proj'); /** @@ -23,12 +24,13 @@ ol.reproj.Triangulation; * @param {ol.Coordinate} a * @param {ol.Coordinate} b * @param {ol.Coordinate} c + * @param {ol.TransformFunction} transformFwd Forward transform (src -> dst). * @param {ol.TransformFunction} transformInv Inverse transform (dst -> src). * @param {ol.Extent=} opt_maxTargetExtent * @param {ol.Extent=} opt_maxSourceExtent */ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, - transformInv, opt_maxTargetExtent, opt_maxSourceExtent) { + transformFwd, transformInv, opt_maxTargetExtent, opt_maxSourceExtent) { if (goog.isDefAndNotNull(opt_maxTargetExtent)) { if (!ol.extent.containsCoordinate(opt_maxTargetExtent, a) && !ol.extent.containsCoordinate(opt_maxTargetExtent, b) && @@ -48,8 +50,22 @@ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, var srcTriangleExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc]); if (!ol.extent.intersects(srcTriangleExtent, opt_maxSourceExtent)) { // whole triangle outside source projection extent -> ignore + // TODO: intersect triangle with the extent rather than bbox ? return; } + if (!ol.extent.containsCoordinate(opt_maxSourceExtent, aSrc) || + !ol.extent.containsCoordinate(opt_maxSourceExtent, bSrc) || + !ol.extent.containsCoordinate(opt_maxSourceExtent, cSrc)) { + // if any vertex is outside projection range, modify the target triangle + // TODO: do not do closestCoordinate, but rather scale the triangle with + // respect to a point inside the extent + aSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, aSrc); + bSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, bSrc); + cSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, cSrc); + a = transformFwd(aSrc); + b = transformFwd(bSrc); + c = transformFwd(cSrc); + } } triangulation.push([[aSrc, a], [bSrc, b], [cSrc, c]]); }; @@ -59,16 +75,18 @@ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, * Triangulates given extent and reprojects vertices. * TODO: improved triangulation, better error handling of some trans fails * @param {ol.Extent} extent - * @param {ol.TransformFunction} transformInv Inverse transform (dst -> src). + * @param {ol.proj.Projection} sourceProj + * @param {ol.proj.Projection} targetProj * @param {ol.Extent=} opt_maxTargetExtent * @param {ol.Extent=} opt_maxSourceExtent * @param {number=} opt_subdiv Subdivision factor (default 4). * @return {ol.reproj.Triangulation} */ -ol.reproj.triangulation.createForExtent = function(extent, transformInv, - opt_maxTargetExtent, - opt_maxSourceExtent, - opt_subdiv) { +ol.reproj.triangulation.createForExtent = function(extent, sourceProj, + targetProj, opt_maxTargetExtent, opt_maxSourceExtent, opt_subdiv) { + + var transformFwd = ol.proj.getTransform(sourceProj, targetProj); + var transformInv = ol.proj.getTransform(targetProj, sourceProj); var triangulation = []; @@ -100,10 +118,10 @@ ol.reproj.triangulation.createForExtent = function(extent, transformInv, ol.reproj.triangulation.addTriangleIfValid_( triangulation, x0y0dst, x1y1dst, x0y1dst, - transformInv, opt_maxTargetExtent, opt_maxSourceExtent); + transformFwd, transformInv, opt_maxTargetExtent, opt_maxSourceExtent); ol.reproj.triangulation.addTriangleIfValid_( triangulation, x0y0dst, x1y0dst, x1y1dst, - transformInv, opt_maxTargetExtent, opt_maxSourceExtent); + transformFwd, transformInv, opt_maxTargetExtent, opt_maxSourceExtent); } } From b221e1ac1f7e3bf954676c207a2715cadd0da75f Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 1 Jun 2015 19:06:56 +0200 Subject: [PATCH 07/80] Basic integration of raster reprojection for tiled sources To allow for easier testing and debugging --- src/ol/source/tileimagesource.js | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index fa92f0671d..daee938631 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -4,11 +4,13 @@ goog.require('goog.asserts'); goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('ol.ImageTile'); +goog.require('ol.TileCache'); goog.require('ol.TileCoord'); goog.require('ol.TileLoadFunctionType'); goog.require('ol.TileState'); goog.require('ol.TileUrlFunction'); goog.require('ol.TileUrlFunctionType'); +goog.require('ol.reproj.Tile'); goog.require('ol.source.Tile'); goog.require('ol.source.TileEvent'); @@ -69,6 +71,17 @@ ol.source.TileImage = function(options) { this.tileClass = options.tileClass !== undefined ? options.tileClass : ol.ImageTile; + /** + * @protected + * @type {ol.TileCache} + */ + this.reprojectedTileCache = new ol.TileCache(); + + /** + * @protected + * @type {ol.proj.Projection} + */ + this.lastProj = null; }; goog.inherits(ol.source.TileImage, ol.source.Tile); @@ -82,11 +95,64 @@ ol.source.TileImage.defaultTileLoadFunction = function(imageTile, src) { }; +/** + * @inheritDoc + */ +ol.source.TileImage.prototype.getTileGridForProjection = function(projection) { + if (projection == this.getProjection() && !goog.isNull(this.tileGrid)) { + return this.tileGrid; + } else { + return ol.tilegrid.getForProjection(projection); + } +}; + + /** * @inheritDoc */ ol.source.TileImage.prototype.getTile = function(z, x, y, pixelRatio, projection) { + if (goog.isNull(this.getProjection()) || this.getProjection() == projection) { + return this.getTileInternal(z, x, y, pixelRatio, projection); + } else { + if (this.lastProj != projection) { + this.reprojectedTileCache.clear(); + this.lastProj = projection; + } + var cache = this.reprojectedTileCache; //TODO: per-projection cache + var tileCoordKey = this.getKeyZXY(z, x, y); + if (cache.containsKey(tileCoordKey)) { + return /** @type {!ol.Tile} */(cache.get(tileCoordKey)); + } else { + var sourceProjection = this.getProjection(); + var sourceTileGrid = this.getTileGridForProjection(sourceProjection); + var targetTileGrid = ol.tilegrid.getForProjection(projection); + //TODO: init cache / tilegrid if needed + var tile = new ol.reproj.Tile( + sourceProjection, sourceTileGrid, + projection, targetTileGrid, + z, x, y, pixelRatio, goog.bind(function(z, x, y, pixelRatio) { + return this.getTileInternal(z, x, y, pixelRatio, sourceProjection); + }, this)); + + cache.set(tileCoordKey, tile); + return tile; + } + } +}; + + +/** + * @param {number} z Tile coordinate z. + * @param {number} x Tile coordinate x. + * @param {number} y Tile coordinate y. + * @param {number} pixelRatio Pixel ratio. + * @param {ol.proj.Projection} projection Projection. + * @return {!ol.Tile} Tile. + * @protected + */ +ol.source.TileImage.prototype.getTileInternal = + function(z, x, y, pixelRatio, projection) { var tileCoordKey = this.getKeyZXY(z, x, y); if (this.tileCache.containsKey(tileCoordKey)) { return /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey)); From ac7698944743ee3e9a86bda717241793fd970452 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Tue, 2 Jun 2015 21:39:40 +0200 Subject: [PATCH 08/80] Detect and handle triangles (tiles) crossing the dateline (projection edge) --- src/ol/reproj/reproj.js | 37 ++++++++++++--- src/ol/reproj/tile.js | 32 ++++++++++--- src/ol/reproj/triangulation.js | 87 ++++++++++++++++++++++++++++------ 3 files changed, 129 insertions(+), 27 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 25da89ad1e..3d5bfbf909 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -57,6 +57,17 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, */ ol.reproj.renderTriangles = function(context, sourceResolution, targetResolution, targetExtent, triangulation, sources) { + + var renderImage = function(image) { + context.scale(sourceResolution, -sourceResolution); + + // the image has to be scaled by half a pixel in every direction + // in order to prevent artifacts between the original tiles + // that are introduced by the canvas antialiasing. + context.drawImage(image, -0.5, -0.5, + image.width + 1, image.height + 1); + }; + goog.array.forEach(triangulation, function(tri, i, arr) { context.save(); @@ -88,6 +99,11 @@ ol.reproj.renderTriangles = function(context, var u0 = tri[0][1][0] - targetTL[0], v0 = -(tri[0][1][1] - targetTL[1]), u1 = tri[1][1][0] - targetTL[0], v1 = -(tri[1][1][1] - targetTL[1]), u2 = tri[2][1][0] - targetTL[0], v2 = -(tri[2][1][1] - targetTL[1]); + if (tri.shiftDistance) { + x0 = goog.math.modulo(x0 + tri.shiftDistance, tri.shiftDistance); + x1 = goog.math.modulo(x1 + tri.shiftDistance, tri.shiftDistance); + x2 = goog.math.modulo(x2 + tri.shiftDistance, tri.shiftDistance); + } var augmentedMatrix = [ [x0, y0, 1, 0, 0, 0, u0 / targetResolution], [x1, y1, 1, 0, 0, 0, u1 / targetResolution], @@ -133,13 +149,22 @@ ol.reproj.renderTriangles = function(context, context.save(); var tlSrcFromData = ol.extent.getTopLeft(src.extent); context.translate(tlSrcFromData[0], tlSrcFromData[1]); - context.scale(sourceResolution, -sourceResolution); + if (tri.shiftDistance) { + context.save(); + context.translate(tri.shiftDistance, 0); + renderImage(src.image); + context.restore(); + renderImage(src.image); - // the image has to be scaled by half a pixel in every direction - // in order to prevent artifacts between the original tiles - // that are introduced by the canvas antialiasing. - context.drawImage(src.image, -0.5, -0.5, - src.image.width + 1, src.image.height + 1); + if (goog.DEBUG) { + context.fillStyle = + sources.length > 16 ? 'rgba(255,0,0,1)' : + (sources.length > 4 ? 'rgba(0,255,0,.3)' : 'rgba(0,0,255,.1)'); + context.fillRect(0, 0, 256, 256); + } + } else { + renderImage(src.image); + } context.restore(); }); diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index efe4c41d17..f9e6d3ccbf 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -97,6 +97,12 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, targetExtent, sourceProj, targetProj, maxTargetExtent, maxSourceExtent); + if (this.triangles_.length === 0) { + // no valid triangles -> EMPTY + this.state = ol.TileState.EMPTY; + return; + } + var targetCenter = ol.extent.getCenter(targetExtent); var targetResolution = targetTileGrid.getResolution(z); var sourceResolution = ol.reproj.calculateSourceResolution( @@ -113,23 +119,35 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangles_); var sourceProjExtent = sourceProj.getExtent(); - if (sourceProjExtent) { + if (!sourceProj.isGlobal() && sourceProjExtent) { srcExtent = ol.extent.getIntersection(srcExtent, sourceProjExtent); } - if (!ol.extent.intersects(sourceTileGrid.getExtent(), srcExtent)) { + if (!goog.isNull(maxSourceExtent) && + !ol.extent.intersects(maxSourceExtent, srcExtent)) { this.state = ol.TileState.EMPTY; } else { var srcRange = sourceTileGrid.getTileRangeForExtentAndZ( srcExtent, this.srcZ_); var srcFullRange = sourceTileGrid.getFullTileRange(this.srcZ_); - srcRange.minX = Math.max(srcRange.minX, srcFullRange.minX); - srcRange.maxX = Math.min(srcRange.maxX, srcFullRange.maxX); srcRange.minY = Math.max(srcRange.minY, srcFullRange.minY); srcRange.maxY = Math.min(srcRange.maxY, srcFullRange.maxY); - if (srcRange.getWidth() * srcRange.getHeight() > 100) { + var xRange; + if (srcRange.minX > srcRange.maxX) { + xRange = goog.array.concat( + goog.array.range(srcRange.minX, srcFullRange.maxX + 1), + goog.array.range(srcFullRange.minX, srcRange.maxX + 1) + ); + } else { + xRange = goog.array.range( + Math.max(srcRange.minX, srcFullRange.minX), + Math.min(srcRange.maxX, srcFullRange.maxX) + 1 + ); + } + + if (xRange.length * srcRange.getHeight() > 100) { // Too many source tiles are needed -- something probably went wrong // This sometimes happens for certain non-global projections // if no extent is specified. @@ -137,14 +155,14 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, this.state = ol.TileState.ERROR; return; } - for (var srcX = srcRange.minX; srcX <= srcRange.maxX; srcX++) { + goog.array.forEach(xRange, function(srcX, i, arr) { for (var srcY = srcRange.minY; srcY <= srcRange.maxY; srcY++) { var tile = getTileFunction(this.srcZ_, srcX, srcY, pixelRatio); if (tile) { this.srcTiles_.push(tile); } } - } + }, this); if (this.srcTiles_.length === 0) { this.state = ol.TileState.EMPTY; diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index aaecb83684..786cddee60 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -24,13 +24,13 @@ ol.reproj.Triangulation; * @param {ol.Coordinate} a * @param {ol.Coordinate} b * @param {ol.Coordinate} c - * @param {ol.TransformFunction} transformFwd Forward transform (src -> dst). - * @param {ol.TransformFunction} transformInv Inverse transform (dst -> src). + * @param {ol.proj.Projection} sourceProj + * @param {ol.proj.Projection} targetProj * @param {ol.Extent=} opt_maxTargetExtent * @param {ol.Extent=} opt_maxSourceExtent */ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, - transformFwd, transformInv, opt_maxTargetExtent, opt_maxSourceExtent) { + sourceProj, targetProj, opt_maxTargetExtent, opt_maxSourceExtent) { if (goog.isDefAndNotNull(opt_maxTargetExtent)) { if (!ol.extent.containsCoordinate(opt_maxTargetExtent, a) && !ol.extent.containsCoordinate(opt_maxTargetExtent, b) && @@ -43,6 +43,7 @@ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, b = ol.extent.closestCoordinate(opt_maxTargetExtent, b); c = ol.extent.closestCoordinate(opt_maxTargetExtent, c); } + var transformInv = ol.proj.getTransform(targetProj, sourceProj); var aSrc = transformInv(a); var bSrc = transformInv(b); var cSrc = transformInv(c); @@ -62,12 +63,56 @@ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, aSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, aSrc); bSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, bSrc); cSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, cSrc); + var transformFwd = ol.proj.getTransform(sourceProj, targetProj); a = transformFwd(aSrc); b = transformFwd(bSrc); c = transformFwd(cSrc); } } - triangulation.push([[aSrc, a], [bSrc, b], [cSrc, c]]); + var shiftDistance = 0; + if (sourceProj.isGlobal()) { + // determine if the triangle crosses the dateline here + // This can be detected by transforming centroid of the target triangle. + // If the transformed centroid is outside the transformed triangle, + // the triangle wraps around projection extent. + // In such case, the + + var srcExtent = ol.extent.createEmpty(); + ol.extent.extendCoordinate(srcExtent, aSrc); + ol.extent.extendCoordinate(srcExtent, bSrc); + ol.extent.extendCoordinate(srcExtent, cSrc); + + var centroid = [(a[0] + b[0] + c[0]) / 3, + (a[1] + b[1] + c[1]) / 3]; + var centroidSrc = transformInv(centroid); + + var pInTriangle = function(p, p0, p1, p2) { + //TODO: move somewhere else + var A = (-p1[1] * p2[0] + p0[1] * (-p1[0] + p2[0]) + + p0[0] * (p1[1] - p2[1]) + p1[0] * p2[1]) / 2; + var sign = A < 0 ? -1 : 1; + var s = (p0[1] * p2[0] - p0[0] * p2[1] + + (p2[1] - p0[1]) * p[0] + + (p0[0] - p2[0]) * p[1]) * sign; + var t = (p0[0] * p1[1] - p0[1] * p1[0] + + (p0[1] - p1[1]) * p[0] + + (p1[0] - p0[0]) * p[1]) * sign; + + return s > 0 && t > 0 && (s + t) < 2 * A * sign; + }; + + if (!pInTriangle(centroidSrc, aSrc, bSrc, cSrc)) { + var sourceProjExtent = sourceProj.getExtent(); + shiftDistance = ol.extent.getWidth(sourceProjExtent); + } + } + var tri = [[aSrc, a], [bSrc, b], [cSrc, c]]; + // TODO: typing ! do not add properties to arrays ! + tri.shiftDistance = shiftDistance; + if (shiftDistance) { + triangulation.shiftDistance = shiftDistance; + } + triangulation.push(tri); }; @@ -85,9 +130,6 @@ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, ol.reproj.triangulation.createForExtent = function(extent, sourceProj, targetProj, opt_maxTargetExtent, opt_maxSourceExtent, opt_subdiv) { - var transformFwd = ol.proj.getTransform(sourceProj, targetProj); - var transformInv = ol.proj.getTransform(targetProj, sourceProj); - var triangulation = []; var tlDst = ol.extent.getTopLeft(extent); @@ -118,10 +160,10 @@ ol.reproj.triangulation.createForExtent = function(extent, sourceProj, ol.reproj.triangulation.addTriangleIfValid_( triangulation, x0y0dst, x1y1dst, x0y1dst, - transformFwd, transformInv, opt_maxTargetExtent, opt_maxSourceExtent); + sourceProj, targetProj, opt_maxTargetExtent, opt_maxSourceExtent); ol.reproj.triangulation.addTriangleIfValid_( triangulation, x0y0dst, x1y0dst, x1y1dst, - transformFwd, transformInv, opt_maxTargetExtent, opt_maxSourceExtent); + sourceProj, targetProj, opt_maxTargetExtent, opt_maxSourceExtent); } } @@ -136,11 +178,28 @@ ol.reproj.triangulation.createForExtent = function(extent, sourceProj, ol.reproj.triangulation.getSourceExtent = function(triangulation) { var extent = ol.extent.createEmpty(); - goog.array.forEach(triangulation, function(triangle, i, arr) { - ol.extent.extendCoordinate(extent, triangle[0][0]); - ol.extent.extendCoordinate(extent, triangle[1][0]); - ol.extent.extendCoordinate(extent, triangle[2][0]); - }); + if (triangulation.shiftDistance) { + var shiftDistance = triangulation.shiftDistance; + goog.array.forEach(triangulation, function(triangle, i, arr) { + ol.extent.extendCoordinate(extent, + [goog.math.modulo(triangle[0][0][0] + shiftDistance, shiftDistance), + triangle[0][0][1]]); + ol.extent.extendCoordinate(extent, + [goog.math.modulo(triangle[1][0][0] + shiftDistance, shiftDistance), + triangle[1][0][1]]); + ol.extent.extendCoordinate(extent, + [goog.math.modulo(triangle[2][0][0] + shiftDistance, shiftDistance), + triangle[2][0][1]]); + }); + if (extent[0] > shiftDistance / 2) extent[0] -= shiftDistance; + if (extent[2] > shiftDistance / 2) extent[2] -= shiftDistance; + } else { + goog.array.forEach(triangulation, function(triangle, i, arr) { + ol.extent.extendCoordinate(extent, triangle[0][0]); + ol.extent.extendCoordinate(extent, triangle[1][0]); + ol.extent.extendCoordinate(extent, triangle[2][0]); + }); + } return extent; }; From e14bede9e9bd5bdd73c8338bc14a15584c0e9ca0 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 3 Jun 2015 16:07:11 +0200 Subject: [PATCH 09/80] Make projection parameter of ol.source.Tile#getTile no longer optional --- src/ol/source/tilesource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index 111fb6cf18..5bf8116f75 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -174,7 +174,7 @@ ol.source.Tile.prototype.getResolutions = function() { * @param {number} x Tile coordinate x. * @param {number} y Tile coordinate y. * @param {number} pixelRatio Pixel ratio. - * @param {ol.proj.Projection=} opt_projection Projection. + * @param {ol.proj.Projection} projection Projection. * @return {!ol.Tile} Tile. */ ol.source.Tile.prototype.getTile = goog.abstractMethod; From bf65b8a4fefd4d0c4c430a9188895a405a94df81 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 3 Jun 2015 15:52:02 +0200 Subject: [PATCH 10/80] Stronger typing of triangles and triangulation --- src/ol/reproj/reproj.js | 29 +++++----- src/ol/reproj/triangulation.js | 99 ++++++++++++++++------------------ 2 files changed, 62 insertions(+), 66 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 3d5bfbf909..6fee71094b 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -68,7 +68,9 @@ ol.reproj.renderTriangles = function(context, image.width + 1, image.height + 1); }; - goog.array.forEach(triangulation, function(tri, i, arr) { + var shiftDistance = triangulation.shiftDistance; + + goog.array.forEach(triangulation.triangles, function(tri, i, arr) { context.save(); var targetTL = ol.extent.getTopLeft(targetExtent); @@ -93,16 +95,17 @@ ol.reproj.renderTriangles = function(context, * | 0 0 0 x1 y1 1 | |a11| |v1| * | 0 0 0 x2 y2 1 | |a12| |v2| */ - var x0 = tri[0][0][0], y0 = tri[0][0][1], - x1 = tri[1][0][0], y1 = tri[1][0][1], - x2 = tri[2][0][0], y2 = tri[2][0][1]; - var u0 = tri[0][1][0] - targetTL[0], v0 = -(tri[0][1][1] - targetTL[1]), - u1 = tri[1][1][0] - targetTL[0], v1 = -(tri[1][1][1] - targetTL[1]), - u2 = tri[2][1][0] - targetTL[0], v2 = -(tri[2][1][1] - targetTL[1]); - if (tri.shiftDistance) { - x0 = goog.math.modulo(x0 + tri.shiftDistance, tri.shiftDistance); - x1 = goog.math.modulo(x1 + tri.shiftDistance, tri.shiftDistance); - x2 = goog.math.modulo(x2 + tri.shiftDistance, tri.shiftDistance); + var src = tri.source, tgt = tri.target; + var x0 = src[0][0], y0 = src[0][1], + x1 = src[1][0], y1 = src[1][1], + x2 = src[2][0], y2 = src[2][1]; + var u0 = tgt[0][0] - targetTL[0], v0 = -(tgt[0][1] - targetTL[1]), + u1 = tgt[1][0] - targetTL[0], v1 = -(tgt[1][1] - targetTL[1]), + u2 = tgt[2][0] - targetTL[0], v2 = -(tgt[2][1] - targetTL[1]); + if (tri.needsShift) { + x0 = goog.math.modulo(x0 + shiftDistance, shiftDistance); + x1 = goog.math.modulo(x1 + shiftDistance, shiftDistance); + x2 = goog.math.modulo(x2 + shiftDistance, shiftDistance); } var augmentedMatrix = [ [x0, y0, 1, 0, 0, 0, u0 / targetResolution], @@ -149,9 +152,9 @@ ol.reproj.renderTriangles = function(context, context.save(); var tlSrcFromData = ol.extent.getTopLeft(src.extent); context.translate(tlSrcFromData[0], tlSrcFromData[1]); - if (tri.shiftDistance) { + if (tri.needsShift) { context.save(); - context.translate(tri.shiftDistance, 0); + context.translate(shiftDistance, 0); renderImage(src.image); context.restore(); renderImage(src.image); diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 786cddee60..07186240ea 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -3,16 +3,27 @@ goog.provide('ol.reproj.triangulation'); goog.require('goog.array'); goog.require('goog.math'); +goog.require('ol.coordinate'); goog.require('ol.extent'); goog.require('ol.proj'); /** - * Array of triangles, - * each triangles is Array (length=3) of - * projected point pairs (length=2; [src, dst]), - * each point is ol.Coordinate. - * @typedef {Array.>>} + * Single triangle; consists of 3 source points and 3 target points. + * `needsShift` can be used to indicate that the whole triangle has to be + * shifted during reprojection. This is needed for triangles crossing edges + * of the source projection (dateline). + * + * @typedef {{source: Array., + * target: Array., + * needsShift: boolean}} + */ +ol.reproj.Triangle; + + +/** + * @typedef {{triangles: Array., + * shiftDistance: number}} */ ol.reproj.Triangulation; @@ -69,50 +80,30 @@ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, c = transformFwd(cSrc); } } - var shiftDistance = 0; - if (sourceProj.isGlobal()) { + var needsShift = false; + if (sourceProj.canWrapX()) { // determine if the triangle crosses the dateline here // This can be detected by transforming centroid of the target triangle. // If the transformed centroid is outside the transformed triangle, // the triangle wraps around projection extent. - // In such case, the - - var srcExtent = ol.extent.createEmpty(); - ol.extent.extendCoordinate(srcExtent, aSrc); - ol.extent.extendCoordinate(srcExtent, bSrc); - ol.extent.extendCoordinate(srcExtent, cSrc); var centroid = [(a[0] + b[0] + c[0]) / 3, (a[1] + b[1] + c[1]) / 3]; var centroidSrc = transformInv(centroid); - var pInTriangle = function(p, p0, p1, p2) { - //TODO: move somewhere else - var A = (-p1[1] * p2[0] + p0[1] * (-p1[0] + p2[0]) + - p0[0] * (p1[1] - p2[1]) + p1[0] * p2[1]) / 2; - var sign = A < 0 ? -1 : 1; - var s = (p0[1] * p2[0] - p0[0] * p2[1] + - (p2[1] - p0[1]) * p[0] + - (p0[0] - p2[0]) * p[1]) * sign; - var t = (p0[0] * p1[1] - p0[1] * p1[0] + - (p0[1] - p1[1]) * p[0] + - (p1[0] - p0[0]) * p[1]) * sign; - - return s > 0 && t > 0 && (s + t) < 2 * A * sign; - }; - - if (!pInTriangle(centroidSrc, aSrc, bSrc, cSrc)) { - var sourceProjExtent = sourceProj.getExtent(); - shiftDistance = ol.extent.getWidth(sourceProjExtent); + if (!ol.coordinate.isInTriangle(centroidSrc, aSrc, bSrc, cSrc)) { + needsShift = true; } } - var tri = [[aSrc, a], [bSrc, b], [cSrc, c]]; - // TODO: typing ! do not add properties to arrays ! - tri.shiftDistance = shiftDistance; - if (shiftDistance) { - triangulation.shiftDistance = shiftDistance; + triangulation.triangles.push({ + source: [aSrc, bSrc, cSrc], + target: [a, b, c], + needsShift: needsShift + }); + if (needsShift) { + var sourceProjExtent = sourceProj.getExtent(); + triangulation.shiftDistance = ol.extent.getWidth(sourceProjExtent); } - triangulation.push(tri); }; @@ -130,7 +121,10 @@ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, ol.reproj.triangulation.createForExtent = function(extent, sourceProj, targetProj, opt_maxTargetExtent, opt_maxSourceExtent, opt_subdiv) { - var triangulation = []; + var triangulation = { + triangles: [], + shiftDistance: 0 + }; var tlDst = ol.extent.getTopLeft(extent); var brDst = ol.extent.getBottomRight(extent); @@ -178,26 +172,25 @@ ol.reproj.triangulation.createForExtent = function(extent, sourceProj, ol.reproj.triangulation.getSourceExtent = function(triangulation) { var extent = ol.extent.createEmpty(); - if (triangulation.shiftDistance) { - var shiftDistance = triangulation.shiftDistance; - goog.array.forEach(triangulation, function(triangle, i, arr) { + var distance = triangulation.shiftDistance; + if (distance > 0) { + goog.array.forEach(triangulation.triangles, function(triangle, i, arr) { + var src = triangle.source; ol.extent.extendCoordinate(extent, - [goog.math.modulo(triangle[0][0][0] + shiftDistance, shiftDistance), - triangle[0][0][1]]); + [goog.math.modulo(src[0][0] + distance, distance), src[0][1]]); ol.extent.extendCoordinate(extent, - [goog.math.modulo(triangle[1][0][0] + shiftDistance, shiftDistance), - triangle[1][0][1]]); + [goog.math.modulo(src[1][0] + distance, distance), src[1][1]]); ol.extent.extendCoordinate(extent, - [goog.math.modulo(triangle[2][0][0] + shiftDistance, shiftDistance), - triangle[2][0][1]]); + [goog.math.modulo(src[2][0] + distance, distance), src[2][1]]); }); - if (extent[0] > shiftDistance / 2) extent[0] -= shiftDistance; - if (extent[2] > shiftDistance / 2) extent[2] -= shiftDistance; + if (extent[0] > distance / 2) extent[0] -= distance; + if (extent[2] > distance / 2) extent[2] -= distance; } else { - goog.array.forEach(triangulation, function(triangle, i, arr) { - ol.extent.extendCoordinate(extent, triangle[0][0]); - ol.extent.extendCoordinate(extent, triangle[1][0]); - ol.extent.extendCoordinate(extent, triangle[2][0]); + goog.array.forEach(triangulation.triangles, function(triangle, i, arr) { + var src = triangle.source; + ol.extent.extendCoordinate(extent, src[0]); + ol.extent.extendCoordinate(extent, src[1]); + ol.extent.extendCoordinate(extent, src[2]); }); } From 6e08fc9e13b88ba3db837969ff845025b9c2ccf5 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 3 Jun 2015 16:09:05 +0200 Subject: [PATCH 11/80] Rename triangles to triangulation to be more precise --- src/ol/reproj/image.js | 9 ++++----- src/ol/reproj/tile.js | 26 ++++++++++++-------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index 17d03e7b89..b01a61e6ab 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -55,9 +55,8 @@ ol.reproj.Image = function(sourceProj, targetProj, * @private * @type {!ol.reproj.Triangulation} */ - this.triangles_ = ol.reproj.triangulation.createForExtent( - targetExtent, sourceProj, targetProj, - maxTargetExtent, maxSourceExtent); + this.triangulation_ = ol.reproj.triangulation.createForExtent( + targetExtent, sourceProj, targetProj, maxTargetExtent, maxSourceExtent); /** * @private @@ -71,7 +70,7 @@ ol.reproj.Image = function(sourceProj, targetProj, */ this.targetExtent_ = targetExtent; - var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangles_); + var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangulation_); var targetCenter = ol.extent.getCenter(targetExtent); var sourceResolution = ol.reproj.calculateSourceResolution( @@ -133,7 +132,7 @@ ol.reproj.Image.prototype.reproject_ = function() { // render the reprojected content ol.reproj.renderTriangles(this.context_, this.srcImage_.getResolution(), this.targetResolution_, this.targetExtent_, - this.triangles_, [{ + this.triangulation_, [{ extent: this.srcImage_.getExtent(), image: this.srcImage_.getImage() }]); diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index f9e6d3ccbf..fc70e1847c 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -57,11 +57,18 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, */ this.targetTileGrid_ = targetTileGrid; + + var targetExtent = targetTileGrid.getTileCoordExtent(this.getTileCoord()); + var maxTargetExtent = this.targetTileGrid_.getExtent(); + var maxSourceExtent = this.sourceTileGrid_.getExtent(); + /** * @private * @type {!ol.reproj.Triangulation} */ - this.triangles_ = []; + this.triangulation_ = ol.reproj.triangulation.createForExtent( + targetExtent, sourceProj, targetProj, + maxTargetExtent, maxSourceExtent); /** * @private @@ -81,23 +88,14 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, */ this.srcZ_ = 0; - - var targetExtent = targetTileGrid.getTileCoordExtent(this.getTileCoord()); - var maxTargetExtent = this.targetTileGrid_.getExtent(); - var maxSourceExtent = this.sourceTileGrid_.getExtent(); - if (!ol.extent.intersects(maxTargetExtent, targetExtent)) { // Tile is completely outside range -> EMPTY // TODO: is it actually correct that the source even creates the tile ? this.state = ol.TileState.EMPTY; - //return; + return; } - this.triangles_ = ol.reproj.triangulation.createForExtent( - targetExtent, sourceProj, targetProj, - maxTargetExtent, maxSourceExtent); - - if (this.triangles_.length === 0) { + if (this.triangulation_.triangles.length === 0) { // no valid triangles -> EMPTY this.state = ol.TileState.EMPTY; return; @@ -116,7 +114,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, } this.srcZ_ = sourceTileGrid.getZForResolution(sourceResolution); - var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangles_); + var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangulation_); var sourceProjExtent = sourceProj.getExtent(); if (!sourceProj.isGlobal() && sourceProjExtent) { @@ -241,7 +239,7 @@ ol.reproj.Tile.prototype.reproject_ = function() { if (sources.length > 0) { var targetExtent = this.targetTileGrid_.getTileCoordExtent(tileCoord); ol.reproj.renderTriangles(context, srcResolution, targetResolution, - targetExtent, this.triangles_, sources); + targetExtent, this.triangulation_, sources); } this.canvas_ = context.canvas; From bc74273208222002d77abd91bd842df603370759 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 4 Jun 2015 17:46:39 +0200 Subject: [PATCH 12/80] Reproject every image only once per triangle If the triangle has to be shifted, determine if the image has to be rendered shifted or not. --- src/ol/reproj/reproj.js | 42 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 6fee71094b..d32eea53c4 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -58,23 +58,12 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, ol.reproj.renderTriangles = function(context, sourceResolution, targetResolution, targetExtent, triangulation, sources) { - var renderImage = function(image) { - context.scale(sourceResolution, -sourceResolution); - - // the image has to be scaled by half a pixel in every direction - // in order to prevent artifacts between the original tiles - // that are introduced by the canvas antialiasing. - context.drawImage(image, -0.5, -0.5, - image.width + 1, image.height + 1); - }; - var shiftDistance = triangulation.shiftDistance; + var targetTL = ol.extent.getTopLeft(targetExtent); goog.array.forEach(triangulation.triangles, function(tri, i, arr) { context.save(); - var targetTL = ol.extent.getTopLeft(targetExtent); - /* Calculate affine transform (src -> dst) * Resulting matrix can be used to transform coordinate * from `sourceProjection` to destination pixels. @@ -150,24 +139,21 @@ ol.reproj.renderTriangles = function(context, goog.array.forEach(sources, function(src, i, arr) { context.save(); - var tlSrcFromData = ol.extent.getTopLeft(src.extent); - context.translate(tlSrcFromData[0], tlSrcFromData[1]); - if (tri.needsShift) { - context.save(); + var dataTL = ol.extent.getTopLeft(src.extent); + context.translate(dataTL[0], dataTL[1]); + // if the triangle needs to be shifted (because of the dateline wrapping), + // shift only the source images that need it + if (tri.needsShift && dataTL[0] < 0) { context.translate(shiftDistance, 0); - renderImage(src.image); - context.restore(); - renderImage(src.image); - - if (goog.DEBUG) { - context.fillStyle = - sources.length > 16 ? 'rgba(255,0,0,1)' : - (sources.length > 4 ? 'rgba(0,255,0,.3)' : 'rgba(0,0,255,.1)'); - context.fillRect(0, 0, 256, 256); - } - } else { - renderImage(src.image); } + context.scale(sourceResolution, -sourceResolution); + + // the image has to be scaled by half a pixel in every direction + // in order to prevent artifacts between the original tiles + // that are introduced by the canvas antialiasing. + context.drawImage(src.image, -0.5, -0.5, + src.image.width + 1, src.image.height + 1); + context.restore(); }); From 3f567b0bf03a47407b66f1b4b8a95f98a07cee12 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 5 Jun 2015 10:29:55 +0200 Subject: [PATCH 13/80] Use per-projection tilecaches and tilegrids (persistent instances) --- src/ol/source/tileimagesource.js | 52 +++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index daee938631..78c66b99c4 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -10,6 +10,7 @@ goog.require('ol.TileLoadFunctionType'); goog.require('ol.TileState'); goog.require('ol.TileUrlFunction'); goog.require('ol.TileUrlFunctionType'); +goog.require('ol.proj'); goog.require('ol.reproj.Tile'); goog.require('ol.source.Tile'); goog.require('ol.source.TileEvent'); @@ -73,15 +74,15 @@ ol.source.TileImage = function(options) { /** * @protected - * @type {ol.TileCache} + * @type {Object.} */ - this.reprojectedTileCache = new ol.TileCache(); + this.tileCacheForProjection = {}; /** * @protected - * @type {ol.proj.Projection} + * @type {Object.} */ - this.lastProj = null; + this.tileGridForProjection = {}; }; goog.inherits(ol.source.TileImage, ol.source.Tile); @@ -99,10 +100,34 @@ ol.source.TileImage.defaultTileLoadFunction = function(imageTile, src) { * @inheritDoc */ ol.source.TileImage.prototype.getTileGridForProjection = function(projection) { - if (projection == this.getProjection() && !goog.isNull(this.tileGrid)) { + if (!goog.isNull(this.tileGrid) && + ol.proj.equivalent(this.getProjection(), projection)) { return this.tileGrid; } else { - return ol.tilegrid.getForProjection(projection); + var projKey = goog.getUid(projection).toString(); + if (!(projKey in this.tileGridForProjection)) { + this.tileGridForProjection[projKey] = + ol.tilegrid.getForProjection(projection); + } + return this.tileGridForProjection[projKey]; + } +}; + + +/** + * @param {ol.proj.Projection} projection Projection. + * @return {ol.TileCache} Tile cache. + * @protected + */ +ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) { + if (ol.proj.equivalent(this.getProjection(), projection)) { + return this.tileCache; + } else { + var projKey = goog.getUid(projection).toString(); + if (!(projKey in this.tileCacheForProjection)) { + this.tileCacheForProjection[projKey] = new ol.TileCache(); + } + return this.tileCacheForProjection[projKey]; } }; @@ -112,22 +137,19 @@ ol.source.TileImage.prototype.getTileGridForProjection = function(projection) { */ ol.source.TileImage.prototype.getTile = function(z, x, y, pixelRatio, projection) { - if (goog.isNull(this.getProjection()) || this.getProjection() == projection) { + if (!goog.isDefAndNotNull(this.getProjection()) || + !goog.isDefAndNotNull(projection) || + ol.proj.equivalent(this.getProjection(), projection)) { return this.getTileInternal(z, x, y, pixelRatio, projection); } else { - if (this.lastProj != projection) { - this.reprojectedTileCache.clear(); - this.lastProj = projection; - } - var cache = this.reprojectedTileCache; //TODO: per-projection cache + var cache = this.getTileCacheForProjection(projection); var tileCoordKey = this.getKeyZXY(z, x, y); if (cache.containsKey(tileCoordKey)) { return /** @type {!ol.Tile} */(cache.get(tileCoordKey)); } else { var sourceProjection = this.getProjection(); var sourceTileGrid = this.getTileGridForProjection(sourceProjection); - var targetTileGrid = ol.tilegrid.getForProjection(projection); - //TODO: init cache / tilegrid if needed + var targetTileGrid = this.getTileGridForProjection(projection); var tile = new ol.reproj.Tile( sourceProjection, sourceTileGrid, projection, targetTileGrid, @@ -229,6 +251,7 @@ ol.source.TileImage.prototype.handleTileChange_ = function(event) { */ ol.source.TileImage.prototype.setTileLoadFunction = function(tileLoadFunction) { this.tileCache.clear(); + this.tileCacheForProjection = {}; this.tileLoadFunction = tileLoadFunction; this.changed(); }; @@ -244,6 +267,7 @@ ol.source.TileImage.prototype.setTileUrlFunction = function(tileUrlFunction) { // FIXME cache. The tile URL function would need to be incorporated into the // FIXME cache key somehow. this.tileCache.clear(); + this.tileCacheForProjection = {}; this.tileUrlFunction = tileUrlFunction; this.changed(); }; From 9b82f19cc23d2ff6dd417d0565fbb2d607b2c4d7 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Tue, 9 Jun 2015 14:28:44 +0200 Subject: [PATCH 14/80] Simplify triangle-shifting logic for reprojection --- src/ol/reproj/reproj.js | 22 ++++++++++++++-------- src/ol/reproj/tile.js | 8 +++++--- src/ol/reproj/triangulation.js | 33 +++++++++++++++++++++------------ 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index d32eea53c4..97d8c16a0e 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -49,6 +49,7 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, * Renders the source into the canvas based on the triangulation. * @param {CanvasRenderingContext2D} context * @param {number} sourceResolution + * @param {ol.Extent} sourceExtent * @param {number} targetResolution * @param {ol.Extent} targetExtent * @param {ol.reproj.Triangulation} triangulation @@ -56,9 +57,13 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, * image: (HTMLCanvasElement|Image)}>} sources */ ol.reproj.renderTriangles = function(context, - sourceResolution, targetResolution, targetExtent, triangulation, sources) { + sourceResolution, sourceExtent, targetResolution, targetExtent, + triangulation, sources) { - var shiftDistance = triangulation.shiftDistance; + var shiftDistance = !goog.isNull(sourceExtent) ? + ol.extent.getWidth(sourceExtent) : null; + var shiftThreshold = !goog.isNull(sourceExtent) ? + (sourceExtent[0] + sourceExtent[2]) / 2 : null; var targetTL = ol.extent.getTopLeft(targetExtent); goog.array.forEach(triangulation.triangles, function(tri, i, arr) { @@ -91,10 +96,10 @@ ol.reproj.renderTriangles = function(context, var u0 = tgt[0][0] - targetTL[0], v0 = -(tgt[0][1] - targetTL[1]), u1 = tgt[1][0] - targetTL[0], v1 = -(tgt[1][1] - targetTL[1]), u2 = tgt[2][0] - targetTL[0], v2 = -(tgt[2][1] - targetTL[1]); - if (tri.needsShift) { - x0 = goog.math.modulo(x0 + shiftDistance, shiftDistance); - x1 = goog.math.modulo(x1 + shiftDistance, shiftDistance); - x2 = goog.math.modulo(x2 + shiftDistance, shiftDistance); + if (tri.needsShift && !goog.isNull(shiftDistance)) { + x0 = goog.math.modulo(x0, shiftDistance); + x1 = goog.math.modulo(x1, shiftDistance); + x2 = goog.math.modulo(x2, shiftDistance); } var augmentedMatrix = [ [x0, y0, 1, 0, 0, 0, u0 / targetResolution], @@ -142,8 +147,9 @@ ol.reproj.renderTriangles = function(context, var dataTL = ol.extent.getTopLeft(src.extent); context.translate(dataTL[0], dataTL[1]); // if the triangle needs to be shifted (because of the dateline wrapping), - // shift only the source images that need it - if (tri.needsShift && dataTL[0] < 0) { + // shift back only the source images that need it + if (tri.needsShift && !goog.isNull(shiftDistance) && + dataTL[0] < shiftThreshold) { context.translate(shiftDistance, 0); } context.scale(sourceResolution, -sourceResolution); diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index fc70e1847c..9f65a23da5 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -114,7 +114,8 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, } this.srcZ_ = sourceTileGrid.getZForResolution(sourceResolution); - var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangulation_); + var srcExtent = ol.reproj.triangulation.getSourceExtent( + this.triangulation_, sourceProj); var sourceProjExtent = sourceProj.getExtent(); if (!sourceProj.isGlobal() && sourceProjExtent) { @@ -238,8 +239,9 @@ ol.reproj.Tile.prototype.reproject_ = function() { if (sources.length > 0) { var targetExtent = this.targetTileGrid_.getTileCoordExtent(tileCoord); - ol.reproj.renderTriangles(context, srcResolution, targetResolution, - targetExtent, this.triangulation_, sources); + ol.reproj.renderTriangles(context, + srcResolution, this.sourceTileGrid_.getExtent(), + targetResolution, targetExtent, this.triangulation_, sources); } this.canvas_ = context.canvas; diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 07186240ea..43c890089f 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -22,8 +22,10 @@ ol.reproj.Triangle; /** + * `needsShift` indicates that _any_ of the triangles has to be shifted during + * reprojection. See {@link ol.reproj.Triangle}. * @typedef {{triangles: Array., - * shiftDistance: number}} + * needsShift: boolean}} */ ol.reproj.Triangulation; @@ -101,8 +103,7 @@ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, needsShift: needsShift }); if (needsShift) { - var sourceProjExtent = sourceProj.getExtent(); - triangulation.shiftDistance = ol.extent.getWidth(sourceProjExtent); + triangulation.needsShift = true; } }; @@ -123,7 +124,7 @@ ol.reproj.triangulation.createForExtent = function(extent, sourceProj, var triangulation = { triangles: [], - shiftDistance: 0 + needsShift: false }; var tlDst = ol.extent.getTopLeft(extent); @@ -167,24 +168,32 @@ ol.reproj.triangulation.createForExtent = function(extent, sourceProj, /** * @param {ol.reproj.Triangulation} triangulation + * @param {ol.proj.Projection} sourceProj * @return {ol.Extent} */ -ol.reproj.triangulation.getSourceExtent = function(triangulation) { +ol.reproj.triangulation.getSourceExtent = function(triangulation, sourceProj) { var extent = ol.extent.createEmpty(); - var distance = triangulation.shiftDistance; - if (distance > 0) { + if (triangulation.needsShift) { + // although only some of the triangles are crossing the dateline, + // all coordiantes need to be "shifted" to be positive + // to properly calculate the extent (and then possibly shifted back) + + var sourceProjExtent = sourceProj.getExtent(); + var sourceProjWidth = ol.extent.getWidth(sourceProjExtent); goog.array.forEach(triangulation.triangles, function(triangle, i, arr) { var src = triangle.source; ol.extent.extendCoordinate(extent, - [goog.math.modulo(src[0][0] + distance, distance), src[0][1]]); + [goog.math.modulo(src[0][0], sourceProjWidth), src[0][1]]); ol.extent.extendCoordinate(extent, - [goog.math.modulo(src[1][0] + distance, distance), src[1][1]]); + [goog.math.modulo(src[1][0], sourceProjWidth), src[1][1]]); ol.extent.extendCoordinate(extent, - [goog.math.modulo(src[2][0] + distance, distance), src[2][1]]); + [goog.math.modulo(src[2][0], sourceProjWidth), src[2][1]]); }); - if (extent[0] > distance / 2) extent[0] -= distance; - if (extent[2] > distance / 2) extent[2] -= distance; + + var right = sourceProjExtent[2]; + if (extent[0] > right) extent[0] -= sourceProjWidth; + if (extent[2] > right) extent[2] -= sourceProjWidth; } else { goog.array.forEach(triangulation.triangles, function(triangle, i, arr) { var src = triangle.source; From 63084d30e9450e12b3e8a618bd3f2d79607dcb55 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Tue, 9 Jun 2015 17:53:34 +0200 Subject: [PATCH 15/80] Improve numerical stability of reprojection By shifting the source points before calculating the affine transform to obtain values from similar ranges (inputs and outputs of the transform). --- src/ol/reproj/reproj.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 97d8c16a0e..97b336c680 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -101,6 +101,19 @@ ol.reproj.renderTriangles = function(context, x1 = goog.math.modulo(x1, shiftDistance); x2 = goog.math.modulo(x2, shiftDistance); } + + // Shift all the source points to improve numerical stability + // of all the subsequent calculations. + // The [x0, y0] is used here, because it should achieve reasonable results + // but any values could actually be chosen. + var srcShiftX = x0, srcShiftY = y0; + x0 = 0; + y0 = 0; + x1 -= srcShiftX; + y1 -= srcShiftY; + x2 -= srcShiftX; + y2 -= srcShiftY; + var augmentedMatrix = [ [x0, y0, 1, 0, 0, 0, u0 / targetResolution], [x1, y1, 1, 0, 0, 0, u1 / targetResolution], @@ -145,7 +158,8 @@ ol.reproj.renderTriangles = function(context, goog.array.forEach(sources, function(src, i, arr) { context.save(); var dataTL = ol.extent.getTopLeft(src.extent); - context.translate(dataTL[0], dataTL[1]); + context.translate(dataTL[0] - srcShiftX, dataTL[1] - srcShiftY); + // if the triangle needs to be shifted (because of the dateline wrapping), // shift back only the source images that need it if (tri.needsShift && !goog.isNull(shiftDistance) && From e0cfa1951ac034a8c213f020b933738b87125a38 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 10 Jun 2015 10:09:34 +0200 Subject: [PATCH 16/80] Add projection parameter to ol.source.Tile#forEachLoadedTile --- .../canvas/canvastilelayerrenderer.js | 3 +- src/ol/renderer/dom/domtilelayerrenderer.js | 3 +- src/ol/renderer/layerrenderer.js | 17 ++++++----- .../renderer/webgl/webgltilelayerrenderer.js | 26 +++++++++-------- src/ol/source/tileimagesource.js | 4 +-- src/ol/source/tilesource.js | 29 +++++++++++++++++-- test/spec/ol/source/tilesource.test.js | 27 +++++++++-------- 7 files changed, 70 insertions(+), 39 deletions(-) diff --git a/src/ol/renderer/canvas/canvastilelayerrenderer.js b/src/ol/renderer/canvas/canvastilelayerrenderer.js index e6ce046ab9..14ada48772 100644 --- a/src/ol/renderer/canvas/canvastilelayerrenderer.js +++ b/src/ol/renderer/canvas/canvastilelayerrenderer.js @@ -309,7 +309,8 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = /** @type {Array.} */ var tilesToClear = []; - var findLoadedTiles = this.createLoadedTileFinder(tileSource, tilesToDrawByZ); + var findLoadedTiles = this.createLoadedTileFinder( + tileSource, projection, tilesToDrawByZ); var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); diff --git a/src/ol/renderer/dom/domtilelayerrenderer.js b/src/ol/renderer/dom/domtilelayerrenderer.js index eb93e39538..d9a66b5f7e 100644 --- a/src/ol/renderer/dom/domtilelayerrenderer.js +++ b/src/ol/renderer/dom/domtilelayerrenderer.js @@ -121,7 +121,8 @@ ol.renderer.dom.TileLayer.prototype.prepareFrame = var tilesToDrawByZ = {}; tilesToDrawByZ[z] = {}; - var findLoadedTiles = this.createLoadedTileFinder(tileSource, tilesToDrawByZ); + var findLoadedTiles = this.createLoadedTileFinder( + tileSource, projection, tilesToDrawByZ); var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index f6dd738106..5dd3da9a29 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -87,6 +87,7 @@ ol.renderer.Layer.prototype.hasFeatureAtCoordinate = goog.functions.FALSE; /** * Create a function that adds loaded tiles to the tile lookup. * @param {ol.source.Tile} source Tile source. + * @param {ol.proj.Projection} projection Projection of the tiles. * @param {Object.>} tiles Lookup of loaded * tiles by zoom level. * @return {function(number, ol.TileRange):boolean} A function that can be @@ -94,7 +95,8 @@ ol.renderer.Layer.prototype.hasFeatureAtCoordinate = goog.functions.FALSE; * lookup. * @protected */ -ol.renderer.Layer.prototype.createLoadedTileFinder = function(source, tiles) { +ol.renderer.Layer.prototype.createLoadedTileFinder = + function(source, projection, tiles) { return ( /** * @param {number} zoom Zoom level. @@ -102,12 +104,13 @@ ol.renderer.Layer.prototype.createLoadedTileFinder = function(source, tiles) { * @return {boolean} The tile range is fully loaded. */ function(zoom, tileRange) { - return source.forEachLoadedTile(zoom, tileRange, function(tile) { - if (!tiles[zoom]) { - tiles[zoom] = {}; - } - tiles[zoom][tile.tileCoord.toString()] = tile; - }); + return source.forEachLoadedTile(projection, zoom, + tileRange, function(tile) { + if (!tiles[zoom]) { + tiles[zoom] = {}; + } + tiles[zoom][tile.tileCoord.toString()] = tile; + }); }); }; diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index 6752062548..675aac21ae 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -105,6 +105,7 @@ ol.renderer.webgl.TileLayer.prototype.disposeInternal = function() { /** * Create a function that adds loaded tiles to the tile lookup. * @param {ol.source.Tile} source Tile source. + * @param {ol.proj.Projection} projection Projection of the tiles. * @param {Object.>} tiles Lookup of loaded * tiles by zoom level. * @return {function(number, ol.TileRange):boolean} A function that can be @@ -113,7 +114,7 @@ ol.renderer.webgl.TileLayer.prototype.disposeInternal = function() { * @protected */ ol.renderer.webgl.TileLayer.prototype.createLoadedTileFinder = - function(source, tiles) { + function(source, projection, tiles) { var mapRenderer = this.mapRenderer; return ( @@ -123,16 +124,17 @@ ol.renderer.webgl.TileLayer.prototype.createLoadedTileFinder = * @return {boolean} The tile range is fully loaded. */ function(zoom, tileRange) { - return source.forEachLoadedTile(zoom, tileRange, function(tile) { - var loaded = mapRenderer.isTileTextureLoaded(tile); - if (loaded) { - if (!tiles[zoom]) { - tiles[zoom] = {}; - } - tiles[zoom][tile.tileCoord.toString()] = tile; - } - return loaded; - }); + return source.forEachLoadedTile(projection, zoom, + tileRange, function(tile) { + var loaded = mapRenderer.isTileTextureLoaded(tile); + if (loaded) { + if (!tiles[zoom]) { + tiles[zoom] = {}; + } + tiles[zoom][tile.tileCoord.toString()] = tile; + } + return loaded; + }); }); }; @@ -239,7 +241,7 @@ ol.renderer.webgl.TileLayer.prototype.prepareFrame = tilesToDrawByZ[z] = {}; var findLoadedTiles = this.createLoadedTileFinder( - tileSource, tilesToDrawByZ); + tileSource, projection, tilesToDrawByZ); var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); var allTilesLoaded = true; diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 78c66b99c4..697d1c0afb 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -115,9 +115,7 @@ ol.source.TileImage.prototype.getTileGridForProjection = function(projection) { /** - * @param {ol.proj.Projection} projection Projection. - * @return {ol.TileCache} Tile cache. - * @protected + * @inheritDoc */ ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) { if (ol.proj.equivalent(this.getProjection(), projection)) { diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index 5bf8116f75..55e96651bd 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -10,6 +10,7 @@ goog.require('ol.Extent'); goog.require('ol.TileCache'); goog.require('ol.TileRange'); goog.require('ol.TileState'); +goog.require('ol.proj'); goog.require('ol.size'); goog.require('ol.source.Source'); goog.require('ol.tilecoord'); @@ -105,6 +106,7 @@ ol.source.Tile.prototype.expireCache = function(usedTiles) { /** + * @param {ol.proj.Projection} projection * @param {number} z Zoom level. * @param {ol.TileRange} tileRange Tile range. * @param {function(ol.Tile):(boolean|undefined)} callback Called with each @@ -112,15 +114,21 @@ ol.source.Tile.prototype.expireCache = function(usedTiles) { * considered loaded. * @return {boolean} The tile range is fully covered with loaded tiles. */ -ol.source.Tile.prototype.forEachLoadedTile = function(z, tileRange, callback) { +ol.source.Tile.prototype.forEachLoadedTile = + function(projection, z, tileRange, callback) { + var tileCache = this.getTileCacheForProjection(projection); + if (goog.isNull(tileCache)) { + return false; + } + var covered = true; var tile, tileCoordKey, loaded; for (var x = tileRange.minX; x <= tileRange.maxX; ++x) { for (var y = tileRange.minY; y <= tileRange.maxY; ++y) { tileCoordKey = this.getKeyZXY(z, x, y); loaded = false; - if (this.tileCache.containsKey(tileCoordKey)) { - tile = /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey)); + if (tileCache.containsKey(tileCoordKey)) { + tile = /** @type {!ol.Tile} */ (tileCache.get(tileCoordKey)); loaded = tile.getState() === ol.TileState.LOADED; if (loaded) { loaded = (callback(tile) !== false); @@ -203,6 +211,21 @@ ol.source.Tile.prototype.getTileGridForProjection = function(projection) { }; +/** + * @param {ol.proj.Projection} projection Projection. + * @return {ol.TileCache} Tile cache. + * @protected + */ +ol.source.Tile.prototype.getTileCacheForProjection = function(projection) { + if (goog.isNull(this.tileCache) || + !ol.proj.equivalent(this.getProjection(), projection)) { + return null; + } else { + return this.tileCache; + } +}; + + /** * @param {number} z Z. * @param {number} pixelRatio Pixel ratio. diff --git a/test/spec/ol/source/tilesource.test.js b/test/spec/ol/source/tilesource.test.js index 59d8fbbc4d..e660f2f520 100644 --- a/test/spec/ol/source/tilesource.test.js +++ b/test/spec/ol/source/tilesource.test.js @@ -26,7 +26,7 @@ describe('ol.source.Tile', function() { var zoom = 3; var range = grid.getTileRangeForExtentAndZ(extent, zoom); - source.forEachLoadedTile(zoom, range, callback); + source.forEachLoadedTile(source.getProjection(), zoom, range, callback); expect(callback.callCount).to.be(0); }); @@ -38,7 +38,7 @@ describe('ol.source.Tile', function() { var zoom = 3; var range = grid.getTileRangeForExtentAndZ(extent, zoom); - source.forEachLoadedTile(zoom, range, callback); + source.forEachLoadedTile(source.getProjection(), zoom, range, callback); expect(source.getTile.callCount).to.be(0); source.getTile.restore(); }); @@ -55,7 +55,7 @@ describe('ol.source.Tile', function() { var zoom = 1; var range = new ol.TileRange(0, 1, 0, 1); - source.forEachLoadedTile(zoom, range, callback); + source.forEachLoadedTile(source.getProjection(), zoom, range, callback); expect(callback.callCount).to.be(3); }); @@ -71,9 +71,10 @@ describe('ol.source.Tile', function() { var zoom = 1; var range = new ol.TileRange(0, 1, 0, 1); - var covered = source.forEachLoadedTile(zoom, range, function() { - return true; - }); + var covered = source.forEachLoadedTile(source.getProjection(), zoom, + range, function() { + return true; + }); expect(covered).to.be(true); }); @@ -89,9 +90,10 @@ describe('ol.source.Tile', function() { var zoom = 1; var range = new ol.TileRange(0, 1, 0, 1); - var covered = source.forEachLoadedTile(zoom, range, function() { - return true; - }); + var covered = source.forEachLoadedTile(source.getProjection(), zoom, + range, function() { + return true; + }); expect(covered).to.be(false); }); @@ -107,9 +109,10 @@ describe('ol.source.Tile', function() { var zoom = 1; var range = new ol.TileRange(0, 1, 0, 1); - var covered = source.forEachLoadedTile(zoom, range, function() { - return false; - }); + var covered = source.forEachLoadedTile(source.getProjection(), zoom, + range, function() { + return false; + }); expect(covered).to.be(false); }); From b0694c1e3b043ba908f19fa9e3fb84638f03fc24 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 10 Jun 2015 10:38:46 +0200 Subject: [PATCH 17/80] Add projection parameter to ol.source.Tile#expireCache and #useTile This is required to be able to determine which cache the xyz coordinates refer to (in case we have more caches). --- src/ol/renderer/layerrenderer.js | 5 +++-- src/ol/source/tileimagesource.js | 36 +++++++++++++++++++++++++++++--- src/ol/source/tilesource.js | 9 ++++++-- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index 5dd3da9a29..e3d368b2e8 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -194,7 +194,8 @@ ol.renderer.Layer.prototype.scheduleExpireCache = */ function(tileSource, map, frameState) { var tileSourceKey = goog.getUid(tileSource).toString(); - tileSource.expireCache(frameState.usedTiles[tileSourceKey]); + tileSource.expireCache(frameState.viewState.projection, + frameState.usedTiles[tileSourceKey]); }, tileSource)); } }; @@ -327,7 +328,7 @@ ol.renderer.Layer.prototype.manageTilePyramid = function( opt_tileCallback.call(opt_this, tile); } } else { - tileSource.useTile(z, x, y); + tileSource.useTile(z, x, y, projection); } } } diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 697d1c0afb..c06c4bc3bc 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -3,6 +3,7 @@ goog.provide('ol.source.TileImage'); goog.require('goog.asserts'); goog.require('goog.events'); goog.require('goog.events.EventType'); +goog.require('goog.object'); goog.require('ol.ImageTile'); goog.require('ol.TileCache'); goog.require('ol.TileCoord'); @@ -96,6 +97,34 @@ ol.source.TileImage.defaultTileLoadFunction = function(imageTile, src) { }; +/** + * @inheritDoc + */ +ol.source.TileImage.prototype.canExpireCache = function() { + var canExpire = this.tileCache.canExpireCache(); + if (canExpire) { + return true; + } else { + return goog.object.some(this.tileCacheForProjection, function(tileCache) { + return tileCache.canExpireCache(); + }); + } +}; + + +/** + * @inheritDoc + */ +ol.source.TileImage.prototype.expireCache = function(projection, usedTiles) { + var usedTileCache = this.getTileCacheForProjection(projection); + + this.tileCache.expireCache(this.tileCache == usedTileCache ? usedTiles : {}); + goog.object.forEach(this.tileCacheForProjection, function(tileCache) { + return tileCache.expireCache(tileCache == usedTileCache ? usedTiles : {}); + }); +}; + + /** * @inheritDoc */ @@ -274,9 +303,10 @@ ol.source.TileImage.prototype.setTileUrlFunction = function(tileUrlFunction) { /** * @inheritDoc */ -ol.source.TileImage.prototype.useTile = function(z, x, y) { +ol.source.TileImage.prototype.useTile = function(z, x, y, projection) { + var tileCache = this.getTileCacheForProjection(projection); var tileCoordKey = this.getKeyZXY(z, x, y); - if (this.tileCache.containsKey(tileCoordKey)) { - this.tileCache.get(tileCoordKey); + if (!goog.isNull(tileCache) && tileCache.containsKey(tileCoordKey)) { + tileCache.get(tileCoordKey); } }; diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index 55e96651bd..d2c8812d3f 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -98,10 +98,14 @@ ol.source.Tile.prototype.canExpireCache = function() { /** + * @param {ol.proj.Projection} projection * @param {Object.} usedTiles Used tiles. */ -ol.source.Tile.prototype.expireCache = function(usedTiles) { - this.tileCache.expireCache(usedTiles); +ol.source.Tile.prototype.expireCache = function(projection, usedTiles) { + var tileCache = this.getTileCacheForProjection(projection); + if (!goog.isNull(tileCache)) { + tileCache.expireCache(usedTiles); + } }; @@ -267,6 +271,7 @@ ol.source.Tile.prototype.getTileCoordForTileUrlFunction = * @param {number} z Tile coordinate z. * @param {number} x Tile coordinate x. * @param {number} y Tile coordinate y. + * @param {ol.proj.Projection} projection Projection. */ ol.source.Tile.prototype.useTile = ol.nullFunction; From c205323ff272abc589558df7345deaf0ae58f724 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 10 Jun 2015 11:15:48 +0200 Subject: [PATCH 18/80] Reproject correctly when transform returns +-Infinity values --- src/ol/reproj/triangulation.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 43c890089f..77b4b24406 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -76,6 +76,19 @@ ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, aSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, aSrc); bSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, bSrc); cSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, cSrc); + + var makeFinite = function(coord, extent) { + if (!goog.math.isFiniteNumber(coord[0])) { + coord[0] = goog.math.clamp(coord[0], extent[0], extent[2]); + } + if (!goog.math.isFiniteNumber(coord[1])) { + coord[1] = goog.math.clamp(coord[1], extent[1], extent[3]); + } + }; + makeFinite(aSrc, opt_maxSourceExtent); + makeFinite(bSrc, opt_maxSourceExtent); + makeFinite(cSrc, opt_maxSourceExtent); + var transformFwd = ol.proj.getTransform(sourceProj, targetProj); a = transformFwd(aSrc); b = transformFwd(bSrc); From f2f77091adefbd9d2145e8f2bc724f14efe86589 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 10 Jun 2015 12:37:11 +0200 Subject: [PATCH 19/80] Handle tile sources without projection Such sources should never be reprojected (and no additional tilecache or tilegrid should be created). --- src/ol/source/tileimagesource.js | 6 ++++-- src/ol/source/tilesource.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index c06c4bc3bc..9e2c850cf3 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -129,8 +129,9 @@ ol.source.TileImage.prototype.expireCache = function(projection, usedTiles) { * @inheritDoc */ ol.source.TileImage.prototype.getTileGridForProjection = function(projection) { + var thisProj = this.getProjection(); if (!goog.isNull(this.tileGrid) && - ol.proj.equivalent(this.getProjection(), projection)) { + (goog.isNull(thisProj) || ol.proj.equivalent(thisProj, projection))) { return this.tileGrid; } else { var projKey = goog.getUid(projection).toString(); @@ -147,7 +148,8 @@ ol.source.TileImage.prototype.getTileGridForProjection = function(projection) { * @inheritDoc */ ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) { - if (ol.proj.equivalent(this.getProjection(), projection)) { + var thisProj = this.getProjection(); + if (goog.isNull(thisProj) || ol.proj.equivalent(thisProj, projection)) { return this.tileCache; } else { var projKey = goog.getUid(projection).toString(); diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index d2c8812d3f..86f4982fac 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -221,8 +221,8 @@ ol.source.Tile.prototype.getTileGridForProjection = function(projection) { * @protected */ ol.source.Tile.prototype.getTileCacheForProjection = function(projection) { - if (goog.isNull(this.tileCache) || - !ol.proj.equivalent(this.getProjection(), projection)) { + var thisProj = this.getProjection(); + if (!goog.isNull(thisProj) && !ol.proj.equivalent(thisProj, projection)) { return null; } else { return this.tileCache; From 15575288e0b12d554b9e4c0a8f7ab26a1087c319 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 11 Jun 2015 14:54:14 +0200 Subject: [PATCH 20/80] Add ol.ENABLE_RASTER_REPROJECTION define --- src/ol/ol.js | 7 +++++++ src/ol/source/tileimagesource.js | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/ol/ol.js b/src/ol/ol.js index cffdecbc29..c0c458349a 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -94,6 +94,13 @@ ol.ENABLE_NAMED_COLORS = false; ol.ENABLE_PROJ4JS = true; +/** + * @define {boolean} Enable automatic reprojection of raster sources. Default is + * `true`. + */ +ol.ENABLE_RASTER_REPROJECTION = true; + + /** * @define {boolean} Enable rendering of ol.layer.Tile based layers. Default is * `true`. Setting this to false at compile time in advanced mode removes diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 9e2c850cf3..3239cf8c36 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -101,6 +101,9 @@ ol.source.TileImage.defaultTileLoadFunction = function(imageTile, src) { * @inheritDoc */ ol.source.TileImage.prototype.canExpireCache = function() { + if (!ol.ENABLE_RASTER_REPROJECTION) { + return goog.base(this, 'canExpireCache'); + } var canExpire = this.tileCache.canExpireCache(); if (canExpire) { return true; @@ -116,11 +119,15 @@ ol.source.TileImage.prototype.canExpireCache = function() { * @inheritDoc */ ol.source.TileImage.prototype.expireCache = function(projection, usedTiles) { + if (!ol.ENABLE_RASTER_REPROJECTION) { + goog.base(this, 'expireCache', projection, usedTiles); + return; + } var usedTileCache = this.getTileCacheForProjection(projection); this.tileCache.expireCache(this.tileCache == usedTileCache ? usedTiles : {}); goog.object.forEach(this.tileCacheForProjection, function(tileCache) { - return tileCache.expireCache(tileCache == usedTileCache ? usedTiles : {}); + tileCache.expireCache(tileCache == usedTileCache ? usedTiles : {}); }); }; @@ -129,6 +136,9 @@ ol.source.TileImage.prototype.expireCache = function(projection, usedTiles) { * @inheritDoc */ ol.source.TileImage.prototype.getTileGridForProjection = function(projection) { + if (!ol.ENABLE_RASTER_REPROJECTION) { + return goog.base(this, 'getTileGridForProjection', projection); + } var thisProj = this.getProjection(); if (!goog.isNull(this.tileGrid) && (goog.isNull(thisProj) || ol.proj.equivalent(thisProj, projection))) { @@ -148,6 +158,9 @@ ol.source.TileImage.prototype.getTileGridForProjection = function(projection) { * @inheritDoc */ ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) { + if (!ol.ENABLE_RASTER_REPROJECTION) { + return goog.base(this, 'getTileCacheForProjection', projection); + } var thisProj = this.getProjection(); if (goog.isNull(thisProj) || ol.proj.equivalent(thisProj, projection)) { return this.tileCache; @@ -166,7 +179,8 @@ ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) { */ ol.source.TileImage.prototype.getTile = function(z, x, y, pixelRatio, projection) { - if (!goog.isDefAndNotNull(this.getProjection()) || + if (!ol.ENABLE_RASTER_REPROJECTION || + !goog.isDefAndNotNull(this.getProjection()) || !goog.isDefAndNotNull(projection) || ol.proj.equivalent(this.getProjection(), projection)) { return this.getTileInternal(z, x, y, pixelRatio, projection); From be0a0de7594d625cd0584d70b4025e02a013f5f2 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 11 Jun 2015 16:26:19 +0200 Subject: [PATCH 21/80] Integrate image reprojection with ol.source.Image --- src/ol/reproj/image.js | 23 +++++++++++++++-------- src/ol/source/imagesource.js | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index b01a61e6ab..f842ae3be8 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -32,7 +32,7 @@ ol.reproj.Image = function(sourceProj, targetProj, /** * @private - * @type {Canvas2DRenderingContext} + * @type {CanvasRenderingContext2D} */ this.context_ = ol.dom.createCanvasContext2D(width, height); this.context_.imageSmoothingEnabled = true; @@ -48,15 +48,21 @@ ol.reproj.Image = function(sourceProj, targetProj, */ this.canvas_ = this.context_.canvas; + /** + * @private + * @type {ol.Extent} + */ + this.maxSourceExtent_ = sourceProj.getExtent(); var maxTargetExtent = targetProj.getExtent(); - var maxSourceExtent = sourceProj.getExtent(); + /** * @private * @type {!ol.reproj.Triangulation} */ this.triangulation_ = ol.reproj.triangulation.createForExtent( - targetExtent, sourceProj, targetProj, maxTargetExtent, maxSourceExtent); + targetExtent, sourceProj, targetProj, + maxTargetExtent, this.maxSourceExtent_); /** * @private @@ -66,11 +72,12 @@ ol.reproj.Image = function(sourceProj, targetProj, /** * @private - * @type {!ol.Extent} + * @type {ol.Extent} */ this.targetExtent_ = targetExtent; - var srcExtent = ol.reproj.triangulation.getSourceExtent(this.triangulation_); + var srcExtent = ol.reproj.triangulation.getSourceExtent( + this.triangulation_, sourceProj); var targetCenter = ol.extent.getCenter(targetExtent); var sourceResolution = ol.reproj.calculateSourceResolution( @@ -130,9 +137,9 @@ ol.reproj.Image.prototype.reproject_ = function() { var srcState = this.srcImage_.getState(); if (srcState == ol.ImageState.LOADED) { // render the reprojected content - ol.reproj.renderTriangles(this.context_, this.srcImage_.getResolution(), - this.targetResolution_, this.targetExtent_, - this.triangulation_, [{ + ol.reproj.renderTriangles(this.context_, + this.srcImage_.getResolution(), this.maxSourceExtent_, + this.targetResolution_, this.targetExtent_, this.triangulation_, [{ extent: this.srcImage_.getExtent(), image: this.srcImage_.getImage() }]); diff --git a/src/ol/source/imagesource.js b/src/ol/source/imagesource.js index 3bd9afe517..d4fad482b1 100644 --- a/src/ol/source/imagesource.js +++ b/src/ol/source/imagesource.js @@ -8,6 +8,8 @@ goog.require('ol.Attribution'); goog.require('ol.Extent'); goog.require('ol.ImageState'); goog.require('ol.array'); +goog.require('ol.proj'); +goog.require('ol.reproj.Image'); goog.require('ol.source.Source'); @@ -90,7 +92,39 @@ ol.source.Image.prototype.findNearestResolution = * @param {ol.proj.Projection} projection Projection. * @return {ol.ImageBase} Single image. */ -ol.source.Image.prototype.getImage = goog.abstractMethod; +ol.source.Image.prototype.getImage = + function(extent, resolution, pixelRatio, projection) { + var sourceProjection = this.getProjection(); + if (!ol.ENABLE_RASTER_REPROJECTION || + !goog.isDefAndNotNull(sourceProjection) || + !goog.isDefAndNotNull(projection) || + ol.proj.equivalent(sourceProjection, projection)) { + if (!goog.isNull(sourceProjection)) { + projection = sourceProjection; + } + return this.getImageInternal(extent, resolution, pixelRatio, projection); + } else { + var image = new ol.reproj.Image( + sourceProjection, projection, extent, resolution, pixelRatio, + goog.bind(function(extent, resolution, pixelRatio) { + return this.getImageInternal(extent, resolution, + pixelRatio, sourceProjection); + }, this)); + + return image; + } +}; + + +/** + * @param {ol.Extent} extent Extent. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {ol.proj.Projection} projection Projection. + * @return {ol.ImageBase} Single image. + * @protected + */ +ol.source.Image.prototype.getImageInternal = goog.abstractMethod; /** From ed1e49045a2260de0253c052cafd0b1fb568ca59 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 11 Jun 2015 16:28:19 +0200 Subject: [PATCH 22/80] Allow different source and view projection in image layer renderers To enable image layer reprojection. --- src/ol/renderer/canvas/canvasimagelayerrenderer.js | 12 +++++++----- src/ol/renderer/dom/domimagelayerrenderer.js | 12 +++++++----- src/ol/renderer/layerrenderer.js | 5 +++-- src/ol/renderer/webgl/webglimagelayerrenderer.js | 12 +++++++----- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/ol/renderer/canvas/canvasimagelayerrenderer.js b/src/ol/renderer/canvas/canvasimagelayerrenderer.js index c752dd4630..22fae30287 100644 --- a/src/ol/renderer/canvas/canvasimagelayerrenderer.js +++ b/src/ol/renderer/canvas/canvasimagelayerrenderer.js @@ -170,11 +170,13 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] && !ol.extent.isEmpty(renderedExtent)) { var projection = viewState.projection; - var sourceProjection = imageSource.getProjection(); - if (sourceProjection) { - goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), - 'projection and sourceProjection are equivalent'); - projection = sourceProjection; + if (!ol.ENABLE_RASTER_REPROJECTION) { + var sourceProjection = imageSource.getProjection(); + if (sourceProjection) { + goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), + 'projection and sourceProjection are equivalent'); + projection = sourceProjection; + } } image = imageSource.getImage( renderedExtent, viewResolution, pixelRatio, projection); diff --git a/src/ol/renderer/dom/domimagelayerrenderer.js b/src/ol/renderer/dom/domimagelayerrenderer.js index 29308d3ee2..033de8614b 100644 --- a/src/ol/renderer/dom/domimagelayerrenderer.js +++ b/src/ol/renderer/dom/domimagelayerrenderer.js @@ -101,11 +101,13 @@ ol.renderer.dom.ImageLayer.prototype.prepareFrame = if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] && !ol.extent.isEmpty(renderedExtent)) { var projection = viewState.projection; - var sourceProjection = imageSource.getProjection(); - if (sourceProjection) { - goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), - 'projection and sourceProjection are equivalent'); - projection = sourceProjection; + if (!ol.ENABLE_RASTER_REPROJECTION) { + var sourceProjection = imageSource.getProjection(); + if (sourceProjection) { + goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), + 'projection and sourceProjection are equivalent'); + projection = sourceProjection; + } } var image_ = imageSource.getImage(renderedExtent, viewResolution, frameState.pixelRatio, projection); diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index e3d368b2e8..cd0dba0093 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -159,8 +159,9 @@ ol.renderer.Layer.prototype.loadImage = function(image) { if (imageState == ol.ImageState.IDLE) { image.load(); imageState = image.getState(); - goog.asserts.assert(imageState == ol.ImageState.LOADING, - 'imageState is "loading"'); + goog.asserts.assert(imageState == ol.ImageState.LOADING || + imageState == ol.ImageState.LOADED, + 'imageState is "loading" or "loaded"'); } return imageState == ol.ImageState.LOADED; }; diff --git a/src/ol/renderer/webgl/webglimagelayerrenderer.js b/src/ol/renderer/webgl/webglimagelayerrenderer.js index 317544fc66..8aad156335 100644 --- a/src/ol/renderer/webgl/webglimagelayerrenderer.js +++ b/src/ol/renderer/webgl/webglimagelayerrenderer.js @@ -125,11 +125,13 @@ ol.renderer.webgl.ImageLayer.prototype.prepareFrame = if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] && !ol.extent.isEmpty(renderedExtent)) { var projection = viewState.projection; - var sourceProjection = imageSource.getProjection(); - if (sourceProjection) { - goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), - 'projection and sourceProjection are equivalent'); - projection = sourceProjection; + if (!ol.ENABLE_RASTER_REPROJECTION) { + var sourceProjection = imageSource.getProjection(); + if (sourceProjection) { + goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), + 'projection and sourceProjection are equivalent'); + projection = sourceProjection; + } } var image_ = imageSource.getImage(renderedExtent, viewResolution, pixelRatio, projection); From 0f408e341f95853b97b261165cc53f4f49f512c7 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 11 Jun 2015 16:29:50 +0200 Subject: [PATCH 23/80] Rename ol.source.Image#getImage implementations to #getImageInternal This allows for the reprojection to be initialized in the #getImage, while #getImageInternal is used to obtain the actual data. --- src/ol/source/imagecanvassource.js | 2 +- src/ol/source/imagemapguidesource.js | 2 +- src/ol/source/imagestaticsource.js | 2 +- src/ol/source/imagewmssource.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ol/source/imagecanvassource.js b/src/ol/source/imagecanvassource.js index 8532e8b313..57f3d8b5c3 100644 --- a/src/ol/source/imagecanvassource.js +++ b/src/ol/source/imagecanvassource.js @@ -59,7 +59,7 @@ goog.inherits(ol.source.ImageCanvas, ol.source.Image); /** * @inheritDoc */ -ol.source.ImageCanvas.prototype.getImage = +ol.source.ImageCanvas.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { resolution = this.findNearestResolution(resolution); diff --git a/src/ol/source/imagemapguidesource.js b/src/ol/source/imagemapguidesource.js index 031becffb9..2ff6a92701 100644 --- a/src/ol/source/imagemapguidesource.js +++ b/src/ol/source/imagemapguidesource.js @@ -126,7 +126,7 @@ ol.source.ImageMapGuide.prototype.getParams = function() { /** * @inheritDoc */ -ol.source.ImageMapGuide.prototype.getImage = +ol.source.ImageMapGuide.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { resolution = this.findNearestResolution(resolution); pixelRatio = this.hidpi_ ? pixelRatio : 1; diff --git a/src/ol/source/imagestaticsource.js b/src/ol/source/imagestaticsource.js index 8bffee68fe..f6e57ce82d 100644 --- a/src/ol/source/imagestaticsource.js +++ b/src/ol/source/imagestaticsource.js @@ -62,7 +62,7 @@ goog.inherits(ol.source.ImageStatic, ol.source.Image); /** * @inheritDoc */ -ol.source.ImageStatic.prototype.getImage = +ol.source.ImageStatic.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { if (ol.extent.intersects(extent, this.image_.getExtent())) { return this.image_; diff --git a/src/ol/source/imagewmssource.js b/src/ol/source/imagewmssource.js index e9b49b95fd..72eabf98fb 100644 --- a/src/ol/source/imagewmssource.js +++ b/src/ol/source/imagewmssource.js @@ -185,7 +185,7 @@ ol.source.ImageWMS.prototype.getParams = function() { /** * @inheritDoc */ -ol.source.ImageWMS.prototype.getImage = +ol.source.ImageWMS.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { if (this.url_ === undefined) { From 6482ccf2f76a3acdc09b2b3fcd6a812c645f44ea Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 4 Jun 2015 14:52:42 +0200 Subject: [PATCH 24/80] Make ol.reproj.Triangulation a class --- src/ol/reproj/image.js | 12 +- src/ol/reproj/reproj.js | 2 +- src/ol/reproj/tile.js | 39 ++-- src/ol/reproj/triangulation.js | 324 ++++++++++++++++++--------------- 4 files changed, 211 insertions(+), 166 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index f842ae3be8..246dceb873 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -9,7 +9,7 @@ goog.require('ol.dom'); goog.require('ol.extent'); goog.require('ol.proj'); goog.require('ol.reproj'); -goog.require('ol.reproj.triangulation'); +goog.require('ol.reproj.Triangulation'); @@ -55,14 +55,15 @@ ol.reproj.Image = function(sourceProj, targetProj, this.maxSourceExtent_ = sourceProj.getExtent(); var maxTargetExtent = targetProj.getExtent(); + var limitedTargetExtent = ol.extent.getIntersection( + targetExtent, maxTargetExtent); /** * @private * @type {!ol.reproj.Triangulation} */ - this.triangulation_ = ol.reproj.triangulation.createForExtent( - targetExtent, sourceProj, targetProj, - maxTargetExtent, this.maxSourceExtent_); + this.triangulation_ = new ol.reproj.Triangulation( + sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_); /** * @private @@ -76,8 +77,7 @@ ol.reproj.Image = function(sourceProj, targetProj, */ this.targetExtent_ = targetExtent; - var srcExtent = ol.reproj.triangulation.getSourceExtent( - this.triangulation_, sourceProj); + var srcExtent = this.triangulation_.calculateSourceExtent(); var targetCenter = ol.extent.getCenter(targetExtent); var sourceResolution = ol.reproj.calculateSourceResolution( diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 97b336c680..d29d5d52a1 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -66,7 +66,7 @@ ol.reproj.renderTriangles = function(context, (sourceExtent[0] + sourceExtent[2]) / 2 : null; var targetTL = ol.extent.getTopLeft(targetExtent); - goog.array.forEach(triangulation.triangles, function(tri, i, arr) { + goog.array.forEach(triangulation.getTriangles(), function(tri, i, arr) { context.save(); /* Calculate affine transform (src -> dst) diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index 9f65a23da5..f3626fb2df 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -12,7 +12,7 @@ goog.require('ol.dom'); goog.require('ol.extent'); goog.require('ol.proj'); goog.require('ol.reproj'); -goog.require('ol.reproj.triangulation'); +goog.require('ol.reproj.Triangulation'); @@ -62,14 +62,6 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, var maxTargetExtent = this.targetTileGrid_.getExtent(); var maxSourceExtent = this.sourceTileGrid_.getExtent(); - /** - * @private - * @type {!ol.reproj.Triangulation} - */ - this.triangulation_ = ol.reproj.triangulation.createForExtent( - targetExtent, sourceProj, targetProj, - maxTargetExtent, maxSourceExtent); - /** * @private * @type {!Array.} @@ -88,21 +80,39 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, */ this.srcZ_ = 0; - if (!ol.extent.intersects(maxTargetExtent, targetExtent)) { + var limitedTargetExtent = ol.extent.getIntersection( + targetExtent, maxTargetExtent); + + if (ol.extent.getArea(limitedTargetExtent) === 0) { // Tile is completely outside range -> EMPTY // TODO: is it actually correct that the source even creates the tile ? this.state = ol.TileState.EMPTY; return; } - if (this.triangulation_.triangles.length === 0) { + var targetResolution = targetTileGrid.getResolution(z); + + var errorThresholdInPixels = 0.5; + + // in source units + var errorThreshold = targetResolution * errorThresholdInPixels * + targetProj.getMetersPerUnit() / sourceProj.getMetersPerUnit(); + + /** + * @private + * @type {!ol.reproj.Triangulation} + */ + this.triangulation_ = new ol.reproj.Triangulation( + sourceProj, targetProj, limitedTargetExtent, maxSourceExtent, + 5, errorThreshold); + + if (this.triangulation_.getTriangles().length === 0) { // no valid triangles -> EMPTY this.state = ol.TileState.EMPTY; return; } - var targetCenter = ol.extent.getCenter(targetExtent); - var targetResolution = targetTileGrid.getResolution(z); + var targetCenter = ol.extent.getCenter(limitedTargetExtent); var sourceResolution = ol.reproj.calculateSourceResolution( sourceProj, targetProj, targetCenter, targetResolution); @@ -114,8 +124,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, } this.srcZ_ = sourceTileGrid.getZForResolution(sourceResolution); - var srcExtent = ol.reproj.triangulation.getSourceExtent( - this.triangulation_, sourceProj); + var srcExtent = this.triangulation_.calculateSourceExtent(); var sourceProjExtent = sourceProj.getExtent(); if (!sourceProj.isGlobal() && sourceProjExtent) { diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 77b4b24406..7e19ab844c 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -1,9 +1,7 @@ goog.provide('ol.reproj.Triangulation'); -goog.provide('ol.reproj.triangulation'); goog.require('goog.array'); goog.require('goog.math'); -goog.require('ol.coordinate'); goog.require('ol.extent'); goog.require('ol.proj'); @@ -21,194 +19,224 @@ goog.require('ol.proj'); ol.reproj.Triangle; + /** - * `needsShift` indicates that _any_ of the triangles has to be shifted during - * reprojection. See {@link ol.reproj.Triangle}. - * @typedef {{triangles: Array., - * needsShift: boolean}} + * @param {ol.proj.Projection} sourceProj + * @param {ol.proj.Projection} targetProj + * @param {ol.Extent} targetExtent + * @param {ol.Extent=} opt_maxSourceExtent + * @param {number=} opt_maxSubdiv Maximal subdivision. + * @param {number=} opt_errorThreshold Acceptable error (in source units). + * @constructor */ -ol.reproj.Triangulation; +ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, + opt_maxSourceExtent, opt_maxSubdiv, opt_errorThreshold) { + + /** + * @type {ol.proj.Projection} + * @private + */ + this.sourceProj_ = sourceProj; + + /** + * @type {ol.proj.Projection} + * @private + */ + this.targetProj_ = targetProj; + + /** + * @type {ol.TransformFunction} + * @private + */ + this.transformInv_ = ol.proj.getTransform(this.targetProj_, this.sourceProj_); + + /** + * @type {ol.Extent} + * @private + */ + this.maxSourceExtent_ = goog.isDef(opt_maxSourceExtent) ? + opt_maxSourceExtent : null; + + var errorThreshold = goog.isDef(opt_errorThreshold) ? + opt_errorThreshold : 0; //TODO: define + /** + * @type {number} + * @private + */ + this.errorThresholdSquared_ = errorThreshold * errorThreshold; + + /** + * @type {Array.} + * @private + */ + this.triangles_ = []; + + /** + * Indicates that _any_ of the triangles has to be shifted during + * reprojection. See {@link ol.reproj.Triangle}. + * @type {boolean} + * @private + */ + this.needsShift_ = false; + + /** + * @type {number} + * @private + */ + this.sourceWorldWidth_ = ol.extent.getWidth(this.sourceProj_.getExtent()); + + var tlDst = ol.extent.getTopLeft(targetExtent); + var trDst = ol.extent.getTopRight(targetExtent); + var brDst = ol.extent.getBottomRight(targetExtent); + var blDst = ol.extent.getBottomLeft(targetExtent); + var tlDstSrc = this.transformInv_(tlDst); + var trDstSrc = this.transformInv_(trDst); + var brDstSrc = this.transformInv_(brDst); + var blDstSrc = this.transformInv_(blDst); + + this.addQuadIfValid_(tlDst, trDst, brDst, blDst, + tlDstSrc, trDstSrc, brDstSrc, blDstSrc, + opt_maxSubdiv || 0); +}; /** - * Adds triangle to the triangulation (and reprojects the vertices) if valid. - * @private - * @param {ol.reproj.Triangulation} triangulation + * Adds triangle to the triangulation. * @param {ol.Coordinate} a * @param {ol.Coordinate} b * @param {ol.Coordinate} c - * @param {ol.proj.Projection} sourceProj - * @param {ol.proj.Projection} targetProj - * @param {ol.Extent=} opt_maxTargetExtent - * @param {ol.Extent=} opt_maxSourceExtent + * @param {ol.Coordinate} aSrc + * @param {ol.Coordinate} bSrc + * @param {ol.Coordinate} cSrc + * @param {boolean} wrapsX + * @private */ -ol.reproj.triangulation.addTriangleIfValid_ = function(triangulation, a, b, c, - sourceProj, targetProj, opt_maxTargetExtent, opt_maxSourceExtent) { - if (goog.isDefAndNotNull(opt_maxTargetExtent)) { - if (!ol.extent.containsCoordinate(opt_maxTargetExtent, a) && - !ol.extent.containsCoordinate(opt_maxTargetExtent, b) && - !ol.extent.containsCoordinate(opt_maxTargetExtent, c)) { - // whole triangle outside target projection extent -> ignore - return; - } - // clamp the vertices to the extent edges before transforming - a = ol.extent.closestCoordinate(opt_maxTargetExtent, a); - b = ol.extent.closestCoordinate(opt_maxTargetExtent, b); - c = ol.extent.closestCoordinate(opt_maxTargetExtent, c); - } - var transformInv = ol.proj.getTransform(targetProj, sourceProj); - var aSrc = transformInv(a); - var bSrc = transformInv(b); - var cSrc = transformInv(c); - if (goog.isDefAndNotNull(opt_maxSourceExtent)) { - var srcTriangleExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc]); - if (!ol.extent.intersects(srcTriangleExtent, opt_maxSourceExtent)) { - // whole triangle outside source projection extent -> ignore - // TODO: intersect triangle with the extent rather than bbox ? - return; - } - if (!ol.extent.containsCoordinate(opt_maxSourceExtent, aSrc) || - !ol.extent.containsCoordinate(opt_maxSourceExtent, bSrc) || - !ol.extent.containsCoordinate(opt_maxSourceExtent, cSrc)) { - // if any vertex is outside projection range, modify the target triangle - // TODO: do not do closestCoordinate, but rather scale the triangle with - // respect to a point inside the extent - aSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, aSrc); - bSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, bSrc); - cSrc = ol.extent.closestCoordinate(opt_maxSourceExtent, cSrc); +ol.reproj.Triangulation.prototype.addTriangle_ = function(a, b, c, + aSrc, bSrc, cSrc, wrapsX) { + // wrapsX could be determined by transforming centroid of the target triangle. + // If the transformed centroid is outside the transformed triangle, + // the triangle wraps around projection extent. + // However, this would require additional transformation call. - var makeFinite = function(coord, extent) { - if (!goog.math.isFiniteNumber(coord[0])) { - coord[0] = goog.math.clamp(coord[0], extent[0], extent[2]); - } - if (!goog.math.isFiniteNumber(coord[1])) { - coord[1] = goog.math.clamp(coord[1], extent[1], extent[3]); - } - }; - makeFinite(aSrc, opt_maxSourceExtent); - makeFinite(bSrc, opt_maxSourceExtent); - makeFinite(cSrc, opt_maxSourceExtent); - - var transformFwd = ol.proj.getTransform(sourceProj, targetProj); - a = transformFwd(aSrc); - b = transformFwd(bSrc); - c = transformFwd(cSrc); - } - } - var needsShift = false; - if (sourceProj.canWrapX()) { - // determine if the triangle crosses the dateline here - // This can be detected by transforming centroid of the target triangle. - // If the transformed centroid is outside the transformed triangle, - // the triangle wraps around projection extent. - - var centroid = [(a[0] + b[0] + c[0]) / 3, - (a[1] + b[1] + c[1]) / 3]; - var centroidSrc = transformInv(centroid); - - if (!ol.coordinate.isInTriangle(centroidSrc, aSrc, bSrc, cSrc)) { - needsShift = true; - } - } - triangulation.triangles.push({ + this.triangles_.push({ source: [aSrc, bSrc, cSrc], target: [a, b, c], - needsShift: needsShift + needsShift: wrapsX }); - if (needsShift) { - triangulation.needsShift = true; + if (wrapsX) { + this.needsShift_ = true; } }; /** - * Triangulates given extent and reprojects vertices. - * TODO: improved triangulation, better error handling of some trans fails - * @param {ol.Extent} extent - * @param {ol.proj.Projection} sourceProj - * @param {ol.proj.Projection} targetProj - * @param {ol.Extent=} opt_maxTargetExtent - * @param {ol.Extent=} opt_maxSourceExtent - * @param {number=} opt_subdiv Subdivision factor (default 4). - * @return {ol.reproj.Triangulation} + * Adds quad (points in clock-wise order) to the triangulation + * (and reprojects the vertices) if valid. + * @param {ol.Coordinate} a + * @param {ol.Coordinate} b + * @param {ol.Coordinate} c + * @param {ol.Coordinate} d + * @param {ol.Coordinate} aSrc + * @param {ol.Coordinate} bSrc + * @param {ol.Coordinate} cSrc + * @param {ol.Coordinate} dSrc + * @param {number} maxSubdiv Maximal allowed subdivision of the quad. + * @private */ -ol.reproj.triangulation.createForExtent = function(extent, sourceProj, - targetProj, opt_maxTargetExtent, opt_maxSourceExtent, opt_subdiv) { +ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, + aSrc, bSrc, cSrc, dSrc, maxSubdiv) { - var triangulation = { - triangles: [], - needsShift: false - }; + var srcQuadExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc, dSrc]); + if (!goog.isNull(this.maxSourceExtent_)) { + if (!ol.extent.intersects(srcQuadExtent, this.maxSourceExtent_)) { + // whole quad outside source projection extent -> ignore + return; + } + } + var srcCoverageX = ol.extent.getWidth(srcQuadExtent) / this.sourceWorldWidth_; - var tlDst = ol.extent.getTopLeft(extent); - var brDst = ol.extent.getBottomRight(extent); + // when the quad is wrapped in the source projection + // it covers most of the projection extent, but not fully + var wrapsX = this.sourceProj_.canWrapX() && + srcCoverageX > 0.5 && srcCoverageX < 1; - var subdiv = opt_subdiv || 4; - for (var y = 0; y < subdiv; y++) { - for (var x = 0; x < subdiv; x++) { - // do 2 triangle: [(x, y), (x + 1, y + 1), (x, y + 1)] - // [(x, y), (x + 1, y), (x + 1, y + 1)] + if (maxSubdiv > 0) { + var needsSubdivision = !wrapsX && this.sourceProj_.isGlobal() && + srcCoverageX > 0.25; //TODO: define - var x0y0dst = [ - goog.math.lerp(tlDst[0], brDst[0], x / subdiv), - goog.math.lerp(tlDst[1], brDst[1], y / subdiv) - ]; - var x1y0dst = [ - goog.math.lerp(tlDst[0], brDst[0], (x + 1) / subdiv), - goog.math.lerp(tlDst[1], brDst[1], y / subdiv) - ]; - var x0y1dst = [ - goog.math.lerp(tlDst[0], brDst[0], x / subdiv), - goog.math.lerp(tlDst[1], brDst[1], (y + 1) / subdiv) - ]; - var x1y1dst = [ - goog.math.lerp(tlDst[0], brDst[0], (x + 1) / subdiv), - goog.math.lerp(tlDst[1], brDst[1], (y + 1) / subdiv) - ]; + var center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2]; + var centerSrc = this.transformInv_(center); - ol.reproj.triangulation.addTriangleIfValid_( - triangulation, x0y0dst, x1y1dst, x0y1dst, - sourceProj, targetProj, opt_maxTargetExtent, opt_maxSourceExtent); - ol.reproj.triangulation.addTriangleIfValid_( - triangulation, x0y0dst, x1y0dst, x1y1dst, - sourceProj, targetProj, opt_maxTargetExtent, opt_maxSourceExtent); + if (!needsSubdivision) { + var dx; + if (wrapsX) { + var centerSrcEstimX = + (goog.math.modulo(aSrc[0], this.sourceWorldWidth_) + + goog.math.modulo(cSrc[0], this.sourceWorldWidth_)) / 2; + dx = centerSrcEstimX - + goog.math.modulo(centerSrc[0], this.sourceWorldWidth_); + } else { + dx = (aSrc[0] + cSrc[0]) / 2 - centerSrc[0]; + } + var dy = (aSrc[1] + cSrc[1]) / 2 - centerSrc[1]; + var centerSrcErrorSquared = dx * dx + dy * dy; + needsSubdivision = centerSrcErrorSquared > this.errorThresholdSquared_; + } + if (needsSubdivision) { + var ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; + var abSrc = this.transformInv_(ab); + var bc = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2]; + var bcSrc = this.transformInv_(bc); + var cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2]; + var cdSrc = this.transformInv_(cd); + var da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2]; + var daSrc = this.transformInv_(da); + + this.addQuadIfValid_(a, ab, center, da, + aSrc, abSrc, centerSrc, daSrc, maxSubdiv - 1); + this.addQuadIfValid_(ab, b, bc, center, + abSrc, bSrc, bcSrc, centerSrc, maxSubdiv - 1); + this.addQuadIfValid_(center, bc, c, cd, + centerSrc, bcSrc, cSrc, cdSrc, maxSubdiv - 1); + this.addQuadIfValid_(da, center, cd, d, + daSrc, centerSrc, cdSrc, dSrc, maxSubdiv - 1); + + return; } } - return triangulation; + this.addTriangle_(a, c, d, aSrc, cSrc, dSrc, wrapsX); + this.addTriangle_(a, b, c, aSrc, bSrc, cSrc, wrapsX); }; /** - * @param {ol.reproj.Triangulation} triangulation - * @param {ol.proj.Projection} sourceProj * @return {ol.Extent} */ -ol.reproj.triangulation.getSourceExtent = function(triangulation, sourceProj) { +ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { var extent = ol.extent.createEmpty(); - if (triangulation.needsShift) { + if (this.needsShift_) { // although only some of the triangles are crossing the dateline, // all coordiantes need to be "shifted" to be positive // to properly calculate the extent (and then possibly shifted back) - var sourceProjExtent = sourceProj.getExtent(); - var sourceProjWidth = ol.extent.getWidth(sourceProjExtent); - goog.array.forEach(triangulation.triangles, function(triangle, i, arr) { + goog.array.forEach(this.triangles_, function(triangle, i, arr) { var src = triangle.source; ol.extent.extendCoordinate(extent, - [goog.math.modulo(src[0][0], sourceProjWidth), src[0][1]]); + [goog.math.modulo(src[0][0], this.sourceWorldWidth_), src[0][1]]); ol.extent.extendCoordinate(extent, - [goog.math.modulo(src[1][0], sourceProjWidth), src[1][1]]); + [goog.math.modulo(src[1][0], this.sourceWorldWidth_), src[1][1]]); ol.extent.extendCoordinate(extent, - [goog.math.modulo(src[2][0], sourceProjWidth), src[2][1]]); - }); + [goog.math.modulo(src[2][0], this.sourceWorldWidth_), src[2][1]]); + }, this); + var sourceProjExtent = this.sourceProj_.getExtent(); var right = sourceProjExtent[2]; - if (extent[0] > right) extent[0] -= sourceProjWidth; - if (extent[2] > right) extent[2] -= sourceProjWidth; + if (extent[0] > right) extent[0] -= this.sourceWorldWidth_; + if (extent[2] > right) extent[2] -= this.sourceWorldWidth_; } else { - goog.array.forEach(triangulation.triangles, function(triangle, i, arr) { + goog.array.forEach(this.triangles_, function(triangle, i, arr) { var src = triangle.source; ol.extent.extendCoordinate(extent, src[0]); ol.extent.extendCoordinate(extent, src[1]); @@ -218,3 +246,11 @@ ol.reproj.triangulation.getSourceExtent = function(triangulation, sourceProj) { return extent; }; + + +/** + * @return {Array.} + */ +ol.reproj.Triangulation.prototype.getTriangles = function() { + return this.triangles_; +}; From 8ab197eba6e8e880297bcee4b423a6e5fde72ce4 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 9 Jul 2015 18:05:54 +0200 Subject: [PATCH 25/80] Optimize the reprojection by stitching the sources prior to rendering This solves canvas antialiasing issues and also simplifies the code handling wrapX in the source projection. --- src/ol/reproj/reproj.js | 115 ++++++++++++++++++++++++--------- src/ol/reproj/triangulation.js | 56 +++++++++------- 2 files changed, 117 insertions(+), 54 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index d29d5d52a1..c302095568 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -2,6 +2,7 @@ goog.provide('ol.reproj'); goog.require('goog.array'); goog.require('goog.math'); +goog.require('ol.dom'); goog.require('ol.extent'); goog.require('ol.math'); goog.require('ol.proj'); @@ -60,10 +61,62 @@ ol.reproj.renderTriangles = function(context, sourceResolution, sourceExtent, targetResolution, targetExtent, triangulation, sources) { - var shiftDistance = !goog.isNull(sourceExtent) ? - ol.extent.getWidth(sourceExtent) : null; - var shiftThreshold = !goog.isNull(sourceExtent) ? - (sourceExtent[0] + sourceExtent[2]) / 2 : null; + var wrapXShiftDistance = !goog.isNull(sourceExtent) ? + ol.extent.getWidth(sourceExtent) : 0; + + var wrapXShiftNeeded = triangulation.getWrapsXInSource() && + (wrapXShiftDistance > 0); + + // If possible, stitch the sources shifted to solve the wrapX issue here. + // This is not possible if crossing both "dateline" and "prime meridian". + var performGlobalWrapXShift = false; + if (wrapXShiftNeeded) { + var triangulationSrcExtent = triangulation.calculateSourceExtent(); + var triangulationSrcWidth = goog.math.modulo( + ol.extent.getWidth(triangulationSrcExtent), wrapXShiftDistance); + performGlobalWrapXShift = triangulationSrcWidth < wrapXShiftDistance / 2; + } + + var srcDataExtent = ol.extent.createEmpty(); + goog.array.forEach(sources, function(src, i, arr) { + if (performGlobalWrapXShift) { + var srcW = src.extent[2] - src.extent[0]; + var srcX = goog.math.modulo(src.extent[0], wrapXShiftDistance); + ol.extent.extend(srcDataExtent, [srcX, src.extent[1], + srcX + srcW, src.extent[3]]); + } else { + ol.extent.extend(srcDataExtent, src.extent); + } + }); + if (!goog.isNull(sourceExtent)) { + if (wrapXType == ol.reproj.WrapXRendering_.NONE) { + srcDataExtent[0] = goog.math.clamp( + srcDataExtent[0], sourceExtent[0], sourceExtent[2]); + srcDataExtent[2] = goog.math.clamp( + srcDataExtent[2], sourceExtent[0], sourceExtent[2]); + } + srcDataExtent[1] = goog.math.clamp( + srcDataExtent[1], sourceExtent[1], sourceExtent[3]); + srcDataExtent[3] = goog.math.clamp( + srcDataExtent[3], sourceExtent[1], sourceExtent[3]); + } + + var srcDataWidth = ol.extent.getWidth(srcDataExtent); + var srcDataHeight = ol.extent.getHeight(srcDataExtent); + var stitchContext = ol.dom.createCanvasContext2D( + Math.ceil(srcDataWidth / sourceResolution), + Math.ceil(srcDataHeight / sourceResolution)); + + stitchContext.scale(1 / sourceResolution, 1 / sourceResolution); + stitchContext.translate(-srcDataExtent[0], srcDataExtent[3]); + + goog.array.forEach(sources, function(src, i, arr) { + var xPos = performGlobalWrapXShift ? + goog.math.modulo(src.extent[0], wrapXShiftDistance) : src.extent[0]; + stitchContext.drawImage(src.image, xPos, -src.extent[3], + src.extent[2] - src.extent[0], src.extent[3] - src.extent[1]); + }); + var targetTL = ol.extent.getTopLeft(targetExtent); goog.array.forEach(triangulation.getTriangles(), function(tri, i, arr) { @@ -96,23 +149,28 @@ ol.reproj.renderTriangles = function(context, var u0 = tgt[0][0] - targetTL[0], v0 = -(tgt[0][1] - targetTL[1]), u1 = tgt[1][0] - targetTL[0], v1 = -(tgt[1][1] - targetTL[1]), u2 = tgt[2][0] - targetTL[0], v2 = -(tgt[2][1] - targetTL[1]); - if (tri.needsShift && !goog.isNull(shiftDistance)) { - x0 = goog.math.modulo(x0, shiftDistance); - x1 = goog.math.modulo(x1, shiftDistance); - x2 = goog.math.modulo(x2, shiftDistance); + + var performIndividualWrapXShift = !performGlobalWrapXShift && + (wrapXShiftNeeded && + (Math.max(x0, x1, x2) - Math.min(x0, x1, x2)) > wrapXShiftDistance / 2); + + if (performGlobalWrapXShift || performIndividualWrapXShift) { + x0 = goog.math.modulo(x0, wrapXShiftDistance); + x1 = goog.math.modulo(x1, wrapXShiftDistance); + x2 = goog.math.modulo(x2, wrapXShiftDistance); } // Shift all the source points to improve numerical stability // of all the subsequent calculations. // The [x0, y0] is used here, because it should achieve reasonable results // but any values could actually be chosen. - var srcShiftX = x0, srcShiftY = y0; + var srcNumericalShiftX = x0, srcNumericalShiftY = y0; x0 = 0; y0 = 0; - x1 -= srcShiftX; - y1 -= srcShiftY; - x2 -= srcShiftX; - y2 -= srcShiftY; + x1 -= srcNumericalShiftX; + y1 -= srcNumericalShiftY; + x2 -= srcNumericalShiftX; + y2 -= srcNumericalShiftY; var augmentedMatrix = [ [x0, y0, 1, 0, 0, 0, u0 / targetResolution], @@ -155,27 +213,22 @@ ol.reproj.renderTriangles = function(context, context.closePath(); context.clip(); - goog.array.forEach(sources, function(src, i, arr) { - context.save(); - var dataTL = ol.extent.getTopLeft(src.extent); - context.translate(dataTL[0] - srcShiftX, dataTL[1] - srcShiftY); + context.save(); + context.translate(srcDataExtent[0] - srcNumericalShiftX, + srcDataExtent[3] - srcNumericalShiftY); - // if the triangle needs to be shifted (because of the dateline wrapping), - // shift back only the source images that need it - if (tri.needsShift && !goog.isNull(shiftDistance) && - dataTL[0] < shiftThreshold) { - context.translate(shiftDistance, 0); - } - context.scale(sourceResolution, -sourceResolution); + context.scale(sourceResolution, -sourceResolution); - // the image has to be scaled by half a pixel in every direction - // in order to prevent artifacts between the original tiles - // that are introduced by the canvas antialiasing. - context.drawImage(src.image, -0.5, -0.5, - src.image.width + 1, src.image.height + 1); + context.drawImage(stitchContext.canvas, 0, 0); - context.restore(); - }); + if (performIndividualWrapXShift) { + // It was not possible to solve the wrapX shifting during stitching -> + // render the data second time (shifted) to solve the wrapX. + context.translate(wrapXShiftDistance / sourceResolution, 0); + context.drawImage(stitchContext.canvas, 0, 0); + } + + context.restore(); if (goog.DEBUG) { context.strokeStyle = 'black'; diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 7e19ab844c..384297ef44 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -8,13 +8,9 @@ goog.require('ol.proj'); /** * Single triangle; consists of 3 source points and 3 target points. - * `needsShift` can be used to indicate that the whole triangle has to be - * shifted during reprojection. This is needed for triangles crossing edges - * of the source projection (dateline). * * @typedef {{source: Array., - * target: Array., - * needsShift: boolean}} + * target: Array.}} */ ol.reproj.Triangle; @@ -72,12 +68,19 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, this.triangles_ = []; /** - * Indicates that _any_ of the triangles has to be shifted during - * reprojection. See {@link ol.reproj.Triangle}. + * @type {ol.Extent} + * @private + */ + this.trianglesSourceExtent_ = null; + + /** + * Indicates that source coordinates has to be shifted during reprojection. + * This is needed when the triangulation crosses + * edge of the source projection (dateline). * @type {boolean} * @private */ - this.needsShift_ = false; + this.wrapsXInSource_ = false; /** * @type {number} @@ -108,24 +111,14 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, * @param {ol.Coordinate} aSrc * @param {ol.Coordinate} bSrc * @param {ol.Coordinate} cSrc - * @param {boolean} wrapsX * @private */ ol.reproj.Triangulation.prototype.addTriangle_ = function(a, b, c, - aSrc, bSrc, cSrc, wrapsX) { - // wrapsX could be determined by transforming centroid of the target triangle. - // If the transformed centroid is outside the transformed triangle, - // the triangle wraps around projection extent. - // However, this would require additional transformation call. - + aSrc, bSrc, cSrc) { this.triangles_.push({ source: [aSrc, bSrc, cSrc], - target: [a, b, c], - needsShift: wrapsX + target: [a, b, c] }); - if (wrapsX) { - this.needsShift_ = true; - } }; @@ -205,8 +198,12 @@ ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, } } - this.addTriangle_(a, c, d, aSrc, cSrc, dSrc, wrapsX); - this.addTriangle_(a, b, c, aSrc, bSrc, cSrc, wrapsX); + if (wrapsX) { + this.wrapsXInSource_ = true; + } + + this.addTriangle_(a, c, d, aSrc, cSrc, dSrc); + this.addTriangle_(a, b, c, aSrc, bSrc, cSrc); }; @@ -214,9 +211,13 @@ ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, * @return {ol.Extent} */ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { + if (!goog.isNull(this.trianglesSourceExtent_)) { + return this.trianglesSourceExtent_; + } + var extent = ol.extent.createEmpty(); - if (this.needsShift_) { + if (this.wrapsXInSource_) { // although only some of the triangles are crossing the dateline, // all coordiantes need to be "shifted" to be positive // to properly calculate the extent (and then possibly shifted back) @@ -244,10 +245,19 @@ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { }); } + this.trianglesSourceExtent_ = extent; return extent; }; +/** + * @return {boolean} + */ +ol.reproj.Triangulation.prototype.getWrapsXInSource = function() { + return this.wrapsXInSource_; +}; + + /** * @return {Array.} */ From 30cd0aa5849e73d106d250b41167bdf961c8804c Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 9 Jul 2015 18:19:10 +0200 Subject: [PATCH 26/80] Improve reprojection performance Utilize the numerical shifting during reprojection to easily reduce complexity of the linear system that needs to be solved. --- src/ol/reproj/reproj.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index c302095568..8d7e835db9 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -161,9 +161,8 @@ ol.reproj.renderTriangles = function(context, } // Shift all the source points to improve numerical stability - // of all the subsequent calculations. - // The [x0, y0] is used here, because it should achieve reasonable results - // but any values could actually be chosen. + // of all the subsequent calculations. The [x0, y0] is used here. + // This is also used to simplify the linear system. var srcNumericalShiftX = x0, srcNumericalShiftY = y0; x0 = 0; y0 = 0; @@ -173,20 +172,18 @@ ol.reproj.renderTriangles = function(context, y2 -= srcNumericalShiftY; var augmentedMatrix = [ - [x0, y0, 1, 0, 0, 0, u0 / targetResolution], - [x1, y1, 1, 0, 0, 0, u1 / targetResolution], - [x2, y2, 1, 0, 0, 0, u2 / targetResolution], - [0, 0, 0, x0, y0, 1, v0 / targetResolution], - [0, 0, 0, x1, y1, 1, v1 / targetResolution], - [0, 0, 0, x2, y2, 1, v2 / targetResolution] + [x1, y1, 0, 0, (u1 - u0) / targetResolution], + [x2, y2, 0, 0, (u2 - u0) / targetResolution], + [0, 0, x1, y1, (v1 - v0) / targetResolution], + [0, 0, x2, y2, (v2 - v0) / targetResolution] ]; var coefs = ol.math.solveLinearSystem(augmentedMatrix); if (goog.isNull(coefs)) { return; } - context.setTransform(coefs[0], coefs[3], coefs[1], - coefs[4], coefs[2], coefs[5]); + context.setTransform(coefs[0], coefs[2], coefs[1], coefs[3], + u0 / targetResolution, v0 / targetResolution); var pixelSize = sourceResolution; var centroid = [(x0 + x1 + x2) / 3, (y0 + y1 + y2) / 3]; From 8b38928aad42d58180325a4fb6b2ae8ea4da4e36 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 9 Jul 2015 19:33:05 +0200 Subject: [PATCH 27/80] Make rendering of reprojection edges configurable --- src/ol/reproj/reproj.js | 14 +++++++++----- src/ol/reproj/tile.js | 20 +++++++++++--------- src/ol/source/tileimagesource.js | 23 ++++++++++++++++++++++- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 8d7e835db9..eaf6587f12 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -56,10 +56,11 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, * @param {ol.reproj.Triangulation} triangulation * @param {Array.<{extent: ol.Extent, * image: (HTMLCanvasElement|Image)}>} sources + * @param {boolean=} opt_renderEdges */ ol.reproj.renderTriangles = function(context, sourceResolution, sourceExtent, targetResolution, targetExtent, - triangulation, sources) { + triangulation, sources, opt_renderEdges) { var wrapXShiftDistance = !goog.isNull(sourceExtent) ? ol.extent.getWidth(sourceExtent) : 0; @@ -210,7 +211,10 @@ ol.reproj.renderTriangles = function(context, context.closePath(); context.clip(); - context.save(); + if (opt_renderEdges) { + context.save(); + } + context.translate(srcDataExtent[0] - srcNumericalShiftX, srcDataExtent[3] - srcNumericalShiftY); @@ -225,11 +229,11 @@ ol.reproj.renderTriangles = function(context, context.drawImage(stitchContext.canvas, 0, 0); } - context.restore(); + if (opt_renderEdges) { + context.restore(); - if (goog.DEBUG) { context.strokeStyle = 'black'; - context.lineWidth = 2 * pixelSize; + context.lineWidth = pixelSize; context.beginPath(); context.moveTo(x0, y0); context.lineTo(x1, y1); diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index f3626fb2df..a9832aeafd 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -28,11 +28,19 @@ goog.require('ol.reproj.Triangulation'); * @param {number} y * @param {number} pixelRatio * @param {function(number, number, number, number) : ol.Tile} getTileFunction + * @param {boolean=} opt_renderEdges */ ol.reproj.Tile = function(sourceProj, sourceTileGrid, - targetProj, targetTileGrid, z, x, y, pixelRatio, getTileFunction) { + targetProj, targetTileGrid, z, x, y, pixelRatio, getTileFunction, + opt_renderEdges) { goog.base(this, [z, x, y], ol.TileState.IDLE); + /** + * @private + * @type {boolean} + */ + this.renderEdges_ = goog.isDef(opt_renderEdges) ? opt_renderEdges : false; + /** * @private * @type {HTMLCanvasElement} @@ -239,18 +247,12 @@ ol.reproj.Tile.prototype.reproject_ = function() { var context = ol.dom.createCanvasContext2D(width, height); context.imageSmoothingEnabled = true; - if (goog.DEBUG) { - context.fillStyle = - sources.length === 0 ? 'rgba(255,0,0,.8)' : - (sources.length == 1 ? 'rgba(0,255,0,.3)' : 'rgba(0,0,255,.1)'); - context.fillRect(0, 0, width, height); - } - if (sources.length > 0) { var targetExtent = this.targetTileGrid_.getTileCoordExtent(tileCoord); ol.reproj.renderTriangles(context, srcResolution, this.sourceTileGrid_.getExtent(), - targetResolution, targetExtent, this.triangulation_, sources); + targetResolution, targetExtent, this.triangulation_, sources, + this.renderEdges_); } this.canvas_ = context.canvas; diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 3239cf8c36..5e3d987962 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -84,6 +84,12 @@ ol.source.TileImage = function(options) { * @type {Object.} */ this.tileGridForProjection = {}; + + /** + * @private + * @type {boolean} + */ + this.renderReprojectionEdges_ = false; }; goog.inherits(ol.source.TileImage, ol.source.Tile); @@ -198,7 +204,7 @@ ol.source.TileImage.prototype.getTile = projection, targetTileGrid, z, x, y, pixelRatio, goog.bind(function(z, x, y, pixelRatio) { return this.getTileInternal(z, x, y, pixelRatio, sourceProjection); - }, this)); + }, this), this.renderReprojectionEdges_); cache.set(tileCoordKey, tile); return tile; @@ -287,6 +293,21 @@ ol.source.TileImage.prototype.handleTileChange_ = function(event) { }; +/** + * Sets whether to render reprojection edges or not (usually for debugging). + * @param {boolean} render + * @api + */ +ol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) { + if (this.renderReprojectionEdges_ == render) return; + this.renderReprojectionEdges_ = render; + goog.object.forEach(this.tileCacheForProjection, function(tileCache) { + tileCache.clear(); + }); + this.changed(); +}; + + /** * Set the tile load function of the source. * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function. From 9a460b5f6b0c1f16c13e453e3b929191453826e0 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 10 Jul 2015 18:46:05 +0200 Subject: [PATCH 28/80] Handle cross-browser canvas antialiasing issues during reprojection By doing strictly 1 drawImage per triangle; triangle overlapping and using different blending mode. --- src/ol/reproj/reproj.js | 178 +++++++++++++++++++++++++--------------- 1 file changed, 111 insertions(+), 67 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index eaf6587f12..ade0a0a9d0 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -46,6 +46,18 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, }; +/** + * Type of solution used to solve the wrapX issue. + * @enum {number} + * @private + */ +ol.reproj.WrapXRendering_ = { + NONE: 0, + STITCH_SHIFT: 1, + STITCH_EXTENDED: 2 +}; + + /** * Renders the source into the canvas based on the triangulation. * @param {CanvasRenderingContext2D} context @@ -65,22 +77,25 @@ ol.reproj.renderTriangles = function(context, var wrapXShiftDistance = !goog.isNull(sourceExtent) ? ol.extent.getWidth(sourceExtent) : 0; - var wrapXShiftNeeded = triangulation.getWrapsXInSource() && - (wrapXShiftDistance > 0); + var wrapXType = ol.reproj.WrapXRendering_.NONE; - // If possible, stitch the sources shifted to solve the wrapX issue here. - // This is not possible if crossing both "dateline" and "prime meridian". - var performGlobalWrapXShift = false; - if (wrapXShiftNeeded) { + if (triangulation.getWrapsXInSource() && wrapXShiftDistance > 0) { + // If possible, stitch the sources shifted to solve the wrapX issue here. + // This is not possible if crossing both "dateline" and "prime meridian". var triangulationSrcExtent = triangulation.calculateSourceExtent(); var triangulationSrcWidth = goog.math.modulo( ol.extent.getWidth(triangulationSrcExtent), wrapXShiftDistance); - performGlobalWrapXShift = triangulationSrcWidth < wrapXShiftDistance / 2; + + if (triangulationSrcWidth < wrapXShiftDistance / 2) { + wrapXType = ol.reproj.WrapXRendering_.STITCH_SHIFT; + } else { + wrapXType = ol.reproj.WrapXRendering_.STITCH_EXTENDED; + } } var srcDataExtent = ol.extent.createEmpty(); goog.array.forEach(sources, function(src, i, arr) { - if (performGlobalWrapXShift) { + if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) { var srcW = src.extent[2] - src.extent[0]; var srcX = goog.math.modulo(src.extent[0], wrapXShiftDistance); ol.extent.extend(srcDataExtent, [srcX, src.extent[1], @@ -104,22 +119,41 @@ ol.reproj.renderTriangles = function(context, var srcDataWidth = ol.extent.getWidth(srcDataExtent); var srcDataHeight = ol.extent.getHeight(srcDataExtent); + var canvasWidthInUnits; + if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) { + canvasWidthInUnits = 2 * wrapXShiftDistance; + } else { + canvasWidthInUnits = srcDataWidth; + } + var stitchContext = ol.dom.createCanvasContext2D( - Math.ceil(srcDataWidth / sourceResolution), - Math.ceil(srcDataHeight / sourceResolution)); + Math.round(canvasWidthInUnits / sourceResolution), + Math.round(srcDataHeight / sourceResolution)); stitchContext.scale(1 / sourceResolution, 1 / sourceResolution); stitchContext.translate(-srcDataExtent[0], srcDataExtent[3]); goog.array.forEach(sources, function(src, i, arr) { - var xPos = performGlobalWrapXShift ? - goog.math.modulo(src.extent[0], wrapXShiftDistance) : src.extent[0]; - stitchContext.drawImage(src.image, xPos, -src.extent[3], - src.extent[2] - src.extent[0], src.extent[3] - src.extent[1]); + var xPos = src.extent[0]; + var yPos = -src.extent[3]; + var srcWidth = ol.extent.getWidth(src.extent); + var srcHeight = ol.extent.getHeight(src.extent); + + if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) { + xPos = goog.math.modulo(xPos, wrapXShiftDistance); + } + stitchContext.drawImage(src.image, xPos, yPos, srcWidth, srcHeight); + + if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) { + stitchContext.drawImage(src.image, wrapXShiftDistance + xPos, yPos, + srcWidth, srcHeight); + } }); var targetTL = ol.extent.getTopLeft(targetExtent); + context.globalCompositeOperation = 'copy'; + goog.array.forEach(triangulation.getTriangles(), function(tri, i, arr) { context.save(); @@ -130,7 +164,7 @@ ol.reproj.renderTriangles = function(context, * To optimize number of context calls and increase numerical stability, * we also do the following operations: * trans(-topLeftExtentCorner), scale(1 / targetResolution), scale(1, -1) - * here before solving the linear system. + * here before solving the linear system so [ui, vi] are pixel coordinates. * * Src points: xi, yi * Dst points: ui, vi @@ -147,15 +181,25 @@ ol.reproj.renderTriangles = function(context, var x0 = src[0][0], y0 = src[0][1], x1 = src[1][0], y1 = src[1][1], x2 = src[2][0], y2 = src[2][1]; - var u0 = tgt[0][0] - targetTL[0], v0 = -(tgt[0][1] - targetTL[1]), - u1 = tgt[1][0] - targetTL[0], v1 = -(tgt[1][1] - targetTL[1]), - u2 = tgt[2][0] - targetTL[0], v2 = -(tgt[2][1] - targetTL[1]); + var u0 = (tgt[0][0] - targetTL[0]) / targetResolution, + v0 = -(tgt[0][1] - targetTL[1]) / targetResolution; + var u1 = (tgt[1][0] - targetTL[0]) / targetResolution, + v1 = -(tgt[1][1] - targetTL[1]) / targetResolution; + var u2 = (tgt[2][0] - targetTL[0]) / targetResolution, + v2 = -(tgt[2][1] - targetTL[1]) / targetResolution; - var performIndividualWrapXShift = !performGlobalWrapXShift && - (wrapXShiftNeeded && - (Math.max(x0, x1, x2) - Math.min(x0, x1, x2)) > wrapXShiftDistance / 2); + var performWrapXShift = false; + if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) { + performWrapXShift = true; + } else if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) { + var minX = Math.min(x0, x1, x2); + var maxX = Math.max(x0, x1, x2); - if (performGlobalWrapXShift || performIndividualWrapXShift) { + performWrapXShift = (maxX - minX) > wrapXShiftDistance / 2 || + minX <= sourceExtent[0]; + } + + if (performWrapXShift) { x0 = goog.math.modulo(x0, wrapXShiftDistance); x1 = goog.math.modulo(x1, wrapXShiftDistance); x2 = goog.math.modulo(x2, wrapXShiftDistance); @@ -173,36 +217,30 @@ ol.reproj.renderTriangles = function(context, y2 -= srcNumericalShiftY; var augmentedMatrix = [ - [x1, y1, 0, 0, (u1 - u0) / targetResolution], - [x2, y2, 0, 0, (u2 - u0) / targetResolution], - [0, 0, x1, y1, (v1 - v0) / targetResolution], - [0, 0, x2, y2, (v2 - v0) / targetResolution] + [x1, y1, 0, 0, u1 - u0], + [x2, y2, 0, 0, u2 - u0], + [0, 0, x1, y1, v1 - v0], + [0, 0, x2, y2, v2 - v0] ]; var coefs = ol.math.solveLinearSystem(augmentedMatrix); if (goog.isNull(coefs)) { return; } - context.setTransform(coefs[0], coefs[2], coefs[1], coefs[3], - u0 / targetResolution, v0 / targetResolution); + var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3; + var calcClipPoint = function(u, v) { + // Enlarges the triangle by 1 pixel to ensure overlap and rounds to whole + // pixels to ensure correct cross-browser behavior. + // Gecko does antialiasing differently than WebKit. - var pixelSize = sourceResolution; - var centroid = [(x0 + x1 + x2) / 3, (y0 + y1 + y2) / 3]; - - // moves the `point` farther away from the `anchor` - var increasePointDistance = function(point, anchor, increment) { - var dir = [point[0] - anchor[0], point[1] - anchor[1]]; - var distance = Math.sqrt(dir[0] * dir[0] + dir[1] * dir[1]); - var scaleFactor = (distance + increment) / distance; - return [anchor[0] + scaleFactor * dir[0], - anchor[1] + scaleFactor * dir[1]]; + var dX = u - centroidX, dY = v - centroidY; + var distance = Math.sqrt(dX * dX + dY * dY); + return [Math.round(u + dX / distance), Math.round(v + dY / distance)]; }; - // enlarge the triangle so that the clip paths of individual triangles - // slightly (1px) overlap to prevent transparency errors on triangle edges - var p0 = increasePointDistance([x0, y0], centroid, pixelSize); - var p1 = increasePointDistance([x1, y1], centroid, pixelSize); - var p2 = increasePointDistance([x2, y2], centroid, pixelSize); + var p0 = calcClipPoint(u0, v0); + var p1 = calcClipPoint(u1, v1); + var p2 = calcClipPoint(u2, v2); context.beginPath(); context.moveTo(p0[0], p0[1]); @@ -211,9 +249,7 @@ ol.reproj.renderTriangles = function(context, context.closePath(); context.clip(); - if (opt_renderEdges) { - context.save(); - } + context.setTransform(coefs[0], coefs[2], coefs[1], coefs[3], u0, v0); context.translate(srcDataExtent[0] - srcNumericalShiftX, srcDataExtent[3] - srcNumericalShiftY); @@ -221,27 +257,35 @@ ol.reproj.renderTriangles = function(context, context.scale(sourceResolution, -sourceResolution); context.drawImage(stitchContext.canvas, 0, 0); - - if (performIndividualWrapXShift) { - // It was not possible to solve the wrapX shifting during stitching -> - // render the data second time (shifted) to solve the wrapX. - context.translate(wrapXShiftDistance / sourceResolution, 0); - context.drawImage(stitchContext.canvas, 0, 0); - } - - if (opt_renderEdges) { - context.restore(); - - context.strokeStyle = 'black'; - context.lineWidth = pixelSize; - context.beginPath(); - context.moveTo(x0, y0); - context.lineTo(x1, y1); - context.lineTo(x2, y2); - context.closePath(); - context.stroke(); - } - context.restore(); }); + + if (opt_renderEdges) { + context.save(); + + context.globalCompositeOperation = 'source-over'; + + context.strokeStyle = 'black'; + context.lineWidth = 1; + + goog.array.forEach(triangulation.getTriangles(), function(tri, i, arr) { + + var tgt = tri.target; + var u0 = (tgt[0][0] - targetTL[0]) / targetResolution, + v0 = -(tgt[0][1] - targetTL[1]) / targetResolution; + var u1 = (tgt[1][0] - targetTL[0]) / targetResolution, + v1 = -(tgt[1][1] - targetTL[1]) / targetResolution; + var u2 = (tgt[2][0] - targetTL[0]) / targetResolution, + v2 = -(tgt[2][1] - targetTL[1]) / targetResolution; + + context.beginPath(); + context.moveTo(u0, v0); + context.lineTo(u1, v1); + context.lineTo(u2, v2); + context.closePath(); + context.stroke(); + }); + + context.restore(); + } }; From f481070f8233d5a4b916f19bce31c710af5158f4 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 13 Jul 2015 15:07:04 +0200 Subject: [PATCH 29/80] Handle tilegrids without extent during tile reprojection --- src/ol/reproj/tile.js | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index a9832aeafd..bec8ca7b59 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -65,11 +65,6 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, */ this.targetTileGrid_ = targetTileGrid; - - var targetExtent = targetTileGrid.getTileCoordExtent(this.getTileCoord()); - var maxTargetExtent = this.targetTileGrid_.getExtent(); - var maxSourceExtent = this.sourceTileGrid_.getExtent(); - /** * @private * @type {!Array.} @@ -88,8 +83,12 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, */ this.srcZ_ = 0; - var limitedTargetExtent = ol.extent.getIntersection( - targetExtent, maxTargetExtent); + var targetExtent = targetTileGrid.getTileCoordExtent(this.getTileCoord()); + var maxTargetExtent = this.targetTileGrid_.getExtent(); + var maxSourceExtent = this.sourceTileGrid_.getExtent(); + + var limitedTargetExtent = goog.isNull(maxTargetExtent) ? + targetExtent : ol.extent.getIntersection(targetExtent, maxTargetExtent); if (ol.extent.getArea(limitedTargetExtent) === 0) { // Tile is completely outside range -> EMPTY @@ -146,21 +145,25 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, var srcRange = sourceTileGrid.getTileRangeForExtentAndZ( srcExtent, this.srcZ_); - var srcFullRange = sourceTileGrid.getFullTileRange(this.srcZ_); - srcRange.minY = Math.max(srcRange.minY, srcFullRange.minY); - srcRange.maxY = Math.min(srcRange.maxY, srcFullRange.maxY); - var xRange; - if (srcRange.minX > srcRange.maxX) { - xRange = goog.array.concat( - goog.array.range(srcRange.minX, srcFullRange.maxX + 1), - goog.array.range(srcFullRange.minX, srcRange.maxX + 1) - ); + var srcFullRange = sourceTileGrid.getFullTileRange(this.srcZ_); + if (!goog.isNull(srcFullRange)) { + srcRange.minY = Math.max(srcRange.minY, srcFullRange.minY); + srcRange.maxY = Math.min(srcRange.maxY, srcFullRange.maxY); + + if (srcRange.minX > srcRange.maxX) { + xRange = goog.array.concat( + goog.array.range(srcRange.minX, srcFullRange.maxX + 1), + goog.array.range(srcFullRange.minX, srcRange.maxX + 1) + ); + } else { + xRange = goog.array.range( + Math.max(srcRange.minX, srcFullRange.minX), + Math.min(srcRange.maxX, srcFullRange.maxX) + 1 + ); + } } else { - xRange = goog.array.range( - Math.max(srcRange.minX, srcFullRange.minX), - Math.min(srcRange.maxX, srcFullRange.maxX) + 1 - ); + xRange = goog.array.range(srcRange.minX, srcRange.maxX + 1); } if (xRange.length * srcRange.getHeight() > 100) { From be6bf00d74a167fe8da13b3302b56eb025589747 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 13 Jul 2015 17:24:54 +0200 Subject: [PATCH 30/80] Add defines for certain reprojection constants --- src/ol/ol.js | 40 ++++++++++++++++++++++++++++++++++ src/ol/reproj/image.js | 2 +- src/ol/reproj/tile.js | 14 ++++++------ src/ol/reproj/triangulation.js | 16 +++++--------- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/ol/ol.js b/src/ol/ol.js index c0c458349a..bd96b4817a 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -28,6 +28,13 @@ ol.DEFAULT_MAX_ZOOM = 42; ol.DEFAULT_MIN_ZOOM = 0; +/** + * @define {number} Default maximum allowed threshold (in pixels) for + * reprojection triangulation. Default is `0.5`. + */ +ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD = 0.5; + + /** * @define {number} Default high water mark. */ @@ -166,6 +173,39 @@ ol.OVERVIEWMAP_MAX_RATIO = 0.75; ol.OVERVIEWMAP_MIN_RATIO = 0.1; +/** + * @define {number} Maximum number of source tiles for raster reprojection. + * If too many source tiles are determined to be loaded to create a single + * reprojected tile the browser can become unresponsive or even crash. + * This can happen if the developer defines projections improperly and/or + * with unlimited extents. + * If too many tiles are required, no tiles are loaded and + * `ol.TileState.ERROR` state is set. Default is `100`. + */ +ol.RASTER_REPROJ_MAX_SOURCE_TILES = 100; + + +/** + * @define {number} Maximum number of subdivision steps during raster + * reprojection triangulation. Prevents high memory usage and large + * number of proj4 calls when for certain transformations and areas. + * At most `2*(4^this)` triangles are created. Default is `5`. + */ +ol.RASTER_REPROJ_MAX_SUBDIVISION = 5; + + +/** + * @define {number} Maximum allowed size of triangle relative to world width. + * When transforming corners of world extent between certain projections, + * The resulting triangulation seems to have zero error and no subdivision + * is performed. + * If the triangle width is more than this (relative to world width; 0-1), + * subdivison is forced (respecting `ol.RASTER_REPROJ_MAX_SUBDIVISION`). + * Default is `0.25`. + */ +ol.RASTER_REPROJ_MAX_TRIANGLE_WIDTH = 0.25; + + /** * @define {number} Tolerance for geometry simplification in device pixels. */ diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index 246dceb873..cfc713a3fc 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -63,7 +63,7 @@ ol.reproj.Image = function(sourceProj, targetProj, * @type {!ol.reproj.Triangulation} */ this.triangulation_ = new ol.reproj.Triangulation( - sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_); + sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_, 0); /** * @private diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index bec8ca7b59..cc6e599c87 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -99,7 +99,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, var targetResolution = targetTileGrid.getResolution(z); - var errorThresholdInPixels = 0.5; + var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; // in source units var errorThreshold = targetResolution * errorThresholdInPixels * @@ -111,7 +111,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, */ this.triangulation_ = new ol.reproj.Triangulation( sourceProj, targetProj, limitedTargetExtent, maxSourceExtent, - 5, errorThreshold); + errorThreshold); if (this.triangulation_.getTriangles().length === 0) { // no valid triangles -> EMPTY @@ -166,11 +166,11 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, xRange = goog.array.range(srcRange.minX, srcRange.maxX + 1); } - if (xRange.length * srcRange.getHeight() > 100) { - // Too many source tiles are needed -- something probably went wrong - // This sometimes happens for certain non-global projections - // if no extent is specified. - // TODO: detect somehow better? or at least make this a define + var tilesRequired = xRange.length * srcRange.getHeight(); + goog.asserts.assert(tilesRequired < ol.RASTER_REPROJ_MAX_SOURCE_TILES, + 'reasonable number of tiles is required'); + + if (tilesRequired > ol.RASTER_REPROJ_MAX_SOURCE_TILES) { this.state = ol.TileState.ERROR; return; } diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 384297ef44..067e7444cd 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -20,13 +20,12 @@ ol.reproj.Triangle; * @param {ol.proj.Projection} sourceProj * @param {ol.proj.Projection} targetProj * @param {ol.Extent} targetExtent - * @param {ol.Extent=} opt_maxSourceExtent - * @param {number=} opt_maxSubdiv Maximal subdivision. - * @param {number=} opt_errorThreshold Acceptable error (in source units). + * @param {ol.Extent} maxSourceExtent + * @param {number} errorThreshold Acceptable error (in source units). * @constructor */ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, - opt_maxSourceExtent, opt_maxSubdiv, opt_errorThreshold) { + maxSourceExtent, errorThreshold) { /** * @type {ol.proj.Projection} @@ -50,11 +49,8 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, * @type {ol.Extent} * @private */ - this.maxSourceExtent_ = goog.isDef(opt_maxSourceExtent) ? - opt_maxSourceExtent : null; + this.maxSourceExtent_ = maxSourceExtent; - var errorThreshold = goog.isDef(opt_errorThreshold) ? - opt_errorThreshold : 0; //TODO: define /** * @type {number} * @private @@ -99,7 +95,7 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, this.addQuadIfValid_(tlDst, trDst, brDst, blDst, tlDstSrc, trDstSrc, brDstSrc, blDstSrc, - opt_maxSubdiv || 0); + ol.RASTER_REPROJ_MAX_SUBDIVISION); }; @@ -155,7 +151,7 @@ ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, if (maxSubdiv > 0) { var needsSubdivision = !wrapsX && this.sourceProj_.isGlobal() && - srcCoverageX > 0.25; //TODO: define + srcCoverageX > ol.RASTER_REPROJ_MAX_TRIANGLE_WIDTH; var center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2]; var centerSrc = this.transformInv_(center); From ebc1bc0096838d69ef5c8c4bed73f123300f3a82 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 22 Jul 2015 10:32:04 +0200 Subject: [PATCH 31/80] Better handling of tilegrids without extent during reprojection For WMTS source, the tilegrid has no extent, but the projection can --- src/ol/reproj/tile.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index cc6e599c87..44984a403a 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -97,6 +97,18 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, return; } + if (!sourceProj.isGlobal()) { + var sourceProjExtent = sourceProj.getExtent(); + if (!goog.isNull(sourceProjExtent)) { + if (goog.isNull(maxSourceExtent)) { + maxSourceExtent = sourceProjExtent; + } else { + maxSourceExtent = ol.extent.getIntersection( + maxSourceExtent, sourceProjExtent); + } + } + } + var targetResolution = targetTileGrid.getResolution(z); var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; @@ -133,11 +145,6 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, this.srcZ_ = sourceTileGrid.getZForResolution(sourceResolution); var srcExtent = this.triangulation_.calculateSourceExtent(); - var sourceProjExtent = sourceProj.getExtent(); - if (!sourceProj.isGlobal() && sourceProjExtent) { - srcExtent = ol.extent.getIntersection(srcExtent, sourceProjExtent); - } - if (!goog.isNull(maxSourceExtent) && !ol.extent.intersects(maxSourceExtent, srcExtent)) { this.state = ol.TileState.EMPTY; @@ -167,10 +174,8 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, } var tilesRequired = xRange.length * srcRange.getHeight(); - goog.asserts.assert(tilesRequired < ol.RASTER_REPROJ_MAX_SOURCE_TILES, - 'reasonable number of tiles is required'); - - if (tilesRequired > ol.RASTER_REPROJ_MAX_SOURCE_TILES) { + if (!goog.asserts.assert(tilesRequired < ol.RASTER_REPROJ_MAX_SOURCE_TILES, + 'reasonable number of tiles is required')) { this.state = ol.TileState.ERROR; return; } From 168b675191f3359304e8735ea0ca20a376ba7e84 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 23 Jul 2015 15:04:58 +0200 Subject: [PATCH 32/80] Add reprojectionErrorThreshold option to the tiled image sources --- externs/olx.js | 99 +++++++++++++++++++++++++++ src/ol/reproj/tile.js | 5 +- src/ol/source/bingmapssource.js | 1 + src/ol/source/mapquestsource.js | 1 + src/ol/source/osmsource.js | 1 + src/ol/source/stamensource.js | 1 + src/ol/source/tilearcgisrestsource.js | 1 + src/ol/source/tileimagesource.js | 9 ++- src/ol/source/tilejsonsource.js | 1 + src/ol/source/tilewmssource.js | 1 + src/ol/source/wmtssource.js | 1 + src/ol/source/xyzsource.js | 1 + src/ol/source/zoomifysource.js | 1 + 13 files changed, 121 insertions(+), 2 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index b7abb24f37..e2cd7a93f5 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -3602,6 +3602,7 @@ olx.source; * key: string, * imagerySet: string, * maxZoom: (number|undefined), + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * wrapX: (boolean|undefined)}} * @api @@ -3642,6 +3643,14 @@ olx.source.BingMapsOptions.prototype.imagerySet; olx.source.BingMapsOptions.prototype.maxZoom; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.BingMapsOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -3761,6 +3770,7 @@ olx.source.TileUTFGridOptions.prototype.url; * logo: (string|olx.LogoOptions|undefined), * opaque: (boolean|undefined), * projection: ol.proj.ProjectionLike, + * reprojectionErrorThreshold: (number|undefined), * state: (ol.source.State|string|undefined), * tileClass: (function(new: ol.ImageTile, ol.TileCoord, * ol.TileState, string, ?string, @@ -3819,6 +3829,14 @@ olx.source.TileImageOptions.prototype.opaque; olx.source.TileImageOptions.prototype.projection; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.TileImageOptions.prototype.reprojectionErrorThreshold; + + /** * Source state. * @type {ol.source.State|string|undefined} @@ -4076,6 +4094,7 @@ olx.source.ImageMapGuideOptions.prototype.params; /** * @typedef {{layer: string, + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined)}} * @api @@ -4091,6 +4110,14 @@ olx.source.MapQuestOptions; olx.source.MapQuestOptions.prototype.layer; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.MapQuestOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -4144,6 +4171,7 @@ olx.source.TileDebugOptions.prototype.wrapX; * @typedef {{attributions: (Array.|undefined), * crossOrigin: (null|string|undefined), * maxZoom: (number|undefined), + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined), * wrapX: (boolean|undefined)}} @@ -4182,6 +4210,14 @@ olx.source.OSMOptions.prototype.crossOrigin; olx.source.OSMOptions.prototype.maxZoom; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.OSMOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -4538,6 +4574,7 @@ olx.source.ImageWMSOptions.prototype.url; * minZoom: (number|undefined), * maxZoom: (number|undefined), * opaque: (boolean|undefined), + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined)}} * @api @@ -4577,6 +4614,14 @@ olx.source.StamenOptions.prototype.maxZoom; olx.source.StamenOptions.prototype.opaque; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.StamenOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -4683,6 +4728,7 @@ olx.source.ImageStaticOptions.prototype.url; * logo: (string|olx.LogoOptions|undefined), * tileGrid: (ol.tilegrid.TileGrid|undefined), * projection: ol.proj.ProjectionLike, + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined), * urls: (Array.|undefined), @@ -4754,6 +4800,14 @@ olx.source.TileArcGISRestOptions.prototype.tileGrid; olx.source.TileArcGISRestOptions.prototype.projection; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.TileArcGISRestOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -4791,6 +4845,7 @@ olx.source.TileArcGISRestOptions.prototype.urls; /** * @typedef {{attributions: (Array.|undefined), * crossOrigin: (null|string|undefined), + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: string, * wrapX: (boolean|undefined)}} @@ -4821,6 +4876,14 @@ olx.source.TileJSONOptions.prototype.attributions; olx.source.TileJSONOptions.prototype.crossOrigin; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.TileJSONOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -4855,6 +4918,7 @@ olx.source.TileJSONOptions.prototype.wrapX; * tileGrid: (ol.tilegrid.TileGrid|undefined), * maxZoom: (number|undefined), * projection: ol.proj.ProjectionLike, + * reprojectionErrorThreshold: (number|undefined), * serverType: (ol.source.wms.ServerType|string|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined), @@ -4955,6 +5019,14 @@ olx.source.TileWMSOptions.prototype.maxZoom; olx.source.TileWMSOptions.prototype.projection; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.TileWMSOptions.prototype.reprojectionErrorThreshold; + + /** * The type of the remote WMS server. Currently only used when `hidpi` is * `true`. Default is `undefined`. @@ -5120,6 +5192,7 @@ olx.source.VectorOptions.prototype.wrapX; * logo: (string|olx.LogoOptions|undefined), * tileGrid: ol.tilegrid.WMTS, * projection: ol.proj.ProjectionLike, + * reprojectionErrorThreshold: (number|undefined), * requestEncoding: (ol.source.WMTSRequestEncoding|string|undefined), * layer: string, * style: string, @@ -5185,6 +5258,14 @@ olx.source.WMTSOptions.prototype.tileGrid; olx.source.WMTSOptions.prototype.projection; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.WMTSOptions.prototype.reprojectionErrorThreshold; + + /** * Request encoding. Default is `KVP`. * @type {ol.source.WMTSRequestEncoding|string|undefined} @@ -5311,6 +5392,7 @@ olx.source.WMTSOptions.prototype.wrapX; * crossOrigin: (null|string|undefined), * logo: (string|olx.LogoOptions|undefined), * projection: ol.proj.ProjectionLike, + * reprojectionErrorThreshold: (number|undefined), * maxZoom: (number|undefined), * minZoom: (number|undefined), * tileGrid: (ol.tilegrid.TileGrid|undefined), @@ -5362,6 +5444,14 @@ olx.source.XYZOptions.prototype.logo; olx.source.XYZOptions.prototype.projection; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.XYZOptions.prototype.reprojectionErrorThreshold; + + /** * Optional max zoom level. Default is `18`. * @type {number|undefined} @@ -5452,6 +5542,7 @@ olx.source.XYZOptions.prototype.wrapX; * @typedef {{attributions: (Array.|undefined), * crossOrigin: (null|string|undefined), * logo: (string|olx.LogoOptions|undefined), + * reprojectionErrorThreshold: (number|undefined), * url: !string, * tierSizeCalculation: (string|undefined), * size: ol.Size}} @@ -5488,6 +5579,14 @@ olx.source.ZoomifyOptions.prototype.crossOrigin; olx.source.ZoomifyOptions.prototype.logo; +/** + * Maximum allowed reprojection error. + * @type {number|undefined} + * @api + */ +olx.source.ZoomifyOptions.prototype.reprojectionErrorThreshold; + + /** * Prefix of URL template. * @type {!string} diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index 44984a403a..cdfbaf79d7 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -28,10 +28,12 @@ goog.require('ol.reproj.Triangulation'); * @param {number} y * @param {number} pixelRatio * @param {function(number, number, number, number) : ol.Tile} getTileFunction + * @param {number=} opt_errorThreshold * @param {boolean=} opt_renderEdges */ ol.reproj.Tile = function(sourceProj, sourceTileGrid, targetProj, targetTileGrid, z, x, y, pixelRatio, getTileFunction, + opt_errorThreshold, opt_renderEdges) { goog.base(this, [z, x, y], ol.TileState.IDLE); @@ -111,7 +113,8 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, var targetResolution = targetTileGrid.getResolution(z); - var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; + var errorThresholdInPixels = goog.isDef(opt_errorThreshold) ? + opt_errorThreshold : ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; // in source units var errorThreshold = targetResolution * errorThresholdInPixels * diff --git a/src/ol/source/bingmapssource.js b/src/ol/source/bingmapssource.js index 998d439442..8c42d1b152 100644 --- a/src/ol/source/bingmapssource.js +++ b/src/ol/source/bingmapssource.js @@ -29,6 +29,7 @@ ol.source.BingMaps = function(options) { crossOrigin: 'anonymous', opaque: true, projection: ol.proj.get('EPSG:3857'), + reprojectionErrorThreshold: options.reprojectionErrorThreshold, state: ol.source.State.LOADING, tileLoadFunction: options.tileLoadFunction, wrapX: options.wrapX !== undefined ? options.wrapX : true diff --git a/src/ol/source/mapquestsource.js b/src/ol/source/mapquestsource.js index 69ecc336b7..49e5241b24 100644 --- a/src/ol/source/mapquestsource.js +++ b/src/ol/source/mapquestsource.js @@ -40,6 +40,7 @@ ol.source.MapQuest = function(opt_options) { crossOrigin: 'anonymous', logo: 'https://developer.mapquest.com/content/osm/mq_logo.png', maxZoom: layerConfig.maxZoom, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, opaque: true, tileLoadFunction: options.tileLoadFunction, url: url diff --git a/src/ol/source/osmsource.js b/src/ol/source/osmsource.js index af17595c5e..f00859485e 100644 --- a/src/ol/source/osmsource.js +++ b/src/ol/source/osmsource.js @@ -36,6 +36,7 @@ ol.source.OSM = function(opt_options) { crossOrigin: crossOrigin, opaque: true, maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileLoadFunction: options.tileLoadFunction, url: url, wrapX: options.wrapX diff --git a/src/ol/source/stamensource.js b/src/ol/source/stamensource.js index f49f2d8b98..733542835e 100644 --- a/src/ol/source/stamensource.js +++ b/src/ol/source/stamensource.js @@ -109,6 +109,7 @@ ol.source.Stamen = function(options) { // FIXME uncomment the following when tilegrid supports minZoom //minZoom: providerConfig.minZoom, opaque: layerConfig.opaque, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileLoadFunction: options.tileLoadFunction, url: url }); diff --git a/src/ol/source/tilearcgisrestsource.js b/src/ol/source/tilearcgisrestsource.js index 3a105fe646..76ba2c336e 100644 --- a/src/ol/source/tilearcgisrestsource.js +++ b/src/ol/source/tilearcgisrestsource.js @@ -41,6 +41,7 @@ ol.source.TileArcGISRest = function(opt_options) { crossOrigin: options.crossOrigin, logo: options.logo, projection: options.projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileGrid: options.tileGrid, tileLoadFunction: options.tileLoadFunction, tileUrlFunction: goog.bind(this.tileUrlFunction_, this), diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 5e3d987962..fa79dc4ca3 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -85,6 +85,12 @@ ol.source.TileImage = function(options) { */ this.tileGridForProjection = {}; + /** + * @private + * @type {number|undefined} + */ + this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold; + /** * @private * @type {boolean} @@ -204,7 +210,8 @@ ol.source.TileImage.prototype.getTile = projection, targetTileGrid, z, x, y, pixelRatio, goog.bind(function(z, x, y, pixelRatio) { return this.getTileInternal(z, x, y, pixelRatio, sourceProjection); - }, this), this.renderReprojectionEdges_); + }, this), this.reprojectionErrorThreshold_, + this.renderReprojectionEdges_); cache.set(tileCoordKey, tile); return tile; diff --git a/src/ol/source/tilejsonsource.js b/src/ol/source/tilejsonsource.js index e1c3f854fc..bf82741004 100644 --- a/src/ol/source/tilejsonsource.js +++ b/src/ol/source/tilejsonsource.js @@ -34,6 +34,7 @@ ol.source.TileJSON = function(options) { attributions: options.attributions, crossOrigin: options.crossOrigin, projection: ol.proj.get('EPSG:3857'), + reprojectionErrorThreshold: options.reprojectionErrorThreshold, state: ol.source.State.LOADING, tileLoadFunction: options.tileLoadFunction, wrapX: options.wrapX !== undefined ? options.wrapX : true diff --git a/src/ol/source/tilewmssource.js b/src/ol/source/tilewmssource.js index e3ad0ed0d3..265b607a2d 100644 --- a/src/ol/source/tilewmssource.js +++ b/src/ol/source/tilewmssource.js @@ -45,6 +45,7 @@ ol.source.TileWMS = function(opt_options) { logo: options.logo, opaque: !transparent, projection: options.projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileGrid: options.tileGrid, tileLoadFunction: options.tileLoadFunction, tileUrlFunction: goog.bind(this.tileUrlFunction_, this), diff --git a/src/ol/source/wmtssource.js b/src/ol/source/wmtssource.js index c80e745752..854f1062d5 100644 --- a/src/ol/source/wmtssource.js +++ b/src/ol/source/wmtssource.js @@ -185,6 +185,7 @@ ol.source.WMTS = function(options) { crossOrigin: options.crossOrigin, logo: options.logo, projection: options.projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileClass: options.tileClass, tileGrid: tileGrid, tileLoadFunction: options.tileLoadFunction, diff --git a/src/ol/source/xyzsource.js b/src/ol/source/xyzsource.js index 1863cdb574..17c1f46ed7 100644 --- a/src/ol/source/xyzsource.js +++ b/src/ol/source/xyzsource.js @@ -49,6 +49,7 @@ ol.source.XYZ = function(options) { crossOrigin: options.crossOrigin, logo: options.logo, projection: projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileGrid: tileGrid, tileLoadFunction: options.tileLoadFunction, tilePixelRatio: options.tilePixelRatio, diff --git a/src/ol/source/zoomifysource.js b/src/ol/source/zoomifysource.js index f9533a74b0..bcddc08af1 100644 --- a/src/ol/source/zoomifysource.js +++ b/src/ol/source/zoomifysource.js @@ -124,6 +124,7 @@ ol.source.Zoomify = function(opt_options) { attributions: options.attributions, crossOrigin: options.crossOrigin, logo: options.logo, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileClass: ol.source.ZoomifyTile_, tileGrid: tileGrid, tileUrlFunction: tileUrlFunction From c2b21985f402c52b2b69f3773b3b9b303d9e4fdc Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 12 Aug 2015 15:50:40 +0200 Subject: [PATCH 33/80] Add ol.source.TileImage#setTileGridForProjection method --- src/ol/source/tileimagesource.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index fa79dc4ca3..74afaa6d9f 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -315,6 +315,25 @@ ol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) { }; +/** + * @param {ol.proj.ProjectionLike} projection + * @param {ol.tilegrid.TileGrid} tilegrid + * @api + */ +ol.source.TileImage.prototype.setTileGridForProjection = + function(projection, tilegrid) { + if (ol.ENABLE_RASTER_REPROJECTION) { + var proj = ol.proj.get(projection); + if (!goog.isNull(proj)) { + var projKey = goog.getUid(proj).toString(); + if (!(projKey in this.tileGridForProjection)) { + this.tileGridForProjection[projKey] = tilegrid; + } + } + } +}; + + /** * Set the tile load function of the source. * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function. From 3cc8291df4a3e55d3e10a111096e8a0c2bf1959b Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 12 Aug 2015 18:48:09 +0200 Subject: [PATCH 34/80] Support pixelRatio during reprojection --- src/ol/reproj/image.js | 50 +++++++++++++++++--------------- src/ol/reproj/reproj.js | 15 ++++++---- src/ol/reproj/tile.js | 13 +++++++-- src/ol/source/tileimagesource.js | 3 +- src/ol/source/tilesource.js | 8 +++++ 5 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index cfc713a3fc..921be2ca79 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -27,27 +27,6 @@ goog.require('ol.reproj.Triangulation'); ol.reproj.Image = function(sourceProj, targetProj, targetExtent, targetResolution, pixelRatio, getImageFunction) { - var width = ol.extent.getWidth(targetExtent) / targetResolution; - var height = ol.extent.getHeight(targetExtent) / targetResolution; - - /** - * @private - * @type {CanvasRenderingContext2D} - */ - this.context_ = ol.dom.createCanvasContext2D(width, height); - this.context_.imageSmoothingEnabled = true; - - if (goog.DEBUG) { - this.context_.fillStyle = 'rgba(255,0,0,0.1)'; - this.context_.fillRect(0, 0, width, height); - } - - /** - * @private - * @type {HTMLCanvasElement} - */ - this.canvas_ = this.context_.canvas; - /** * @private * @type {ol.Extent} @@ -90,6 +69,31 @@ ol.reproj.Image = function(sourceProj, targetProj, this.srcImage_ = getImageFunction(srcExtent, sourceResolution, pixelRatio, sourceProj); + var width = ol.extent.getWidth(targetExtent) / targetResolution; + var height = ol.extent.getHeight(targetExtent) / targetResolution; + + /** + * @private + * @type {number} + */ + this.srcPixelRatio_ = + !goog.isNull(this.srcImage_) ? this.srcImage_.getPixelRatio() : 1; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.context_ = ol.dom.createCanvasContext2D( + this.srcPixelRatio_ * width, this.srcPixelRatio_ * height); + this.context_.imageSmoothingEnabled = true; + this.context_.scale(this.srcPixelRatio_, this.srcPixelRatio_); + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = this.context_.canvas; + /** * @private * @type {goog.events.Key} @@ -105,7 +109,7 @@ ol.reproj.Image = function(sourceProj, targetProj, attributions = this.srcImage_.getAttributions(); } - goog.base(this, targetExtent, targetResolution, pixelRatio, + goog.base(this, targetExtent, targetResolution, this.srcPixelRatio_, state, attributions); }; goog.inherits(ol.reproj.Image, ol.ImageBase); @@ -142,7 +146,7 @@ ol.reproj.Image.prototype.reproject_ = function() { this.targetResolution_, this.targetExtent_, this.triangulation_, [{ extent: this.srcImage_.getExtent(), image: this.srcImage_.getImage() - }]); + }], this.srcPixelRatio_); } this.state = srcState; this.changed(); diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index ade0a0a9d0..e1de8e3f18 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -68,11 +68,12 @@ ol.reproj.WrapXRendering_ = { * @param {ol.reproj.Triangulation} triangulation * @param {Array.<{extent: ol.Extent, * image: (HTMLCanvasElement|Image)}>} sources + * @param {number} sourcePixelRatio * @param {boolean=} opt_renderEdges */ ol.reproj.renderTriangles = function(context, sourceResolution, sourceExtent, targetResolution, targetExtent, - triangulation, sources, opt_renderEdges) { + triangulation, sources, sourcePixelRatio, opt_renderEdges) { var wrapXShiftDistance = !goog.isNull(sourceExtent) ? ol.extent.getWidth(sourceExtent) : 0; @@ -127,10 +128,11 @@ ol.reproj.renderTriangles = function(context, } var stitchContext = ol.dom.createCanvasContext2D( - Math.round(canvasWidthInUnits / sourceResolution), - Math.round(srcDataHeight / sourceResolution)); + Math.round(sourcePixelRatio * canvasWidthInUnits / sourceResolution), + Math.round(sourcePixelRatio * srcDataHeight / sourceResolution)); - stitchContext.scale(1 / sourceResolution, 1 / sourceResolution); + stitchContext.scale(sourcePixelRatio / sourceResolution, + sourcePixelRatio / sourceResolution); stitchContext.translate(-srcDataExtent[0], srcDataExtent[3]); goog.array.forEach(sources, function(src, i, arr) { @@ -249,12 +251,13 @@ ol.reproj.renderTriangles = function(context, context.closePath(); context.clip(); - context.setTransform(coefs[0], coefs[2], coefs[1], coefs[3], u0, v0); + context.transform(coefs[0], coefs[2], coefs[1], coefs[3], u0, v0); context.translate(srcDataExtent[0] - srcNumericalShiftX, srcDataExtent[3] - srcNumericalShiftY); - context.scale(sourceResolution, -sourceResolution); + context.scale(sourceResolution / sourcePixelRatio, + -sourceResolution / sourcePixelRatio); context.drawImage(stitchContext.canvas, 0, 0); context.restore(); diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index cdfbaf79d7..cbc0916de4 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -43,6 +43,12 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, */ this.renderEdges_ = goog.isDef(opt_renderEdges) ? opt_renderEdges : false; + /** + * @private + * @type {number} + */ + this.pixelRatio_ = pixelRatio; + /** * @private * @type {HTMLCanvasElement} @@ -253,17 +259,18 @@ ol.reproj.Tile.prototype.reproject_ = function() { var targetResolution = this.targetTileGrid_.getResolution(z); var srcResolution = this.sourceTileGrid_.getResolution(this.srcZ_); - var width = goog.isNumber(size) ? size : size[0]; - var height = goog.isNumber(size) ? size : size[1]; + var width = this.pixelRatio_ * (goog.isNumber(size) ? size : size[0]); + var height = this.pixelRatio_ * (goog.isNumber(size) ? size : size[1]); var context = ol.dom.createCanvasContext2D(width, height); context.imageSmoothingEnabled = true; + context.scale(this.pixelRatio_, this.pixelRatio_); if (sources.length > 0) { var targetExtent = this.targetTileGrid_.getTileCoordExtent(tileCoord); ol.reproj.renderTriangles(context, srcResolution, this.sourceTileGrid_.getExtent(), targetResolution, targetExtent, this.triangulation_, sources, - this.renderEdges_); + this.pixelRatio_, this.renderEdges_); } this.canvas_ = context.canvas; diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 74afaa6d9f..a991f1f071 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -208,7 +208,8 @@ ol.source.TileImage.prototype.getTile = var tile = new ol.reproj.Tile( sourceProjection, sourceTileGrid, projection, targetTileGrid, - z, x, y, pixelRatio, goog.bind(function(z, x, y, pixelRatio) { + z, x, y, this.getTilePixelRatio(), + goog.bind(function(z, x, y, pixelRatio) { return this.getTileInternal(z, x, y, pixelRatio, sourceProjection); }, this), this.reprojectionErrorThreshold_, this.renderReprojectionEdges_); diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index 86f4982fac..5c0b92af51 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -230,6 +230,14 @@ ol.source.Tile.prototype.getTileCacheForProjection = function(projection) { }; +/** + * @return {number} + */ +ol.source.Tile.prototype.getTilePixelRatio = function() { + return this.tilePixelRatio_; +}; + + /** * @param {number} z Z. * @param {number} pixelRatio Pixel ratio. From 4c236a64b860f986d21a82d2756882726e315ebe Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 13 Aug 2015 15:01:58 +0200 Subject: [PATCH 35/80] Handle canvas clip antialiasing during reprojection --- src/ol/reproj/reproj.js | 60 ++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index e1de8e3f18..6dc5e90325 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -1,6 +1,8 @@ goog.provide('ol.reproj'); goog.require('goog.array'); +goog.require('goog.labs.userAgent.browser'); +goog.require('goog.labs.userAgent.platform'); goog.require('goog.math'); goog.require('ol.dom'); goog.require('ol.extent'); @@ -58,6 +60,20 @@ ol.reproj.WrapXRendering_ = { }; +/** + * We need to employ more sophisticated solution + * if the web browser antialiases clipping edges on canvas. + * + * Currently only Chrome does not antialias the edges, but this is probably + * going to be "fixed" in the future: http://crbug.com/424291 + * + * @type {boolean} + * @private + */ +ol.reproj.browserAntialiasesClip_ = !goog.labs.userAgent.browser.isChrome() || + goog.labs.userAgent.platform.isIos(); + + /** * Renders the source into the canvas based on the triangulation. * @param {CanvasRenderingContext2D} context @@ -154,8 +170,6 @@ ol.reproj.renderTriangles = function(context, var targetTL = ol.extent.getTopLeft(targetExtent); - context.globalCompositeOperation = 'copy'; - goog.array.forEach(triangulation.getTriangles(), function(tri, i, arr) { context.save(); @@ -229,25 +243,29 @@ ol.reproj.renderTriangles = function(context, return; } - var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3; - var calcClipPoint = function(u, v) { - // Enlarges the triangle by 1 pixel to ensure overlap and rounds to whole - // pixels to ensure correct cross-browser behavior. - // Gecko does antialiasing differently than WebKit. - - var dX = u - centroidX, dY = v - centroidY; - var distance = Math.sqrt(dX * dX + dY * dY); - return [Math.round(u + dX / distance), Math.round(v + dY / distance)]; - }; - - var p0 = calcClipPoint(u0, v0); - var p1 = calcClipPoint(u1, v1); - var p2 = calcClipPoint(u2, v2); - context.beginPath(); - context.moveTo(p0[0], p0[1]); - context.lineTo(p1[0], p1[1]); - context.lineTo(p2[0], p2[1]); + if (ol.reproj.browserAntialiasesClip_) { + // Enlarge the clipping triangle by 1 pixel to ensure the edges overlap + // in order to mask gaps caused by antialiasing. + var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3; + var calcClipPoint = function(u, v) { + var dX = u - centroidX, dY = v - centroidY; + var distance = Math.sqrt(dX * dX + dY * dY); + return [Math.round(u + dX / distance), Math.round(v + dY / distance)]; + }; + + var p0 = calcClipPoint(u0, v0); + var p1 = calcClipPoint(u1, v1); + var p2 = calcClipPoint(u2, v2); + + context.moveTo(p0[0], p0[1]); + context.lineTo(p1[0], p1[1]); + context.lineTo(p2[0], p2[1]); + } else { + context.moveTo(u0, v0); + context.lineTo(u1, v1); + context.lineTo(u2, v2); + } context.closePath(); context.clip(); @@ -266,8 +284,6 @@ ol.reproj.renderTriangles = function(context, if (opt_renderEdges) { context.save(); - context.globalCompositeOperation = 'source-over'; - context.strokeStyle = 'black'; context.lineWidth = 1; From 3b1d72202a9403ad48d416580cb29fc0a0453ba3 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 24 Aug 2015 10:21:27 +0200 Subject: [PATCH 36/80] Correctly reproject sources in wrappable projection With extent smaller than the projection extent --- src/ol/reproj/tile.js | 17 ++++++++--------- src/ol/reproj/triangulation.js | 13 +++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index cbc0916de4..a6472e30f6 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -105,15 +105,13 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, return; } - if (!sourceProj.isGlobal()) { - var sourceProjExtent = sourceProj.getExtent(); - if (!goog.isNull(sourceProjExtent)) { - if (goog.isNull(maxSourceExtent)) { - maxSourceExtent = sourceProjExtent; - } else { - maxSourceExtent = ol.extent.getIntersection( - maxSourceExtent, sourceProjExtent); - } + var sourceProjExtent = sourceProj.getExtent(); + if (!goog.isNull(sourceProjExtent)) { + if (goog.isNull(maxSourceExtent)) { + maxSourceExtent = sourceProjExtent; + } else { + maxSourceExtent = ol.extent.getIntersection( + maxSourceExtent, sourceProjExtent); } } @@ -155,6 +153,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, var srcExtent = this.triangulation_.calculateSourceExtent(); if (!goog.isNull(maxSourceExtent) && + !this.triangulation_.getWrapsXInSource() && !ol.extent.intersects(maxSourceExtent, srcExtent)) { this.state = ol.TileState.EMPTY; } else { diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 067e7444cd..374dd81988 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -78,6 +78,16 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, */ this.wrapsXInSource_ = false; + /** + * @type {boolean} + * @private + */ + this.canWrapXInSource_ = this.sourceProj_.canWrapX() && + !goog.isNull(maxSourceExtent) && + !goog.isNull(this.sourceProj_.getExtent()) && + (ol.extent.getWidth(maxSourceExtent) == + ol.extent.getWidth(this.sourceProj_.getExtent())); + /** * @type {number} * @private @@ -195,6 +205,9 @@ ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, } if (wrapsX) { + if (!this.canWrapXInSource_) { + return; + } this.wrapsXInSource_ = true; } From fc23a38692f4148db5d93afaaae93d5c9daf40a7 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 24 Aug 2015 11:12:45 +0200 Subject: [PATCH 37/80] Take target extent into account when ensuring minimal reproj subdivision --- src/ol/reproj/triangulation.js | 48 +++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 374dd81988..da4655d0d1 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -1,6 +1,7 @@ goog.provide('ol.reproj.Triangulation'); goog.require('goog.array'); +goog.require('goog.asserts'); goog.require('goog.math'); goog.require('ol.extent'); goog.require('ol.proj'); @@ -89,10 +90,18 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, ol.extent.getWidth(this.sourceProj_.getExtent())); /** - * @type {number} + * @type {?number} * @private */ - this.sourceWorldWidth_ = ol.extent.getWidth(this.sourceProj_.getExtent()); + this.sourceWorldWidth_ = !goog.isNull(this.sourceProj_.getExtent()) ? + ol.extent.getWidth(this.sourceProj_.getExtent()) : null; + + /** + * @type {?number} + * @private + */ + this.targetWorldWidth_ = !goog.isNull(this.targetProj_.getExtent()) ? + ol.extent.getWidth(this.targetProj_.getExtent()) : null; var tlDst = ol.extent.getTopLeft(targetExtent); var trDst = ol.extent.getTopRight(targetExtent); @@ -146,29 +155,43 @@ ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, aSrc, bSrc, cSrc, dSrc, maxSubdiv) { var srcQuadExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc, dSrc]); - if (!goog.isNull(this.maxSourceExtent_)) { - if (!ol.extent.intersects(srcQuadExtent, this.maxSourceExtent_)) { - // whole quad outside source projection extent -> ignore - return; - } - } - var srcCoverageX = ol.extent.getWidth(srcQuadExtent) / this.sourceWorldWidth_; + var srcCoverageX = !goog.isNull(this.sourceWorldWidth_) ? + ol.extent.getWidth(srcQuadExtent) / this.sourceWorldWidth_ : null; // when the quad is wrapped in the source projection // it covers most of the projection extent, but not fully var wrapsX = this.sourceProj_.canWrapX() && srcCoverageX > 0.5 && srcCoverageX < 1; - if (maxSubdiv > 0) { - var needsSubdivision = !wrapsX && this.sourceProj_.isGlobal() && - srcCoverageX > ol.RASTER_REPROJ_MAX_TRIANGLE_WIDTH; + var needsSubdivision = false; + if (maxSubdiv > 0) { + if (this.targetProj_.isGlobal() && !goog.isNull(this.targetWorldWidth_)) { + var tgtQuadExtent = ol.extent.boundingExtent([a, b, c, d]); + var tgtCoverageX = + ol.extent.getWidth(tgtQuadExtent) / this.targetWorldWidth_; + needsSubdivision |= tgtCoverageX > ol.RASTER_REPROJ_MAX_TRIANGLE_WIDTH; + } + if (!wrapsX && this.sourceProj_.isGlobal() && !goog.isNull(srcCoverageX)) { + needsSubdivision |= srcCoverageX > ol.RASTER_REPROJ_MAX_TRIANGLE_WIDTH; + } + } + + if (!needsSubdivision && !goog.isNull(this.maxSourceExtent_)) { + if (!ol.extent.intersects(srcQuadExtent, this.maxSourceExtent_)) { + // whole quad outside source projection extent -> ignore + return; + } + } + + if (maxSubdiv > 0) { var center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2]; var centerSrc = this.transformInv_(center); if (!needsSubdivision) { var dx; if (wrapsX) { + goog.asserts.assert(!goog.isNull(this.sourceWorldWidth_)); var centerSrcEstimX = (goog.math.modulo(aSrc[0], this.sourceWorldWidth_) + goog.math.modulo(cSrc[0], this.sourceWorldWidth_)) / 2; @@ -232,6 +255,7 @@ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { // to properly calculate the extent (and then possibly shifted back) goog.array.forEach(this.triangles_, function(triangle, i, arr) { + goog.asserts.assert(!goog.isNull(this.sourceWorldWidth_)); var src = triangle.source; ol.extent.extendCoordinate(extent, [goog.math.modulo(src[0][0], this.sourceWorldWidth_), src[0][1]]); From 615b54eb6787b05ba48f00c73cf7dc2519bf06cb Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 24 Aug 2015 11:23:08 +0200 Subject: [PATCH 38/80] Use error threshold for image sources reprojection --- src/ol/reproj/image.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index 921be2ca79..1717076c0d 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -37,12 +37,20 @@ ol.reproj.Image = function(sourceProj, targetProj, var limitedTargetExtent = ol.extent.getIntersection( targetExtent, maxTargetExtent); + + var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; + + // in source units + var errorThreshold = targetResolution * errorThresholdInPixels * + targetProj.getMetersPerUnit() / sourceProj.getMetersPerUnit(); + /** * @private * @type {!ol.reproj.Triangulation} */ this.triangulation_ = new ol.reproj.Triangulation( - sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_, 0); + sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_, + errorThreshold); /** * @private From aad5f9455612fa624ffed6e582aedf3e1cfb5cdb Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 24 Aug 2015 17:45:38 +0200 Subject: [PATCH 39/80] Minor type fix in ol.reproj.Image --- src/ol/reproj/image.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index 1717076c0d..6841372bbe 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -21,8 +21,7 @@ goog.require('ol.reproj.Triangulation'); * @param {ol.Extent} targetExtent * @param {number} targetResolution * @param {number} pixelRatio - * @param {function(ol.Extent, number, number, ol.proj.Projection) : - * ol.ImageBase} getImageFunction + * @param {function(ol.Extent, number, number):ol.ImageBase} getImageFunction */ ol.reproj.Image = function(sourceProj, targetProj, targetExtent, targetResolution, pixelRatio, getImageFunction) { @@ -74,8 +73,7 @@ ol.reproj.Image = function(sourceProj, targetProj, * @private * @type {ol.ImageBase} */ - this.srcImage_ = getImageFunction(srcExtent, sourceResolution, - pixelRatio, sourceProj); + this.srcImage_ = getImageFunction(srcExtent, sourceResolution, pixelRatio); var width = ol.extent.getWidth(targetExtent) / targetResolution; var height = ol.extent.getHeight(targetExtent) / targetResolution; From 14e20e23a00adb9533a36ecece15806ed0d4b294 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Tue, 25 Aug 2015 17:35:07 +0200 Subject: [PATCH 40/80] Correctly reproject projections with undefined units --- src/ol/reproj/image.js | 13 ++++--------- src/ol/reproj/reproj.js | 8 ++++++-- src/ol/reproj/tile.js | 38 +++++++++++++++++--------------------- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index 6841372bbe..af9e266afd 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -36,20 +36,19 @@ ol.reproj.Image = function(sourceProj, targetProj, var limitedTargetExtent = ol.extent.getIntersection( targetExtent, maxTargetExtent); + var targetCenter = ol.extent.getCenter(limitedTargetExtent); + var sourceResolution = ol.reproj.calculateSourceResolution( + sourceProj, targetProj, targetCenter, targetResolution); var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; - // in source units - var errorThreshold = targetResolution * errorThresholdInPixels * - targetProj.getMetersPerUnit() / sourceProj.getMetersPerUnit(); - /** * @private * @type {!ol.reproj.Triangulation} */ this.triangulation_ = new ol.reproj.Triangulation( sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_, - errorThreshold); + sourceResolution * errorThresholdInPixels); /** * @private @@ -65,10 +64,6 @@ ol.reproj.Image = function(sourceProj, targetProj, var srcExtent = this.triangulation_.calculateSourceExtent(); - var targetCenter = ol.extent.getCenter(targetExtent); - var sourceResolution = ol.reproj.calculateSourceResolution( - sourceProj, targetProj, targetCenter, targetResolution); - /** * @private * @type {ol.ImageBase} diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 6dc5e90325..db42c15f68 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -27,10 +27,14 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, var sourceCenter = ol.proj.transform(targetCenter, targetProj, sourceProj); + var targetMPU = targetProj.getMetersPerUnit(); + var sourceMPU = sourceProj.getMetersPerUnit(); + // calculate the ideal resolution of the source data var sourceResolution = - targetProj.getPointResolution(targetResolution, targetCenter) * - targetProj.getMetersPerUnit() / sourceProj.getMetersPerUnit(); + targetProj.getPointResolution(targetResolution, targetCenter); + if (goog.isDef(targetMPU)) sourceResolution *= targetMPU; + if (goog.isDef(sourceMPU)) sourceResolution /= sourceMPU; // based on the projection properties, the point resolution at the specified // coordinates may be slightly different. We need to reverse-compensate this diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index a6472e30f6..17ed887718 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -117,27 +117,6 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, var targetResolution = targetTileGrid.getResolution(z); - var errorThresholdInPixels = goog.isDef(opt_errorThreshold) ? - opt_errorThreshold : ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; - - // in source units - var errorThreshold = targetResolution * errorThresholdInPixels * - targetProj.getMetersPerUnit() / sourceProj.getMetersPerUnit(); - - /** - * @private - * @type {!ol.reproj.Triangulation} - */ - this.triangulation_ = new ol.reproj.Triangulation( - sourceProj, targetProj, limitedTargetExtent, maxSourceExtent, - errorThreshold); - - if (this.triangulation_.getTriangles().length === 0) { - // no valid triangles -> EMPTY - this.state = ol.TileState.EMPTY; - return; - } - var targetCenter = ol.extent.getCenter(limitedTargetExtent); var sourceResolution = ol.reproj.calculateSourceResolution( sourceProj, targetProj, targetCenter, targetResolution); @@ -149,6 +128,23 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, return; } + var errorThresholdInPixels = goog.isDef(opt_errorThreshold) ? + opt_errorThreshold : ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; + + /** + * @private + * @type {!ol.reproj.Triangulation} + */ + this.triangulation_ = new ol.reproj.Triangulation( + sourceProj, targetProj, limitedTargetExtent, maxSourceExtent, + sourceResolution * errorThresholdInPixels); + + if (this.triangulation_.getTriangles().length === 0) { + // no valid triangles -> EMPTY + this.state = ol.TileState.EMPTY; + return; + } + this.srcZ_ = sourceTileGrid.getZForResolution(sourceResolution); var srcExtent = this.triangulation_.calculateSourceExtent(); From 016df5b9028a5f2ca16ebcad9be40862b2c72112 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 26 Aug 2015 14:52:48 +0200 Subject: [PATCH 41/80] Minor ol.reproj.Image fixes --- src/ol/reproj/image.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index af9e266afd..4a81022e77 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -33,8 +33,8 @@ ol.reproj.Image = function(sourceProj, targetProj, this.maxSourceExtent_ = sourceProj.getExtent(); var maxTargetExtent = targetProj.getExtent(); - var limitedTargetExtent = ol.extent.getIntersection( - targetExtent, maxTargetExtent); + var limitedTargetExtent = goog.isNull(maxTargetExtent) ? + targetExtent : ol.extent.getIntersection(targetExtent, maxTargetExtent); var targetCenter = ol.extent.getCenter(limitedTargetExtent); var sourceResolution = ol.reproj.calculateSourceResolution( @@ -85,7 +85,8 @@ ol.reproj.Image = function(sourceProj, targetProj, * @type {CanvasRenderingContext2D} */ this.context_ = ol.dom.createCanvasContext2D( - this.srcPixelRatio_ * width, this.srcPixelRatio_ * height); + Math.round(this.srcPixelRatio_ * width), + Math.round(this.srcPixelRatio_ * height)); this.context_.imageSmoothingEnabled = true; this.context_.scale(this.srcPixelRatio_, this.srcPixelRatio_); From 726bcbef8383da4a334e4d5c9404f50c6bbe7460 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 26 Aug 2015 18:37:18 +0200 Subject: [PATCH 42/80] Alternative reprojection triangulation strategy The quads are now subdivided more granually (to 2 instead of 4), which usually leads to reduced number of triangles and higher performance. --- src/ol/ol.js | 4 ++-- src/ol/reproj/triangulation.js | 41 ++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/ol/ol.js b/src/ol/ol.js index bd96b4817a..48d80a1416 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -189,9 +189,9 @@ ol.RASTER_REPROJ_MAX_SOURCE_TILES = 100; * @define {number} Maximum number of subdivision steps during raster * reprojection triangulation. Prevents high memory usage and large * number of proj4 calls when for certain transformations and areas. - * At most `2*(4^this)` triangles are created. Default is `5`. + * At most `2*(2^this)` triangles are created. Default is `10`. */ -ol.RASTER_REPROJ_MAX_SUBDIVISION = 5; +ol.RASTER_REPROJ_MAX_SUBDIVISION = 10; /** diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index da4655d0d1..26f2b274ae 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -185,10 +185,10 @@ ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, } if (maxSubdiv > 0) { - var center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2]; - var centerSrc = this.transformInv_(center); - if (!needsSubdivision) { + var center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2]; + var centerSrc = this.transformInv_(center); + var dx; if (wrapsX) { goog.asserts.assert(!goog.isNull(this.sourceWorldWidth_)); @@ -205,24 +205,27 @@ ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, needsSubdivision = centerSrcErrorSquared > this.errorThresholdSquared_; } if (needsSubdivision) { - var ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; - var abSrc = this.transformInv_(ab); - var bc = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2]; - var bcSrc = this.transformInv_(bc); - var cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2]; - var cdSrc = this.transformInv_(cd); - var da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2]; - var daSrc = this.transformInv_(da); + if (Math.abs(a[0] - c[0]) <= Math.abs(a[1] - c[1])) { + var bc = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2]; + var bcSrc = this.transformInv_(bc); + var da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2]; + var daSrc = this.transformInv_(da); - this.addQuadIfValid_(a, ab, center, da, - aSrc, abSrc, centerSrc, daSrc, maxSubdiv - 1); - this.addQuadIfValid_(ab, b, bc, center, - abSrc, bSrc, bcSrc, centerSrc, maxSubdiv - 1); - this.addQuadIfValid_(center, bc, c, cd, - centerSrc, bcSrc, cSrc, cdSrc, maxSubdiv - 1); - this.addQuadIfValid_(da, center, cd, d, - daSrc, centerSrc, cdSrc, dSrc, maxSubdiv - 1); + this.addQuadIfValid_(a, b, bc, da, + aSrc, bSrc, bcSrc, daSrc, maxSubdiv - 1); + this.addQuadIfValid_(da, bc, c, d, + daSrc, bcSrc, cSrc, dSrc, maxSubdiv - 1); + } else { + var ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; + var abSrc = this.transformInv_(ab); + var cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2]; + var cdSrc = this.transformInv_(cd); + this.addQuadIfValid_(a, ab, cd, d, + aSrc, abSrc, cdSrc, dSrc, maxSubdiv - 1); + this.addQuadIfValid_(ab, b, c, cd, + abSrc, bSrc, cSrc, cdSrc, maxSubdiv - 1); + } return; } } From 03c75a86481ede2e2adda4da9be35ff0edba39ba Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 27 Aug 2015 08:05:39 +0200 Subject: [PATCH 43/80] Transformation caching during reprojection triangulation This usually saves around 30-35% of transformation calls. --- src/ol/reproj/triangulation.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 26f2b274ae..84f08d6062 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -40,11 +40,19 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, */ this.targetProj_ = targetProj; + /** @type {!Object.} */ + var transformInvCache = {}; + var transformInv = ol.proj.getTransform(this.targetProj_, this.sourceProj_); + /** - * @type {ol.TransformFunction} + * @param {ol.Coordinate} c + * @return {ol.Coordinate} * @private */ - this.transformInv_ = ol.proj.getTransform(this.targetProj_, this.sourceProj_); + this.transformInv_ = function(c) { + var key = c[0] + '/' + c[1]; + return transformInvCache[key] || (transformInvCache[key] = transformInv(c)); + }; /** * @type {ol.Extent} @@ -115,6 +123,8 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, this.addQuadIfValid_(tlDst, trDst, brDst, blDst, tlDstSrc, trDstSrc, brDstSrc, blDstSrc, ol.RASTER_REPROJ_MAX_SUBDIVISION); + + transformInvCache = {}; }; From 7864ed7861212419069aafdd523c2bbf83c07f26 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 27 Aug 2015 09:02:35 +0200 Subject: [PATCH 44/80] Force subdivision when transformation returns infinite coordinate --- src/ol/reproj/triangulation.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 84f08d6062..4b34d172b5 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -194,6 +194,19 @@ ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, } } + if (!needsSubdivision) { + if (!isFinite(aSrc[0]) || !isFinite(aSrc[1]) || + !isFinite(bSrc[0]) || !isFinite(bSrc[1]) || + !isFinite(cSrc[0]) || !isFinite(cSrc[1]) || + !isFinite(dSrc[0]) || !isFinite(dSrc[1])) { + if (maxSubdiv > 0) { + needsSubdivision = true; + } else { + return; + } + } + } + if (maxSubdiv > 0) { if (!needsSubdivision) { var center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2]; From 87337570e098b45652604569b733d89284438fb4 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 1 Jun 2015 19:08:55 +0200 Subject: [PATCH 45/80] Add reprojection example for tiled raster sources --- examples/reprojection.html | 49 ++++++++ examples/reprojection.js | 238 +++++++++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 examples/reprojection.html create mode 100644 examples/reprojection.js diff --git a/examples/reprojection.html b/examples/reprojection.html new file mode 100644 index 0000000000..52ddf0d862 --- /dev/null +++ b/examples/reprojection.html @@ -0,0 +1,49 @@ +--- +template: example.html +title: Raster reprojection example +shortdesc: Demonstrates client-side raster reprojection between various projections. +docs: > + This example shows client-side raster reprojection capabilities of + OpenLayers 3 between various projections. +tags: "reprojection, projection, proj4js, mapquest, wms" +resources: + - http://cdnjs.cloudflare.com/ajax/libs/proj4js/2.3.6/proj4.js +--- +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
diff --git a/examples/reprojection.js b/examples/reprojection.js new file mode 100644 index 0000000000..fb0cf70d09 --- /dev/null +++ b/examples/reprojection.js @@ -0,0 +1,238 @@ +goog.require('ol.Attribution'); +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.extent'); +goog.require('ol.format.WMTSCapabilities'); +goog.require('ol.layer.Tile'); +goog.require('ol.proj'); +goog.require('ol.source.MapQuest'); +goog.require('ol.source.TileImage'); +goog.require('ol.source.TileWMS'); +goog.require('ol.source.WMTS'); +goog.require('ol.source.XYZ'); +goog.require('ol.tilegrid.TileGrid'); + + +proj4.defs('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' + + '+x_0=400000 +y_0=-100000 +ellps=airy ' + + '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + + '+units=m +no_defs'); +var proj27700 = ol.proj.get('EPSG:27700'); +proj27700.setExtent([0, 0, 700000, 1300000]); + +proj4.defs('EPSG:23032', '+proj=utm +zone=32 +ellps=intl ' + + '+towgs84=-87,-98,-121,0,0,0,0 +units=m +no_defs'); +var proj23032 = ol.proj.get('EPSG:23032'); +proj23032.setExtent([-1206118.71, 4021309.92, 1295389.00, 8051813.28]); + +proj4.defs('EPSG:5479', '+proj=lcc +lat_1=-76.66666666666667 +lat_2=' + + '-79.33333333333333 +lat_0=-78 +lon_0=163 +x_0=7000000 +y_0=5000000 ' + + '+ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'); +var proj5479 = ol.proj.get('EPSG:5479'); +proj5479.setExtent([6825737.53, 4189159.80, 9633741.96, 5782472.71]); + +proj4.defs('EPSG:5041', '+proj=stere +lat_0=90 +lat_ts=90 +lon_0=0 +k=0.994 ' + + '+x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs'); +var proj5041 = ol.proj.get('EPSG:5041'); +proj5041.setExtent([1994055.62, 5405875.53, 2000969.46, 2555456.55]); + +proj4.defs('EPSG:21781', '+proj=somerc +lat_0=46.95240555555556 ' + + '+lon_0=7.439583333333333 +k_0=1 +x_0=600000 +y_0=200000 +ellps=bessel ' + + '+towgs84=674.4,15.1,405.3,0,0,0,0 +units=m +no_defs'); +var proj21781 = ol.proj.get('EPSG:21781'); +proj21781.setExtent([485071.54, 75346.36, 828515.78, 299941.84]); + +proj4.defs('EPSG:3413', '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 ' + + '+x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); +var proj3413 = ol.proj.get('EPSG:3413'); +proj3413.setExtent([-4194304, -4194304, 4194304, 4194304]); + +proj4.defs('EPSG:2163', '+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 ' + + '+a=6370997 +b=6370997 +units=m +no_defs'); +var proj2163 = ol.proj.get('EPSG:2163'); +proj2163.setExtent([-8040784.5135, -2577524.9210, 3668901.4484, 4785105.1096]); + +proj4.defs('ESRI:54009', '+proj=moll +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 ' + + '+units=m +no_defs'); +var proj54009 = ol.proj.get('ESRI:54009'); +proj54009.setExtent([-18e6, -9e6, 18e6, 9e6]); + + +var layers = []; + +layers['OS'] = new ol.layer.Tile({ + source: new ol.source.XYZ({ + projection: 'EPSG:27700', + url: 'https://googledrive.com/host/0B0bm2WdRuvICflNqUmxEdUNOV0ZRUFQ3cXNXR' + + 'FlOTm9MWmJxSDAxM2V5M1ZJX2lITE9oejA/{z}/{x}/{y}.png', + maxZoom: 3 + }) +}); + +layers['MapQuest'] = new ol.layer.Tile({ + source: new ol.source.MapQuest({layer: 'osm'}) +}); + +layers['wms4326'] = new ol.layer.Tile({ + source: new ol.source.TileWMS({ + url: 'http://demo.boundlessgeo.com/geoserver/wms', + params: { + 'LAYERS': 'ne:NE1_HR_LC_SR_W_DR' + }, + projection: 'EPSG:4326' + }) +}); + +layers['wms21781'] = new ol.layer.Tile({ + source: new ol.source.TileWMS({ + attributions: [new ol.Attribution({ + html: '© ' + + '' + + 'Pixelmap 1:1000000 / geo.admin.ch' + })], + crossOrigin: 'anonymous', + params: { + 'LAYERS': 'ch.swisstopo.pixelkarte-farbe-pk1000.noscale', + 'FORMAT': 'image/jpeg' + }, + url: 'http://wms.geo.admin.ch/', + projection: 'EPSG:21781' + }) +}); + +var parser = new ol.format.WMTSCapabilities(); +$.ajax('http://map1.vis.earthdata.nasa.gov/wmts-arctic/' + + 'wmts.cgi?SERVICE=WMTS&request=GetCapabilities').then(function(response) { + var result = parser.read(response); + var options = ol.source.WMTS.optionsFromCapabilities(result, + {layer: 'OSM_Land_Mask', matrixSet: 'EPSG3413_250m'}); + options.projection = 'EPSG:3413'; + options.wrapX = false; + layers['wmts3413'] = new ol.layer.Tile({ + source: new ol.source.WMTS(options) + }); +}); + +layers['grandcanyon'] = new ol.layer.Tile({ + source: new ol.source.XYZ({ + url: 'http://tileserver.maptiler.com/grandcanyon@2x/{z}/{x}/{y}.png', + tilePixelRatio: 2, + maxZoom: 15, + attributions: [new ol.Attribution({ + html: 'Tiles © USGS, rendered with ' + + 'MapTiler' + })] + }) +}); + +var startResolution = + ol.extent.getWidth(ol.proj.get('EPSG:3857').getExtent()) / 256; +var resolutions = new Array(22); +for (var i = 0, ii = resolutions.length; i < ii; ++i) { + resolutions[i] = startResolution / Math.pow(2, i); +} + +layers['states'] = new ol.layer.Tile({ + source: new ol.source.TileWMS({ + url: 'http://demo.boundlessgeo.com/geoserver/wms', + params: {'LAYERS': 'topp:states', 'TILED': true}, + serverType: 'geoserver', + tileGrid: new ol.tilegrid.TileGrid({ + extent: [-13884991, 2870341, -7455066, 6338219], + resolutions: resolutions, + tileSize: [512, 256] + }), + projection: 'EPSG:3857' + }) +}); + + +var map = new ol.Map({ + layers: [ + layers['MapQuest'], + layers['OS'] + ], + renderer: common.getRendererFromQueryString(), + target: 'map', + view: new ol.View({ + projection: 'EPSG:3857', + center: [0, 0], + zoom: 2 + }) +}); + + +var baseMapSelect = document.getElementById('base-map'); +var overlayMapSelect = document.getElementById('overlay-map'); +var viewProjSelect = document.getElementById('view-projection'); +var renderEdgesCheckbox = document.getElementById('render-edges'); +var renderEdges = false; + +function updateViewProjection() { + var newProj = ol.proj.get(viewProjSelect.value); + var newProjExtent = newProj.getExtent(); + var newView = new ol.View({ + projection: newProj, + center: ol.extent.getCenter(newProjExtent || [0, 0, 0, 0]), + zoom: 0, + extent: newProjExtent || undefined + }); + map.setView(newView); +} + + +/** + * @param {Event} e Change event. + */ +viewProjSelect.onchange = function(e) { + updateViewProjection(); +}; + +updateViewProjection(); + +var updateRenderEdgesOnLayer = function(layer) { + if (layer instanceof ol.layer.Tile) { + var source = layer.getSource(); + if (source instanceof ol.source.TileImage) { + source.setRenderReprojectionEdges(renderEdges); + } + } +}; + + +/** + * @param {Event} e Change event. + */ +baseMapSelect.onchange = function(e) { + var layer = layers[baseMapSelect.value]; + if (layer) { + layer.setOpacity(1); + updateRenderEdgesOnLayer(layer); + map.getLayers().setAt(0, layer); + } +}; + + +/** + * @param {Event} e Change event. + */ +overlayMapSelect.onchange = function(e) { + var layer = layers[overlayMapSelect.value]; + if (layer) { + layer.setOpacity(0.7); + updateRenderEdgesOnLayer(layer); + map.getLayers().setAt(1, layer); + } +}; + + +/** + * @param {Event} e Change event. + */ +renderEdgesCheckbox.onchange = function(e) { + renderEdges = renderEdgesCheckbox.checked; + map.getLayers().forEach(function(layer) { + updateRenderEdgesOnLayer(layer); + }); +}; From 690a5f1f900d0a75918cc41ebbd052c3ae5df41b Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 11 Jun 2015 16:31:59 +0200 Subject: [PATCH 46/80] Add reprojection example for ol.source.ImageStatic --- examples/reprojection-image.html | 16 +++++++++++++ examples/reprojection-image.js | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 examples/reprojection-image.html create mode 100644 examples/reprojection-image.js diff --git a/examples/reprojection-image.html b/examples/reprojection-image.html new file mode 100644 index 0000000000..f6ca664fb4 --- /dev/null +++ b/examples/reprojection-image.html @@ -0,0 +1,16 @@ +--- +template: example.html +title: Image reprojection example +shortdesc: Demonstrates client-side reprojection of single image. +docs: > + This example shows client-side single-image reprojection capabilities of + OpenLayers 3. +tags: "reprojection, projection, proj4js, mapquest, image" +resources: + - http://cdnjs.cloudflare.com/ajax/libs/proj4js/2.3.6/proj4.js +--- +
+
+
+
+
diff --git a/examples/reprojection-image.js b/examples/reprojection-image.js new file mode 100644 index 0000000000..0143246cb1 --- /dev/null +++ b/examples/reprojection-image.js @@ -0,0 +1,40 @@ +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.extent'); +goog.require('ol.layer.Image'); +goog.require('ol.layer.Tile'); +goog.require('ol.proj'); +goog.require('ol.source.ImageStatic'); +goog.require('ol.source.MapQuest'); + + +proj4.defs('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' + + '+x_0=400000 +y_0=-100000 +ellps=airy ' + + '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + + '+units=m +no_defs'); +var extent = [0, 0, 700000, 1300000]; +var proj27700 = ol.proj.get('EPSG:27700'); +proj27700.setExtent(extent); + +var map = new ol.Map({ + layers: [ + new ol.layer.Tile({ + source: new ol.source.MapQuest({layer: 'osm'}) + }), + new ol.layer.Image({ + source: new ol.source.ImageStatic({ + url: 'http://upload.wikimedia.org/wikipedia/commons/thumb/1/18/' + + 'British_National_Grid.svg/2000px-British_National_Grid.svg.png', + projection: 'EPSG:27700', + imageExtent: extent + }) + }) + ], + renderer: common.getRendererFromQueryString(), + target: 'map', + view: new ol.View({ + center: ol.proj.transform(ol.extent.getCenter(extent), + proj27700, 'EPSG:3857'), + zoom: 4 + }) +}); From 0c48a560b24bf942fa11daf4cae990feb373ec24 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Tue, 25 Aug 2015 17:47:42 +0200 Subject: [PATCH 47/80] Add reprojection example with proj4 defs loading from EPSG.io --- examples/reprojection-by-code.html | 26 +++++++ examples/reprojection-by-code.js | 112 +++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 examples/reprojection-by-code.html create mode 100644 examples/reprojection-by-code.js diff --git a/examples/reprojection-by-code.html b/examples/reprojection-by-code.html new file mode 100644 index 0000000000..c4db7ec985 --- /dev/null +++ b/examples/reprojection-by-code.html @@ -0,0 +1,26 @@ +--- +template: example.html +title: Reprojection with EPSG.io database search +shortdesc: Demonstrates client-side raster reprojection of MapQuest OSM to any projection +docs: > + This example shows client-side raster reprojection capabilities of + OpenLayers 3 from MapQuest OSM (EPSG:3857) to any projection by searching + in EPSG.io database. +tags: "reprojection, projection, proj4js, mapquest, epsg.io" +resources: + - http://cdnjs.cloudflare.com/ajax/libs/proj4js/2.3.6/proj4.js +--- +
+
+
+
+
+ + + + +
+ +
+
+
diff --git a/examples/reprojection-by-code.js b/examples/reprojection-by-code.js new file mode 100644 index 0000000000..07a94a3175 --- /dev/null +++ b/examples/reprojection-by-code.js @@ -0,0 +1,112 @@ +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.extent'); +goog.require('ol.layer.Tile'); +goog.require('ol.proj'); +goog.require('ol.source.MapQuest'); +goog.require('ol.source.TileImage'); + + + +var map = new ol.Map({ + layers: [ + new ol.layer.Tile({ + source: new ol.source.MapQuest({layer: 'osm'}) + }) + ], + renderer: common.getRendererFromQueryString(), + target: 'map', + view: new ol.View({ + projection: 'EPSG:3857', + center: [0, 0], + zoom: 1 + }) +}); + + +var queryInput = document.getElementById('epsg-query'); +var searchButton = document.getElementById('epsg-search'); +var resultSpan = document.getElementById('epsg-result'); +var renderEdgesCheckbox = document.getElementById('render-edges'); + +function setProjection(code, name, proj4def, bbox) { + if (code === null || name === null || proj4def === null || bbox === null) { + resultSpan.innerHTML = 'Nothing usable found, using EPSG:3857...'; + map.setView(new ol.View({ + projection: 'EPSG:3857', + center: [0, 0], + zoom: 1 + })); + return; + } + + resultSpan.innerHTML = '(' + code + ') ' + name; + + var newProjCode = 'EPSG:' + code; + proj4.defs(newProjCode, proj4def); + var newProj = ol.proj.get(newProjCode); + var fromLonLat = ol.proj.getTransform('EPSG:4326', newProj); + + // very approximate calculation of projection extent + var extent = ol.extent.applyTransform( + [bbox[1], bbox[2], bbox[3], bbox[0]], fromLonLat); + newProj.setExtent(extent); + var newView = new ol.View({ + projection: newProj + }); + map.setView(newView); + + var size = map.getSize(); + if (size) { + newView.fit(extent, size); + } +} + + +function search(query) { + resultSpan.innerHTML = 'Searching...'; + $.ajax('http://epsg.io/?format=json&q=' + query).then(function(response) { + if (response) { + var results = response['results']; + if (results && results.length > 0) { + for (var i = 0; i < results.length; i++) { + var result = results[i]; + if (result) { + var code = result['code'], name = result['name'], + proj4def = result['proj4'], bbox = result['bbox']; + if (code && code.length > 0 && proj4def && proj4def.length > 0 && + bbox && bbox.length == 4) { + setProjection(code, name, proj4def, bbox); + return; + } + } + } + } + } + setProjection(null, null, null, null); + }); +} + + +/** + * @param {Event} e Change event. + */ +searchButton.onclick = function(e) { + search(queryInput.value); + e.preventDefault(); +}; + + +/** + * @param {Event} e Change event. + */ +renderEdgesCheckbox.onchange = function(e) { + map.getLayers().forEach(function(layer) { + if (layer instanceof ol.layer.Tile) { + var source = layer.getSource(); + if (source instanceof ol.source.TileImage) { + source.setRenderReprojectionEdges(renderEdgesCheckbox.checked); + } + } + }); +}; From c899100dab45b1d260513facab5f21fa3f52b603 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 20 Jul 2015 16:30:30 +0200 Subject: [PATCH 48/80] Add tests for ol.reproj.* --- test/spec/ol/reproj/image.test.js | 59 ++++++++++++++ test/spec/ol/reproj/reproj.test.js | 45 +++++++++++ test/spec/ol/reproj/tile.test.js | 99 +++++++++++++++++++++++ test/spec/ol/reproj/triangulation.test.js | 58 +++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 test/spec/ol/reproj/image.test.js create mode 100644 test/spec/ol/reproj/reproj.test.js create mode 100644 test/spec/ol/reproj/tile.test.js create mode 100644 test/spec/ol/reproj/triangulation.test.js diff --git a/test/spec/ol/reproj/image.test.js b/test/spec/ol/reproj/image.test.js new file mode 100644 index 0000000000..08b3b6d409 --- /dev/null +++ b/test/spec/ol/reproj/image.test.js @@ -0,0 +1,59 @@ +goog.provide('ol.test.reproj.Image'); + +describe('ol.reproj.Image', function() { + function createImage(pixelRatio) { + return new ol.reproj.Image( + ol.proj.get('EPSG:3857'), ol.proj.get('EPSG:4326'), + [-180, -85, 180, 85], 10, pixelRatio, + function(extent, resolution, pixelRatio) { + return new ol.Image(extent, resolution, pixelRatio, [], + 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=', '', + function(image, src) { + image.getImage().src = src; + }); + }); + } + + it('changes state as expected', function(done) { + var image = createImage(1); + expect(image.getState()).to.be(ol.ImageState.IDLE); + image.listen('change', function() { + if (image.getState() == ol.ImageState.LOADED) { + done(); + } + }); + image.load(); + }); + + it('returns correct canvas size', function(done) { + var image = createImage(1); + image.listen('change', function() { + if (image.getState() == ol.ImageState.LOADED) { + var canvas = image.getImage(); + expect(canvas.width).to.be(36); + expect(canvas.height).to.be(17); + done(); + } + }); + image.load(); + }); + + it('respects pixelRatio', function(done) { + var image = createImage(2); + image.listen('change', function() { + if (image.getState() == ol.ImageState.LOADED) { + var canvas = image.getImage(); + expect(canvas.width).to.be(72); + expect(canvas.height).to.be(34); + done(); + } + }); + image.load(); + }); +}); + + +goog.require('ol.Image'); +goog.require('ol.ImageState'); +goog.require('ol.proj'); +goog.require('ol.reproj.Image'); diff --git a/test/spec/ol/reproj/reproj.test.js b/test/spec/ol/reproj/reproj.test.js new file mode 100644 index 0000000000..44283a86c1 --- /dev/null +++ b/test/spec/ol/reproj/reproj.test.js @@ -0,0 +1,45 @@ +goog.provide('ol.test.reproj'); + +describe('ol.reproj', function() { + + describe('#calculateSourceResolution', function() { + var proj3857 = ol.proj.get('EPSG:3857'); + var proj4326 = ol.proj.get('EPSG:4326'); + var origin = [0, 0]; + var point3857 = [50, 40]; + var point4326 = ol.proj.transform(point3857, proj3857, proj4326); + + it('is identity for identical projection', function() { + var result; + var resolution = 500; + result = ol.reproj.calculateSourceResolution( + proj3857, proj3857, origin, resolution); + expect(result).to.be(resolution); + + result = ol.reproj.calculateSourceResolution( + proj3857, proj3857, point3857, resolution); + expect(result).to.be(resolution); + + result = ol.reproj.calculateSourceResolution( + proj4326, proj4326, point4326, resolution); + expect(result).to.be(resolution); + }); + + it('calculates correctly', function() { + var resolution4326 = 5; + + var resolution3857 = ol.reproj.calculateSourceResolution( + proj3857, proj4326, point4326, resolution4326); + expect(resolution3857).not.to.be(resolution4326); + expect(resolution3857).to.roughlyEqual(555974.3714343394, 1e-6); + + var result = ol.reproj.calculateSourceResolution( + proj4326, proj3857, point3857, resolution3857); + expect(result).to.be(resolution4326); + }); + }); +}); + + +goog.require('ol.reproj'); +goog.require('ol.proj'); diff --git a/test/spec/ol/reproj/tile.test.js b/test/spec/ol/reproj/tile.test.js new file mode 100644 index 0000000000..a209a1c471 --- /dev/null +++ b/test/spec/ol/reproj/tile.test.js @@ -0,0 +1,99 @@ +goog.provide('ol.test.reproj.Tile'); + +describe('ol.reproj.Tile', function() { + beforeEach(function() { + proj4.defs('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 ' + + '+k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy ' + + '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + + '+units=m +no_defs'); + var proj27700 = ol.proj.get('EPSG:27700'); + proj27700.setExtent([0, 0, 700000, 1300000]); + }); + + afterEach(function() { + delete proj4.defs['EPSG:27700']; + }); + + + function createTile(pixelRatio, opt_tileSize) { + var proj4326 = ol.proj.get('EPSG:4326'); + var proj3857 = ol.proj.get('EPSG:3857'); + return new ol.reproj.Tile( + proj3857, ol.tilegrid.createForProjection(proj3857), proj4326, + ol.tilegrid.createForProjection(proj4326, 3, opt_tileSize), + 3, 2, -2, pixelRatio, function(z, x, y, pixelRatio) { + return new ol.ImageTile([z, x, y], ol.TileState.IDLE, + 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=', '', + function(tile, src) { + tile.getImage().src = src; + }); + }); + } + + it('changes state as expected', function(done) { + var tile = createTile(1); + expect(tile.getState()).to.be(ol.TileState.IDLE); + tile.listen('change', function() { + if (tile.getState() == ol.TileState.LOADED) { + done(); + } + }); + tile.load(); + }); + + it('is empty when outside target tile grid', function() { + var proj4326 = ol.proj.get('EPSG:4326'); + var proj3857 = ol.proj.get('EPSG:3857'); + var tile = new ol.reproj.Tile( + proj3857, ol.tilegrid.createForProjection(proj3857), + proj4326, ol.tilegrid.createForProjection(proj4326), + 0, -1, 0, 1, function() { + expect().fail('No tiles should be required'); + }); + expect(tile.getState()).to.be(ol.TileState.EMPTY); + }); + + it('is empty when outside source tile grid', function() { + var proj4326 = ol.proj.get('EPSG:4326'); + var proj27700 = ol.proj.get('EPSG:27700'); + var tile = new ol.reproj.Tile( + proj27700, ol.tilegrid.createForProjection(proj27700), + proj4326, ol.tilegrid.createForProjection(proj4326), + 3, 2, -2, 1, function() { + expect().fail('No tiles should be required'); + }); + expect(tile.getState()).to.be(ol.TileState.EMPTY); + }); + + it('respects tile size of target tile grid', function(done) { + var tile = createTile(1, [100, 40]); + tile.listen('change', function() { + if (tile.getState() == ol.TileState.LOADED) { + var canvas = tile.getImage(); + expect(canvas.width).to.be(100); + expect(canvas.height).to.be(40); + done(); + } + }); + tile.load(); + }); + + it('respects pixelRatio', function(done) { + var tile = createTile(3, [60, 20]); + tile.listen('change', function() { + if (tile.getState() == ol.TileState.LOADED) { + var canvas = tile.getImage(); + expect(canvas.width).to.be(180); + expect(canvas.height).to.be(60); + done(); + } + }); + tile.load(); + }); +}); + + +goog.require('ol.ImageTile'); +goog.require('ol.TileState'); +goog.require('ol.proj'); +goog.require('ol.reproj.Tile'); diff --git a/test/spec/ol/reproj/triangulation.test.js b/test/spec/ol/reproj/triangulation.test.js new file mode 100644 index 0000000000..f99fcf67ed --- /dev/null +++ b/test/spec/ol/reproj/triangulation.test.js @@ -0,0 +1,58 @@ +goog.provide('ol.test.reproj.Triangulation'); + +describe('ol.reproj.Triangulation', function() { + beforeEach(function() { + proj4.defs('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 ' + + '+k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy ' + + '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + + '+units=m +no_defs'); + var proj27700 = ol.proj.get('EPSG:27700'); + proj27700.setExtent([0, 0, 700000, 1300000]); + }); + + afterEach(function() { + delete proj4.defs['EPSG:27700']; + }); + + describe('constructor', function() { + it('is trivial for identity', function() { + var proj4326 = ol.proj.get('EPSG:4326'); + var triangulation = new ol.reproj.Triangulation(proj4326, proj4326, + [20, 20, 30, 30], [-180, -90, 180, 90], 0); + expect(triangulation.getTriangles().length).to.be(2); + }); + + it('is empty when outside source extent', function() { + var proj4326 = ol.proj.get('EPSG:4326'); + var proj27700 = ol.proj.get('EPSG:27700'); + var triangulation = new ol.reproj.Triangulation(proj27700, proj4326, + [0, 0, 10, 10], proj27700.getExtent(), 0); + expect(triangulation.getTriangles().length).to.be(0); + }); + + it('can handle null source extent', function() { + var proj4326 = ol.proj.get('EPSG:4326'); + var triangulation = new ol.reproj.Triangulation(proj4326, proj4326, + [20, 20, 30, 30], null, 0); + expect(triangulation.getTriangles().length).to.be(2); + }); + + it('can handle wrapX in source', function() { + proj4.defs('merc_180', '+proj=merc +lon_0=180 +units=m +no_defs'); + var proj_ = ol.proj.get('merc_180'); + proj_.setExtent([-20026376.39, -20048966.10, 20026376.39, 20048966.10]); + + var proj4326 = ol.proj.get('EPSG:4326'); + var triangulation = new ol.reproj.Triangulation(proj4326, proj_, + proj_.getExtent(), [-180, -90, 180, 90], 0); + expect(triangulation.getWrapsXInSource()).to.be(true); + var triExtent = triangulation.calculateSourceExtent(); + expect(triExtent[2] < triExtent[0]).to.be(true); + }); + + }); +}); + + +goog.require('ol.proj'); +goog.require('ol.reproj.Triangulation'); From 8f1aab9236a876459d3b428a10de3905ee39edba Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Mon, 24 Aug 2015 19:12:19 +0200 Subject: [PATCH 49/80] Add tests for ol.source.TileImage --- test/spec/ol/source/tileimagesource.test.js | 94 +++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 test/spec/ol/source/tileimagesource.test.js diff --git a/test/spec/ol/source/tileimagesource.test.js b/test/spec/ol/source/tileimagesource.test.js new file mode 100644 index 0000000000..2b90a23717 --- /dev/null +++ b/test/spec/ol/source/tileimagesource.test.js @@ -0,0 +1,94 @@ +goog.provide('ol.test.source.TileImageSource'); + +describe('ol.source.TileImage', function() { + function createSource(opt_proj, opt_tileGrid) { + var proj = opt_proj || 'EPSG:3857'; + return new ol.source.TileImage({ + projection: proj, + tileGrid: opt_tileGrid || + ol.tilegrid.createForProjection(proj, undefined, [2, 2]), + tileUrlFunction: ol.TileUrlFunction.createFromTemplate( + 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=') + }); + } + + describe('#setTileGridForProjection', function() { + it('uses the tilegrid for given projection', function() { + var source = createSource(); + var tileGrid = ol.tilegrid.createForProjection('EPSG:4326', 3, [10, 20]); + source.setTileGridForProjection('EPSG:4326', tileGrid); + var retrieved = source.getTileGridForProjection(ol.proj.get('EPSG:4326')); + expect(retrieved).to.be(tileGrid); + }); + }); + + describe('#getTile', function() { + it('does not do reprojection for identity', function() { + var source3857 = createSource('EPSG:3857'); + var tile3857 = source3857.getTile(0, 0, -1, 1, ol.proj.get('EPSG:3857')); + expect(tile3857).to.be.a(ol.ImageTile); + expect(tile3857).not.to.be.a(ol.reproj.Tile); + + var projXXX = new ol.proj.Projection({ + code: 'XXX', + units: 'degrees' + }); + var sourceXXX = createSource(projXXX); + var tileXXX = sourceXXX.getTile(0, 0, -1, 1, projXXX); + expect(tileXXX).to.be.a(ol.ImageTile); + expect(tileXXX).not.to.be.a(ol.reproj.Tile); + }); + + beforeEach(function() { + proj4.defs('4326_noextentnounits', '+proj=longlat +datum=WGS84 +no_defs'); + }); + + afterEach(function() { + delete proj4.defs['4326_noextentnounits']; + }); + + it('can handle source projection without extent and units', function(done) { + var source = createSource('4326_noextentnounits', ol.tilegrid.createXYZ({ + extent: [-180, -90, 180, 90], + tileSize: [2, 2] + })); + var tile = source.getTile(0, 0, -1, 1, ol.proj.get('EPSG:3857')); + expect(tile).to.be.a(ol.reproj.Tile); + + tile.listen('change', function() { + if (tile.getState() == ol.TileState.LOADED) { + done(); + } + }); + tile.load(); + }); + + it('can handle target projection without extent and units', function(done) { + var proj = ol.proj.get('4326_noextentnounits'); + var source = createSource(); + source.setTileGridForProjection(proj, + ol.tilegrid.createXYZ({ + extent: [-180, -90, 180, 90], + tileSize: [2, 2] + })); + var tile = source.getTile(0, 0, -1, 1, proj); + expect(tile).to.be.a(ol.reproj.Tile); + + tile.listen('change', function() { + if (tile.getState() == ol.TileState.LOADED) { + done(); + } + }); + tile.load(); + }); + }); +}); + +goog.require('ol.ImageTile'); +goog.require('ol.Tile'); +goog.require('ol.TileState'); +goog.require('ol.TileUrlFunction'); +goog.require('ol.proj'); +goog.require('ol.proj.Projection'); +goog.require('ol.reproj.Tile'); +goog.require('ol.source.TileImage'); From 404fa10bb7f630acb4bd7ada29c34fb0b1f051c1 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 9 Jul 2015 19:34:22 +0200 Subject: [PATCH 50/80] Make resembleCanvas available to tests --- test/test-extensions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-extensions.js b/test/test-extensions.js index 70b7c2dbdc..7d7551264e 100644 --- a/test/test-extensions.js +++ b/test/test-extensions.js @@ -390,6 +390,7 @@ done(); }); }; + global.resembleCanvas = resembleCanvas; function expectResembleCanvas(map, referenceImage, tolerance, done) { map.render(); From f078a9c9358d8dbb9b936aeed1a05bfe0ac08daa Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 9 Jul 2015 19:35:15 +0200 Subject: [PATCH 51/80] Add rendering tests for ol.reproj.Tile --- .../spec/ol/data/tiles/4326/0/0/0.png | Bin 0 -> 18226 bytes .../spec/ol/data/tiles/osm-512x256/5/3/12.png | Bin 0 -> 56817 bytes .../spec/ol/data/tiles/osm/5/5/13.png | Bin 0 -> 3923 bytes .../spec/ol/data/tiles/osm/5/6/12.png | Bin 0 -> 9195 bytes .../spec/ol/data/tiles/osm/5/6/13.png | Bin 0 -> 12718 bytes .../spec/ol/reproj/expected/4326-to-3857.png | Bin 0 -> 4315 bytes .../ol/reproj/expected/512x256-to-64x128.png | Bin 0 -> 3844 bytes .../ol/reproj/expected/dateline-merc-180.png | Bin 0 -> 4277 bytes .../spec/ol/reproj/expected/dateline-pole.png | Bin 0 -> 5685 bytes .../spec/ol/reproj/expected/osm4326.png | Bin 0 -> 4217 bytes .../spec/ol/reproj/expected/osm5070.png | Bin 0 -> 3528 bytes .../spec/ol/reproj/expected/osm54009.png | Bin 0 -> 2712 bytes .../ol/reproj/expected/stitch-osm3740.png | Bin 0 -> 2867 bytes .../ol/reproj/expected/stitch-osm4326.png | Bin 0 -> 2507 bytes test_rendering/spec/ol/reproj/tile.test.js | 178 ++++++++++++++++++ 15 files changed, 178 insertions(+) create mode 100644 test_rendering/spec/ol/data/tiles/4326/0/0/0.png create mode 100644 test_rendering/spec/ol/data/tiles/osm-512x256/5/3/12.png create mode 100644 test_rendering/spec/ol/data/tiles/osm/5/5/13.png create mode 100644 test_rendering/spec/ol/data/tiles/osm/5/6/12.png create mode 100644 test_rendering/spec/ol/data/tiles/osm/5/6/13.png create mode 100644 test_rendering/spec/ol/reproj/expected/4326-to-3857.png create mode 100644 test_rendering/spec/ol/reproj/expected/512x256-to-64x128.png create mode 100644 test_rendering/spec/ol/reproj/expected/dateline-merc-180.png create mode 100644 test_rendering/spec/ol/reproj/expected/dateline-pole.png create mode 100644 test_rendering/spec/ol/reproj/expected/osm4326.png create mode 100644 test_rendering/spec/ol/reproj/expected/osm5070.png create mode 100644 test_rendering/spec/ol/reproj/expected/osm54009.png create mode 100644 test_rendering/spec/ol/reproj/expected/stitch-osm3740.png create mode 100644 test_rendering/spec/ol/reproj/expected/stitch-osm4326.png create mode 100644 test_rendering/spec/ol/reproj/tile.test.js diff --git a/test_rendering/spec/ol/data/tiles/4326/0/0/0.png b/test_rendering/spec/ol/data/tiles/4326/0/0/0.png new file mode 100644 index 0000000000000000000000000000000000000000..0b99038d5a318c0b16915b3f0e2c9ce7e3c0113f GIT binary patch literal 18226 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 cgXZ5Hm-Kh8x_z^;rh* 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꫿~wJJ_UhUAk=upLDY17 zuG(h(7c@{Z_j8AwyV?4S#I7n9fJ3#{(9mcy*EL`O0Hm~s8Ic+3_hn((Y3QYq0@Gh6 z&lb9y5N1#pc*T91j)y!Qg=`E(u&1$^tVH!UO`KRD{7|fqLR~THGhqReiFB#38n!$C ze=uo$J{{;E@cBkvq7yZ*N{?Yubk)tC3chY;YBJwks$gOL#hvcH1GEEhP)=R6}q->t<9-Uzh{CQ4rPoaY88Xs~$h z7s&o>Y|f*41UIIVMg4HV+Fz*eEv6D;VR_`e_08}d^na+P5d4+a812JO$5PF=s;h$^dO5=F0r_0LPp;RcHD1=mrUff+<&S8W~U_DW!R`IPe*mkVJO z7^2mg#QNtoJu;a}TOOq6VcC1_(k@uzvr+CG!I<9VE4`h96Z8+VPOelE&zh}2K}~eF zE}=ju)zFqeNDAh!$+gAWKv>dFFYaN$H15ZTp(jTjhkEs{Y9mLB$--yJX9j#tMF*h& zLlOYdi^i)>RL>To9d3=8W#yBzspWV=%0Z^en`TP2a`p17;Y^ITRC&HAI^ZY_#8(nT z!CXJtxR`~J<+Ig1;R{F>5v1BgDKMT`QBEm00^b)Ej7qX+jw#cTKD@yND%?XAJrPJy zP2nSN&c+Nq1bszc2)M7pQ>rpIvORuyB6;WCuJihF3(_2)jjigc83^cCSC=UeMdMjs ziuP_OXq&cr8d9ZT(F+LW2 z?-rCMoQaB#ydf*Mv8TqSInPTU`z{LI4V& z*seVAXVlDU!k5aWh-z_29K+e;)bT%+29s}KabDSXbbTV`N94HM+^L$CbTT7q1{E$+ zx4YqoPQ^xN!=SY3<)U<1ZedgCvVzzs2 zrLqCNztB(f{`nN!nqVgN$Fd1svuuUwQPd%y%fAlimV!-SGQdYJGlr9sR{7Oy`umBW zh9?dj*sINJchHfzelKY;n_fTB_S36tKfcgG9`O0szV|M4!iOChat!v^iagjyzArC$ z&Qt;cy;xX(?yJ9iqeps97I6(XAu&baH%`w7ATn(qL$>~gU_N3PKtpu;czc)q*Mn32 zp@7W&JgMF~Sd1w>K3B zh|f`^)l3?BI2|N57SsK-y3N_c;>Pqic{wUdS)hwW&Ut2la?-<-; zxg8vvyw~yV)1EGe{e!Ek*!_w5*u=9a8ys_T(3PLAhcZ_%6<$g#<1<8XvHKT!6P1&l zrTx@eT-y0L0iw*yMiI%U^$q}V1Kt6qN;H3Bp-BBbniFfnEXm7v!ik_Fv8=`q&v(@0 zqJ(b%h^NgZnMp;m(Re0--S?_)5toHC;rrpo-ImT?OE-qQ(AHqH@>BQtKaop{2_fQ% z;(S7uWj-Hd!E82jZ`Ae|7aOm`ie1JSnG@~DyL&4X=ogl8C7j$A3?V2)*|^YJEhd-E zvdX!_moO8UB`$m&H(|<) z&wq;Y3vfPP0is1Cv65f|He=i1NKXha_gt)0a3>uA&D9-lv-G?zqCbK4BM$M3Yor;@p6*S; zH#RD71|D&`^EMhqhm4eA8|%Cl3MGQtz#jX|Lewo;>r7C@4UFC7A#L{6ITa@a3Jd|! z98d|NeZD4vSK!o@p>wcP6lRH_$tTVY?~|Iah)j84saF+vk^=f41}q69zCCk zAiV|`Ro-mntt4TG@XB}sa+(dC-2f;Prx)ulEF&Bti&X`?3Ox={+oCOLa}hxOR#d@T9(q%S0eEY>}1fpjXD3rcYjJ+(kR2|3@|=A)4)bXqzPIgngZ z^A>SB8T?n1Yp)!r2p>qMaj0qqhwY3`37&vQ9wA|au62jv5S~C#<7Jx(z*ju=_l6>CJNy609Drt2{$V zIB8BAY+Y6$E*Z(la#nV9{Bz%U5)C%r|hxmxH@pZ#<84_BkXzv);JKl+O5wq0_|ex>Z%>92Ipg-@@WaU{ zuumqH8J!GsTq38Bg@bxp`r_JB#*h={Y$C1dde4b|JM&BF5_tdG+7DlSo_v6{bH`M7 zzPJ~-c>MXRlapw}Cuz3AD^xhZ62&anOqzu1^Rnxt$Lr4TwC!02GXQJuv-EBZx7rmB zfP&#z^buhry6vr3PCU5BhGhSH=e8%6##33RZF`HXm?Y%Vkjbp4Xdlf+6;r)|5r|4G zsNWtB_O&ZV54wZ#R3Mx=HDsMxi8IF$-K5oEveTx;<=n_2i^ZNs$*3X4c4CX`fm==W z;(6By;WZUW$(oGQ#1nAWz=JC6#KOvpQhc83#s*L4SA_vwK3nb6dUyyah|Po6`!kV% zKZ#(O%c-VP=>z@l6Z;(b=77Kba_P^%A+%K3GyR7-6rNJb2Vbv|hVMudDHK2g0Z?*9 zQU*l@vI25mDw_gZr=Zu236Z4~4$1(vlxM_D^UxTeWod{_1ybCZccvn%#?o8ELpxrt zA`R!Q?%>-%Ib*+FuuChg?v|hCcyB7kko0=QE%I{^>sidqp(%m)T;>yfZ_x9_bZ~G; z>1dZkNuT}Hr3FaXSiy!o?W!S*k}~zAed^won`^dC8)Kgt1&p#24nU0HXB4AHb-|<( zm1i;rI>IWd`IkZZybhI_z=9+@IU7mkq_N2e4_%jO{lfdFvzecNwPIxAs$v~J=@;j5 zH!rOJj;ztA;S+;95P>pDLz6XR0P?nSGCs{lp(h{&TTA*T;?n{g6e&6Ec!y%gXDGQ= zU{*4mZ>2`SR9KKr+k?3cgabG={nSc8>=wqi&p2``TNnJ^; zVQMXkM%0X3st^Tw5`B8mzP~WfYwToPU@qLufOoHJ&=vK?jmgls_T+-|-Gi6!38E-67%haj#jbH!HDtS*KXV|=R`6al9|a5w85)pBz@ zuof{WY;5}mgmq>j0$6C)n*G_e)rPC^a!Q?wT~~BkMgZBHp#@7{1@7wcd~_+XddAW_ z;hwxX@?~A-7}rN?RsKEC7jcTWD?fDDCwrP;b&Lozg)Nv^@0SPRH#k$ zVEzBVX`q8$*0HH*%LJThNyqv(28$3pOI73gYobO|pKd0ztbRp}cvS^z`fLf%C>l$@ zk4vB#70Tz4xKruDyA$p5M9xW}c1>IkU_rJW!4K@-<}z310!cS92z6S2K@~% zceNtNmsPNx#;S4>cTh=g8hjy?2;KC0PH0SuSBOytqA8j z=prkugz}{*FKD2foRe6(6}|_|4dPeBS)25=BE8T&2arpUWmFI78R7E01OKBzX*1x8 zjy6DHg0)uWrc4K1Lev5cpQF}8MGd#C$NJjpj2>494xZh= zm6pjDGwUXINI)O$mbOhfC-@1fFL|~u(Mcr(FA?X#&TJa^PcO&2J(kx`c&cqRU_2oWmT$jT|DxL2{bB!ppqQm+9Cblc_;^C39B`i$U0g;G(Z(@TW`S>Akje300+Q& z{e%}GGa(52oG#6w?M&?Gj9gJw zytuKEh!>i83bWY#Jo$*f!eRW+L%bg`Juz$b|KQbqR<-J2p-pp_?gR^}pUs7>7u{7o zXEv1nVl#AIQS}_M4e+a?6wsIQ3zZ0()JPrZUar2$<2aG7D;Dg8~}Nf zVlv@`$p)lq){FHS!$q6ZHO;+uC2)ftsOMUZHJi;$P}bYVZylVHq|P#<#PA|t ztY4$P-GXV3f>qM8McjvFClA0pm1nTvDx&GM-E7Y9{QU0v@j;uY?uKF+D!pvv34wom z^9cYEhRf^J&iD4^jUbOAfDStt3}+udU1_sh*Q0Tp)_;9DBxJf(*~G{;%zJX<$D{k~ z$M-oZXku81b-0i&Wi`kXgv8MIyGqP=WGy@G0#1f!|`S5 zq!ASeh@pa{FWwd6fZ%D>I)LDO*sLkBB}DFQCUZrjx*FyHSZF7a1D+UgJdctFEc)oI z&Q>P@YoRnE!o)kxXzIyG#%mtIZGWEG>)BjY%?;c+SgwWuCWOf?Am>m)!4OETUBvac zNgx_T8_UE8?H12^X4P)q!?qBE!88wjqxz0M*!}4U4j>RqQO8OqP2C>-yJz>JEOe;f zRz3@1#LB&y_49`vVe9NWjzjfp_iahPn5>I-$A0VfT>md;9zHeQ+gr99MGohe6S{o_%e^n5O~^rV)rNG#*9 zHs$7Ljt5%YSpU`x2X5-19$A$uxBd+jQ2-?Aw6qJlOY;`$D@ajFEnnDuAR4$*oVP7o zzOp%lH1HE*$1g``kL|PP51_`2xr`^WzrHhjbVw)a|L%!?_Qzrd0z2htnwQ4b2^rB` zBQBnZO-Xexs12YfMZnrtG7|HtCFqic)<2t&QUUr_v*wh9UJ!fOh8xNr9?%f-5B2~_ zu~D=$zg-H$qN~H%Zg%8E>#w5ARRsad$+d=M04%7Vs{GsnK|7JR%=r=>Q01Dih%(S} z60G*;dZE`?VIvjo`xVCi>0H`WnZgXsapY1>ZDY(2huV=vn=4#Td;rrzO2Y*_#QL&j z(Am#69CIdp-SRzIT;G=$%HP+k{JSfQU0!!IY>q~vXet2J(-m}WJh)YRBd_!HS?rBd zZ6v+|jAm2=UbarpOZi>}IMyDEkT#?}(l)ACy7qIMV$rZcg$PnPo+dv)=LB8!noGU>2bigxBD^uHP~5f*P;Vnx`N1an zPKxJfF;SE1d8<{Rr$SgFq#&Ba`Y)Bid4B&;`KmAB9u}nXH+<4{l$fz{xs}D_N;9ya z`)gAUyBTl2ijCE zBu&NGU~1URYV6r^s&_WVF;XDEYCUBnhP#U37tE#=~KFyHugQpb&{D1SwJO^tgQ*h1VChX5-{o%CO z`H$b}GQa>)|J|vG=*w;1+=Zl(l(fQ>UfvXB7jl?eEl{Z&kHS(z7oiEJ zyaorro6ip=-F6ZF3oQeak;9d5TL;!{u6LZ@@yFNYoXQT1?vy!YikaxH`zxr(W?5k?u8zXxK7j{xy7vG zh7VQVg%}+bS~QVJ2oRDKd&1s_{oS@eG`*eAka)9-3!F;;>V(?^b*cy(CJVx3btMc+V(38kxz?}@! zHaEXTx0^M6?U~=Dw!xycJKGYWBtLS%_@#%q0kheDZ#tsB^->$eF9=?!J6L4d<5GLQ zy33{l_SfIl^3t;GQ+I)+G^tzgc}QX z5sHc--MerJ6*I6eJRcwV*1F$nQO6$|tIlYEcm)8$+&7c`}Z_vp_SlO`>TiCA3C z^mghUs_EZ-v2bG8HpC;#+MEE|4Qx7)ve@sLW0it82)7s03j zAoye<7Ks~_!Ob{a)nphsX6tZR92M^k(Ae=y`X_^yE3^3gSiyGZ~)jT z#E_*{Ei|H~=V5^h*KBi1;b-NAJc9h3E>ICMqZovz{3tZh5H}aNxBS!#{=+b+xTH=7 zB2P_nn_Jh-a?q5b-W!bOtoZ1pJikh=rV8|YZ7p%@Nf3#!9mE5w>aIwvrSgKB6AO`x zC%lao>~z#X57QOB+FmfTSGKfm|G|mzgVVmfNB8=d0wU^nce&XJECqCro`^>8{`!7o zJ@VTZ!iVdc#@AUPz5SEX^@DwO#MWO7g)hGbI(3R;^-rJ{qgv2YJn1^tu6kN*loOd| z0a=QsG?sG-zEKZ6!YiO#P`{!Epc#|DcEACoO)k|T#*!FWL!jhvoHpetx}(F(^Rba3 z8???3uCAJN^njF+{cfUu4tW3avR#w0l{D*iy3N6rL{iFdSR#{6ZK#{vcobqVf^~S^62aP#aMN`Q)ze{jcJHNM+9Ei*W>p z@FyOcHOG>f8ld(A`LR-!#7q}#0O!fN8t zcqC@Zm=ba_oB4WteQZW0%claIN*bSy`bZAqNm_R~Ax1FHa6kt{aM*xiDD3B>zOk8* z*Qy1wYX=TxXg$O0(EBP(aFZ5JdaL}#B|DjC2MkzLK@~u{7m(ekmZvhmq{v%b^ce@VI|-Tu|>k@rTPTzhi(wZmhdjvadSkf+CU^Y3pSx^Sqj zyZW7%+NYNiAKzT->Cgo!csDODVrhq#XrdnMw$=lc0!t^tbB zOlglB39;?a9Ev2Pog%SP!q%%rc?Jn`Rhe}>o22~*Ra$QTdWv!%?lRm*s^ST5gWRtg*3uHFxWW$g$Agy~;R`f9tnP|ad4 zysE3^SwONPu?YMD`mIhYP*2Zr&(*)Vdf_i$4K4>A?T#})I1}+ja18Z^bu^3f;CiA% z{rbys^E13i`Wqf!mAGg zy=@jU^-cACdBEMAS`Gwbxn$Dts3xdBow+PszL0#tAZ!OV5Q2hbOS;N|Afis~xesk_bjZP-fKD>m}e9P3|1Hnbm_ zE70hn^}1%F0WWWLTNLNE5Rm3y^xM65wqN_J*I*5aJ~AVfV&}@#N_0MG?Qy1)>Bi*s zTueAsM(zB`tDX3O7c|gK$142Ux4H|bRCl_y$_r?>LwDb4d%dU+{)oLrGdQU^*IBpd z(At6Kw;BrXKICW&Ml9tp4R5u-_POs$oj!mX05KXy3Z6{OaVuQ=uS0iRp9H|~{`Kh6 z_s{m0$7U7pYPT!;J?C+>eTe$8iTn51_q6MukNV#l@v53}XL;^??}7Yaa2y#qLA%!y zh#LR!)o%7boQ~cY_XFD5%$l(N?4+E)gAFwI$EL&d2YBoZ821*XzlHc1y~@y1jVwt+ z2F@|Fi#PQIn#nw1+lvq2xi}Ape0YmSka+3NBDoPt+~VRKwhuMLLnF+EBCpxBtuAbY zsDe*d^H@Vmo2G1#S@@K)b*nkslGUZ1`|&xpsn^Jop3P-m`}bZ`6z%Mf&f2^da)Q93 z|Md4yH_TXL%Pt)Ae0Xd3Dg|XTS!vg-_g?O}Iv%J3{Bay9x=eGqY|B7Q66)jC!`Y(Y zXfe5r=`VwZXBs5UxRhAgqjig91s`yA{if&$t7nPfSw!(Qdl8tR;^{40wDn#a5B%{f zd$9iHQ!9A%;3RkpA)O7zkKyK7r-mJu$NbZ=S%v@#X$Q#ufBEe_qy^*?;F(UF!cKm5 zbkP3ZxvuY>?xLsw9E1y*TKerdXW|Ny5!6~e?z@yIWx+X|hGr?HvB!{~3oZ{3?;L-LxBL-mdy zD~j+mPbI+PVq&SDk@1SI##a(_12EwnVYn^Rkyu#MHFviYwof7!x;l3$DJ44w+RVDy zv&W@bE&E^I4>#1?>lIQE4O<|CTr`_fgZMf*-Me`vaO-CUpF8NraaJGgsPG%==$AY+e=#(kSbSf7T`c38}bX^)mPij)RPB@^FJE8Uo*1^ zQ^f!&Oj`@!{za5+auNy++ZN=wqG+xbjUCvy!~J$L_`-QFbONuWm%n>_0Hc=Qj|Ihg&t7Y?hftzVGjvG`4d%IrzK#w$Yzit@mt~&JoKht z83PoH+E%cbf(BZ6#+H03BbOy~w^&oI-as@e`B&^_IcKrt_IG;*J#CAD_|wI>kdG#A z=k($x`JaD`6?5Cn$#kXzSktD4q{SY~Udu*eQO5OtM9b7Jb?UqsnG^#)a4&cS2+0rvQ8S3_KLl6Brbnn}3ua$DW zTP!Fx9-G(--YMkmaK~n(+)Iulg$#|_Cbx)%7vZmfr*8{VndM^o(3|ZSh$Ub4oV1#( z_0)2y2h?27rJS&2w4pzP0)f=%#cdppXXkyfa5US~t^wmz8c52rP|0TjMY16b94eRs z!o`Jr3b|?D`@5&RXiwQHG$A1nDFwbnSCvEkR&tK~i3(`# zg+`x^B7!d|i?H~_{`jy+CWb!Xuyv4_19M+Ef6byzyk<&?#N!s1kGCSd zEb8RnET>(-S)2D;JY7h{lQczTW+BL?%ug*#n#r0RMi-nIQ zve*{Buh+;NCXmk?)0o>zp%^(@bDIfXvz95;ib+d2S& z3zrtIi74x@2XNa&jTpiSue#3UcY+Mucof895)X2#i(YyJ>%kVxis{6FQ&A*w*!_D} zmmUp-Gt?d%g7DC-i7m$6dEI6Pqr4tWelEeb{}dud6m%1fQbNKebynNqwf463c-kmxQcIKe@V`mG=BQX_+(0xfB+1 zf3NU@l`bP?v>j<c-&KzN6)P$_&;W!bve~d(D%09+on5ME}+?p)q5-UT=as^UyyetoLfs!E|u?DFcJ0n_2iW$0;E$MtS&D0mb) zwx_H9tSJ{?4hG_Q7h%d%l9Du;v;B^qam6qzVd1XE*XKPp%Ln5KHy>J+tQ)A8jj4rs zmc2t&`?Q`yWdHKD)n84Ar1^EeJ|c7oPW!s7c1;-Z2K47Yj!nJ0SyvZdH;-cEHvgq8 zr+wiR*9JQ@Maxgb#BaImDkLu2HHyA#&p{WaKNpw66S4ewNVIHm02L|@P0WYR9q|^( zK{tE}lQ0lr08<=ox+xK(z zVF1(x*ib~rRV}8Y5HYBd(xr853eAa5b%>0xo*v)x4xFHC%(BncmruNau&&(;h$3*| zfcP@<$UDN6P8Y{9kaAwlk^pdo#a!pI3#9iC z4hpbL`7-o-)4BW$MYA{oW$93W({hEdsye`r@P@hT_9`|G)@a zsSHuM;e@kYIY`L3w;|bfHTpGx1|%N=xJkDB0s02g5|>RfR&mJc+S=bIbVVbL$PBggtz;NfNlt^J#d29{#uTzEW+O6U-FJ&xawTfq9~ zD}oGe3&joGhM3w*RtI;B{iH`&U?Fqz!TpJJ*yXXC^X%K^ipjitrv0N^K2SwEJVg^z zqZT&|C?4f0yG0eWdNOW4`QA>Q`(!H`s3OUQIm$=3*0{lpwBfL6rw_PxO4jO+UhYI& z5t?1)ayLTTeMc7H4>Acv9NBEPAyxfsD!Xk7Ivj~?HTg@}_eyHH0HAO>xeba^)v=if zt*s))r)2luh0Z*SQEJ4p>vDJZCUFL@&jTi<)aWHlzbu(5X^N&6`rgKTCvj1oJt%QE zm6j_2Opu0L&=AXu(1L#MwC(EM05p;^eQ@1v9h7P-r>lWuhz`NC?o%*)wi<-TwRhG% zQoi-1kZOsUn~lv4diL$h>81is3fg}YQnVpwyFyvI1J(gT;nK6AmWXD_h; zT1LiDO-)u41C^sW0J-UohKiyyz~I9hYd^fuiLEa*=)^KJt8p$M9stBF(L$vMDdmDJqWmeVm?lDO@)ItNzz3Q zyHofNVH(#B7ct0;MbV(Tx8m<^EksfIymeZBAclHW7cdBCer!5Ih(vHr_tV*fO)RJ| z5%=%`t(C*SaMC~u6}E}Id)6zuqFANH-uzk$Bwst`J+8klNS0&_nynY``khnVxUn+z z0L(FN058sIFEq;Z8PK^6eO{;|P*QR#HfsLZ_e=h-4%vpv7~Jw7Et}1lRW+}>u##FM zeS|%kkbWx%iOJ4x5T9DvB)=6kPB&R9C&=VW=)Upr!-t7mmh|C0?o<1_w&f1+W#v+G zChCwS^I7L9-nO6;GiPF4)VRsR*u>e5HJ@%`42WrPjhJ>PX3SPB17mO(uB64cwj# z2?fxXhFs|kgs?0QNs&wa~9ZSbl?y{GMokKmgGN}L$@_=4T zmnc-SjZP|qf+21dv3-`)tBCPk58a0GS$ogFtU(MFwt`ajro93rF)Jw&4hP7IRj&)s z#F-7-%M@U@5BA9n@-(&zSC@l+V70SDp6OmX<=Q?XB&9pB$98i(F#S+U<&5{Q`7l5# zbl%p$+&{Y#&G+flxqfrqWMojK&jEtNin{Q~+5|>|Qs!~0J#89@(^hYT=n~48AW$<&RS?}M-#K*k=w3lI zE}k1BMzg`2Dqm|tT|nBol`v97F^!#o;i-TeNj#e`>(#bCA6*Y8jP3(#UWanl)lxPZ zmtIz<_7|5J8t>#cD>VWISG;N|7}$DT#T zCL^yN^}v@O4X+Ni96;;Sdr|I_n!KnC`Hdh_`1M~QaL>Sw3-`d8!5?fz_sHb7*Mpjh z&0w{0;h(MlM&3GlJ8C;be@VC2Ns>{9(tg>0y^LNrXwrk|6Y9X=JlNJku%@u?%C97+ zbZnoU;;MkxYg?7DFvW`H;#;>vR@MgWOQSmE=yS*x#=@xKkpH-OsB&%_9SPXCYXK#X zIJJlX1A)ved6SP1)_#4jBp^XUgB#gjBTR4Wv{f#14cy6~J5CnSLB3Sb)FoOouvS|g zKtI$O?C;Jj!81D}2GQhKOj!TAX`q#?LyrA|Tv)VtOGI;Kbb-3_Cpq~?nSh z;PZz(pWIo-J<~ai(dW$s(g7fTa0aNH5O90-E~0fVA+|BfjF=`X!b4EkkxCW^6T+8+ zzg3!KTx2|39l)s|_7`2R`Gp=bkNggb5P2;*Se4;;M#$UMwg7g-t8KgZy9i%z%2ETc zsxVq2g|fJjMwQ5NNPjDA%)+S?Vv4^-EBMF0%b1i_IRH%-s?o&n|L<>BrjZP+W^q5C z9d_>qHP9C10hAmzn+5ie%X+6~%u;#Ylz7T*#&4TV1I;glz&2IN<5tKwd}6S^dK`FX z#EaeE6e@%elFk}bq@)%us5iRjc(>tUCr7D6yi0!b@k2tXU4 zfBqNnh$<=DYG@M=!AUFQagjVK8VQ+>7=_XZBkmL6NL))|&$;1_k4M*Po+1afQ%Q;5 zW8RVvMpVA-HK{ly&7tuSd{!olLB+15dP;*KPQ9phjSpy%1K`gW`Z}nVqI(PDj=h6D zx%mj(sSwAYt|d4*&8Hjg{K_n+XiOXTO;4`RuRjbTl}w0x@6Pxs+es`fTxRz8$3hHI ziOwcCo}I~6L+s=scCt3znO@7X)BhmMPX6bW$tZaRL^9YZj%One%Cmj%#zWT=yYV=e z-^sH$OdQ!RwUHc%7Zh(b6D~fC4PHOLv*5@|M|A>!VeZ9^x|a4q-r&)pdBN4EW~4{Y zLQr_$IM|UQN(P|hARI4L|R ziWEua>Y2SmetFQgzhCd88yO==X30?BZGkn;WSQTdb{z+dDjW!t3a5%JAa9;@FfZC8 z+N_#Kqo7s{0$HY4aofrAMmU>JW^iW0_oLY1r~sH{cK@3v+BW4c2`!)|uJXzuAX7A| z&8;O#5fW)iPZ^06V|tKUunGQ)osN6HX9qXQb4K>$o6A z?7k%~>*XeOCJW7X1Yx=@or3c5!RqI)x4%L)VrC_F>CxJe9?#@r3=wu1x*h$IPOt1S zyDASw@hfwQ1x!!Ngv!My%JcN=T(}dAW;0SlO6Z7`kxP}CCsWAg1bI6-=!7fLkW=&| zKG|vR*!FuQ9qhGqlSY&|027}_&jXJ_wmmk;Gwj$(o$cl$%uzxZtEv|D4-l~O6L`p4 z#Ftgc8!laml~i;gM))r@;P5DTN-m`B8>gk-o zacm|HjUGg|;?W&=`pOcz509?sZzT{qCJ4V6RCE`9mO@&fl1Jxe#!0hFbV?U-0zdnc z`HR*)MMO6Y^-U9pW~PbcIds(kx)G0V8Dh~{%q1pmmWED2V~7)^5mIYTD}}cBYk}HA z7(-X$<_71w%ewok`qvrjo9th^Tf4hETYq-{?El%XimPHS(Iz@3aMM%NDQay}?YEe3 zl;uShjfO7kTkM@Bw2jjy$SYHn)mh4D%jhC)5&Id7ot$8jX@t3%zKXsT29>c&txu~T zLJ##$O*Rq6X6bZN$MhC^YipBD9U)OBDFZW8l)?FR)(UfTb7yOPe{Yw}V6^AAuaH(4 zb5zD6qj_{}oI=^z-o!WHM;6GNJ3DmZV&Cj6nLwUvB{C`0sjjJ!_6Z7;S%xYjPcJkP z2&}EG=|$3X@ASsbPEUDH&6}EL+}Hy4C;2FPW*RdZ=kfEUFG>&g_d1I@>t`C8N%iQC z;p+b34kEERtNAC5);BU+iZ=c%QTpMg7%nZyFH;kc6dYkG;vc|KhjJZzS z#82WJnX)uHM;{~2_H_}f=c;BF=Yy)HZ2{mo-NM)a8Ge_M{rl0Pt^{gtQCY(yV*po$ z!zG^4)YLq61ZF<$?&WcaK z^_hEm81?1!dDJmZ;N>69Ydw^&`rcU!RJ7l`o?--QxLAaATEA1Lr{l_GXgN^Ec6I9Y zQR@4C@#WTJ8AhioNrW^Mt-e@OoBf-($3;m9=7TC)id{`l-*B~`vvpHZr~^PD7nM*3 zh)keAULiFJ3+~0qLy*#_48V2ij(Jzvskin(3~(hRbt0lr+yHT%LmcQ`_7rhlxAs!% zGvtjX-fIeFj$_yXyG>Tj2s8K8=6;SALkJ~TcI-%Mti*VKA-=W_}A*A1h zMS{;~i;4v30+`=?NdRvmLLMj?dCL33c?R@ zrSZs-kN@iL3I(PM1nNhh*O#a}V ze0*_XRd*&?ZP25+v7PVckbuj4i%!xuQ>qhV6p$o%@G$h?k(_V7yqCg`iq!1E%okC1 z?@qROQP=`;#9qKEg)Am}ROdb$HgXNsqm-^GfjQ_E0Yt`gg?Ex8t1!N{m-8 zNnbD6eI66OfZab_^>b^l5nM0{^*ET4(5u@P6;=QAQgvtKusDyfKNE$Im&pPGum4(# zc`kP5aWIXH;j1if#c*_=$>|&r+vpsn_3P<2Y8?tEw&^wtszGw4oQa|@m_c+1pq_h3 z%g=A|OCEaB7)&Q)O2XR(Rh~chyyuxNy48ABA+qgP4krA8XVTx&7D2@2 zT~1Vdz~G>>MCaFlT#4M-gR_F+CjKsSnnMhH%*7o?B?&`+kf_YQ}-*zk8T-L!9KPNmlH z$WEdPAkHwcqT)NLbzDdX74ZYgfAJS126<4?-MNPXoJn zn2i=^J-OzRhPz*!M}nzquQI}wm&4pA$g_(u-0)rXZND18EuxwAE|nZo8jvlgotOhh zJu^^WEzo%=Wu)Gv_ETy#$)ob-VL*Z@;VCHAJ>41jkY2|Mu(r82293AVOh|vF$k8&l z^Es{VS$4EXmsa#CaE9u`8GS?Z0En1;Tq5(Ya(~*ZqDQu(P=tKXrlWk50u1p~86^Q1 zJ2m5IdmDLv%OCDzrNTXlD%5(SgvzLJI`bp?Yk3%C-2GX)N)tO!{8tW;^6?z3p5Ln} z^et%RKIz9L6K2BQJbN%UY^dgEfiUr`svOTfp)ARa&}aZC3 zoDO`70RX*fVNh;g!I7{Mx2D%AhdrPlZt_FZ5A)XOLLa=tR&a0zp_JvLBOs(#0= z&*#$k#9|rHPjuH&)Hb$-@#_>!NMn=R9>^z9PQs^CzTQr8^|#ntZiy}o(crsKDvl7m z=hmm?=o2feYU6)R4RK0FduHfW(YR5NC|iUNWm!tUm3kY9%jUKl*D=t|_Rdk#uv_F3 zIDPguZyr~zN9eRwy2U?U{vW?rU7lx5???;s7v9(2hJ@U{ir>6JEW zd^6a1IKbf8x(@7)?n2&ecMzF{;un#0ta_9T@^nyHFwB%k1BI%guOB!3yR|L`9`b7I z-8R0gB5=AdSGefD9~ik^7UTF^*5h*P>28l|;yNGv^T`&)_cmqJ5URa$&@$iET?lZZ zsoU>~U(B$TeyBLe^Ig&;KZ=r;B`o79dR8W<*~P0mT!`;AQ*_S8qXH@KaJCf-F4&8C z{^Kzh;zj?ev)h-O5Y#Pul!sTsfgIuj5#DZa_a!edE=y)^w!(lYO}y;STrfhrOW?~P z3@CCt2X);p0a6t9+kmgcHn2Ch;Eh~f=$|~c_I>uYZ-gB_;t&eAhK)WcT&cBZC?8!> z+orMd6*lLQ5$jX#ZQM{9qj7GqHcucUan2rG@W>?1OZxAvq?2V>A*5 zf{r^BJL=3^@Vd@0x<=%bP)a_6>6Lm;K&R!E2+NKPwPjCWxD9`OUR*dW{tpt&^q6e>HJ`wvOghH<}H1B%e6^(WcGV#d2PV5H=Y!(|Uyoc|KLMEF;_w z3wu+=_n*`c>DHf7+g3o#rR!t$j+tRW8;@dNI>U96+!!Yj{{GoOn?5$K59k#}{T~EW zL7bmfAFHaNL_O7H#6auXB5hb6>&^A|yq17|ul=+lX#LiwU<41TsWQ}Z?(Wp(nYxsx zyOJyc!MuM5%t91FRwOUKifkaXdg9UZ8a06V@AKn4C|wZu|2zMHxJi&g>*zI-tYpy9 PvkX|6+%W!T=ouw-=;rC{;l0V)q_)zn+pFTn+~DHm-Rah;wbPWV&CIjR z(7Di>tj@vA+QQ7-#m?NCuF%cZ-qP6M)!N{iu+Xr+*2?6?%;d(bgRIcy%F^b{)aK6E z=+e#P$lc)M;KJJA#M_><(c;M5^Pv5K*~-ni`Q z=DFau?(69A?&t97=)9Y}^6={P@$LEe_WJqxrMA@V;pWNW#L?E^_3-uV?c?_J@ZZ4M z`1SbS&ECz_;G(q6$l}8M{QBn6;ON!j?A_++-RRW5)YjbLwZzuI%G$=z+_sUnz@fnI z<>>9??ZTwO#HPg7!q(R2(AdS;+UV5q<>G1UK^vSKs)7jp+mbv)$ z^R&X&fD^M&Z4!{ z=hk8q&D!eS$mGJ{;^XPqveUiV$lBh$*!BAKsC}rNvdOQAuiwz$nzG8<;@z9L$*sQF zuEWiw*`ui1rtI40nY7BR)V96O)0(x+w!E|B?A++=-^97UowmfCw#ur#(d6UirPimU z)~VCn=-<`j`}_RD(AL(g(8$!+$xW(6;yTPim$kD*g-MrWA@AI|W zv%jFf=jPwu#o5ci$J)r;z{c6Z&(-AZ+pNFR^YrfY@#~?l(f0K3@cHlc^YNpz)Y;wN z_xJV0(Br3jr_S2vG}Qnv(&M!x~sOx)yllae&eX`001DSNkl$t(nE*b`t+&7JE3iG_NHF-##148da{z38V!uJ5p`cDDO$DZq zC@d2|bv@#&+&7Sg8EB`@3^m@NIWOeJ1Lz4-!IE+oK;W+j=)+)SZb->zy zLMnqi01gUG^*};6V*ul(*Mf+fppcE3La2h)Z>kNzjTMu2H-$(3FS0QT?bKt%t|;UT zL4_hb(jEfkL|kLRl|kDu1biE0+^oqm0Q~0oZ`UENso<&rpBMmMx$TMb|m;^Yrr@?!yf<}0mhS)4&u!rBMW*p5s%=Q;wI`c%VBtQdC#1D0MrJMjHw6EVgyidWzc#AD?STp5$YN;={_zS0BTqm?Eng`2Za2! zW(ZszkaUFhA){{j9sm?icOa&)%AgUo765)bq)ah3$mp9<5Wvkq3$oUwVE_lsK-7+8 zK8v*`K+!`qO+9-AGGxkNT=AQQ7{CbX%zz0AcrzOuI$UFACZK|R00+yFwXM*~0CqGa z1>j}qx1rLgq5v>hfX9I8R>SLm3fSF{4FQW zfw3Ta=fmCIw}H{FuCA3P+!YR|1`zlK;$Ge0iWW#2tn2w#O97hhp`8qa1R9h<8i_mw zQwDd|gQ>#H<<(e^uzVG;p+J=!HdhUEwABI?)DMS;AEHpP+|BwL_$Y0AfRn+x9EWC} zdh*E^fM7}eMr5sTLN0*X0OAUw40;+7Q!^BN^2zeQ8#EU`Wy5;N1yExvoB(_*HOM^t z8Uzee43z)#-)$%_p@Wq#?{L)CIDJs>yleoVkV^Y9_=ST=^Jf6F5!g``pl;fpcb3-= z7C%`B6oY=tVy&Unqtcmxg8VU{8RmL6RK}RPnR)5LP)3S%cL8 zUMRl_3busbb=qhr)Ce!j1Yl(#sskGEF&rh}yZ{#rd=7PHrW1lr%H~9hdU|Q-Hzs#7 zJ5dop0v{cL3f*!6d>Y~gBe@?81(pszjyUhj>=OM{rwxFYdK{^~HCPQ85TwTkY8WJX zoPr9yGG%aw1>)-HpXF*|WY`(l0q~I_E0S#laKr`-4kL{oROn}}2_NhKBuyg;6M=HK zWCGwIX_r3-Rlw_@s32X*P;5|Q=cPIs2U10ic39v2X*Elgv*iIKi!iw=gL7N{1BWeJ zzB7*%-&t6k&t(}Lu6g#;+>q6a%pA6Aq%lkb(6>$k_4up{DTa1HxlC1n5b_TeiC_VO z#cfwe?gFTzselg&41S2KeHkg$?Rh%OZ&I|cva@ z$$U|jEr5~-n?lc z7Xbw535ei3c*x^Z=f3{{X{-(?)Vqvi&YDL7ES|Vdc5iDYV}x8jUDiyR37Ng3I)K|q z)5$>6t59bMV)%L|l=)}Q&Q7P(+3AcqSUc1ZcoYEHewC2NPHj6%_RsF$KX&365wf_E zgDOCIA*o{$sstg*R}V4XVXyabZ)YUpjSLSvX_~f~&E|$*++-M=#cXCQX6tZ56>Ry70XNM95-({Q*>%k%4C%DpXk^t|rJ(U#N7@RFDf&EFBE8+z>@` zHio7dj$s;vKU|&6(cvqsQ`v*fVug3 z2<9+9KQ}iQ`$;8+Gjo8CJ>*4PgB5DbFD%?xeHV+nw;k^g)+qJc(rVQ^Xb@@1*jxn1tJvGd5TA>3D%oBH~25d{{X6xOBcu-?p&TF zqOp>nE1FNmZ%+^70DClrlp4n-$ zP=i51@#7G2PF(_vo0|JKJ+}S*>)Xzre5^FqyjV&uo!WF^w*UIJvgX(p(lPm|+y>xe zClF_Icq;~}`c8-m&^?Uf(d1-jfT??~4XdUkt_RrLpu#+Pqx59iCUWWI0kZ!S@#b#n z)&3XBfeZbYVq1P-Z}%WudqpmYc&TBm32*_d@Y*OO`dp2fqbRo0ixhl|-0kwZc6A8| zb}y`aBV?`FawoQt2!22ad1-@5kF4DixgNlJvBqXWTztNPcl0a;sUTMufv85=!H^!R z%+1BV1BuOC{9ET2ck-c-2Qt)M605@+rZVxlGYHH*LCX{}@wdqZ&}c+1{k-JQ`P`oe z#MCt)!Nz*V%sL>eQTBpJTd2v@h+O$)`Hw=IbABEW6RblbpF>e?bhpbtFVM;x2aqGr zJAN8+BWr&{`l9ID46@P$1$dnsxpL@rWN(jT02QncS$Vo$(UYTJLas~%*Asx$RpF@~ zWJ~W>GB;>I&MYB&eS6abumav|^)<*^TNIDXbU|Fp2gnu6A!O~h_K5_*1z2kYIno6Q zdgM%fj?3W04j*!5<`yK_Tahb=92%6t9GTezDecIW&r!(MgX3BU&;>2nJvq9OYwz;M zGXOvTaR~rn5(`6;-Q`BEM3_Py3-lr{%vD(i@XD`583Z_F=fMN51K5YW=qcFY z@F?;ks6hZ)_W-^Fxnp}7S$QB5z}BuXFxs`!%#3z*tu(j0y0-3p5bo^_kB;sOhr`{Y zqsx8a@b`O1j|(rmd%L=XSAD|o_5!0@W6k;1=5|-_=;-<0-gp!49mRR!_g9+V7n=C1 z(b4eg?+AbU?S1=3`)-H(y1V+$W2Ucbr5O!(b=~@~=@$N(zV2}E`O#6K={r8Uzk2o6 z%D(;F^KscodZ{0VFTf$%Ps5##o{{F4LzFWc@-wQnG+S)yencln&hud~Rl(4w* z@tK*KiF<+>U}mkEnVu1S21oc+2!hkd?DOID34xi3^Fnhz_E+S;3YZ3vL$Zs@F7JVi zJXe8sBXa5)nLO`3fQB)ncC0QrULG}{G|@CM}65eCV28K#lsxLg_Z>%OeXSU^00c9+&4LHx8a zd6mvr_8_BnonO;3Xhbp{a%qY~I^NK#JOHCxtKqQ3|M3~xeZZG zXNJV8o?!sF!+9Ypsxz2g!>WC2gT8pv+jXn&!RY9ouI|2ljmvwxyZavC{N8Z4HdVkL zMG8KIoN)vo$>TEU?bBEtRsy&eZ;s!anV6Yb*(!XWy0^T1Zz}e&JK6-$(S}s5cH|0k z5E%{Fr|;c{JPpfWYYWmC{P_w1?S}vT_wbPphg&+3#vkILQX3C3#S@UT{Qo*W($OGq zL^{6_s^c0n1x z%Mb8&hzNS22(NEU5A~y+89LLBMxjdakg@r-rX~oYEvl!5q(=xmfHS)aVQMUD(3y~t zeSlwW{n_3uI3R)m&UkXLh|ayla^x{0uC1}K9-Gr69q&E^X+HXKC@|BF1O^^!-1fq{ zS6Hw1xtIxd7)s29k-$BUIDKK=BBMhZ|K`o#h9uSrg#kzeZPa{bNEH*GHek}XbbQ+h zag}UscAu!!I^NXLwDQ$tETmOrq$Z3ygAhdrpwx_H@%AgTS86ajOi!{z^CzQL_1#yPFltBZ;DZEg4GiXH$ z2SraH7Xj!r4&Zdu5BJkX4;1o;kfwoo^RKLT-GJq;kLVb%=c2)2H-)gCePCyBC*?zu zFIbTyrswie1zasq%GHV#HScYN(}o~}580kVj_f+4C;-p;0{Gbr6)!7&%c0ONBnnUv z{K-%t!1@qJA!l@o0?@Cg3~r=pv%?Y?qMcBJ|6~+#6N6L$D@;D%;6too4nw8oZkaA8 z6M!N6G8h@ML=Y39>m5+Sx#N^&%44j==+{@P9C z)N&XCYmj1t97o;s;*qw0_{XLY>^frrHijZco0WkrNazF_O<&&Qrtzai0!Sl&$&or$o0Lvgn1*{bTIsieS$JQT) zgb%olXae9i8eBYpqKpSD3&r(N&qt6!BtX-OmeO*TL?G^Ke_A{=VFH|V1yrk~IEr;( zjf3TS*eYc3!nY^mW3_g@-6I}A5^n`fah(w5ZJ=!xN~gxW<<o zft{OCb10UKg{^=R-w0AnW$i|&M)64GFH$`HN>kRvz1Zuhq?oLx#~p``*aD7DykqI1 zccd?vZ>o}&4Z&f>GiRvVNjb8+EbcehrvZbHk_!NwT-6`6<#biqAmlFM%aada+^~7| z@5~s0zp(?Lt$AHZ%4vaw_Rr>Q?aU@!6fqa=nF26PUIZ{jAzL$0kMcr_E=TXfpYV{O zo|O*3Z&*8JK|b|B(uS6t1aQuT51F2k2;gbg=8zu(%a9j&w8IMIybCP%CU*Z3B=Ec| z5dhAZ{19+~{H`Wvsem;5nROoopVKuWiF6fcasznC%ybG{d^S!uwLk%+6;A&h?04=&EiKkBEiZ=Xq zs)nR(cjo8s>|Vf{IYNFg8O1qIc4biSM!E=>6hP8-!l6U?yF=s#xxbB&e)1Rp=RD@I z`Xy|;LdfO&WXrs;aSn4g$loOY`qjn``%ClctH5(1H^h7l0x)^D%!8*MpI=z`PQGrEBA2cnCH*JK>DiOp zO7AYt-rZJ4$fcA2h@3q+8{7EUPe}jSPi!Y=XJ;>$UB7U50m|L$gWmuNzx`%f07gQ{ z4Wm3X)Uoia`pb~l>?{Co*>&xXE#03ys7Ly5w;!OcJy>vY~$is|Mk0N+sm$! zZC4MF15XfjSB0gY*blsr7Qjn)#~%C3|Ai1z)=zfdSzO#r7H+Kc$Sn(mEX*Z5#FD!k zCFjY}D`!t1+h26-*zAQQ7W=P|#eU%@#I#K3^gQ4OX(ro_HeWeP1f{38 zKXK|bS=m-POU5q8Hl8>-wuy|b93@u%Tz&C>L4k3-!KiO)dGr57A?e6hQv)DxT)#X^ z$PX_((R}y7f$OJcua?b{A6|XphvdL%;SJ405N*wE8C+cXuRPjJ#-6xTO3Iqa+5dWX zVP%_Ob>rouo9>_5c9e`A_}*h1`=vzqI(w2_=obPIUXii; zv-i)=l8bH8+*g75g*#j37w`PH1w!uJSX}wq65EEkg*!Kdjk|Z>5nhrzvBS_JAvYAS zL&572<2Q~#oR3NhU_sdF?%qAtUwWUE-ambTkkU)!iBjR!iDM`G$!r!n0^)xUlJ(Fxs=R%tyVOnz8;RU{@#%lQ zqcI*pZ3B|nJMy!#G+hU^cs)eu1~M%tXlU6iG5ud8aYw$9&(F%zJy6ee4rz8Z0gzru zYTB-IV+|)7fP+R7eSTJ!t}{cu0TWW$jYzV$7pe4Z8RrE$StN2B^AiB;Ll%rk)@F#x zf3P4-ABgsVX#jN;LzkpK(WreHRB6~W3H^dJ83IlN44Th2N@ecRJo#o{}^(2+SAZ3~Qw}vlsKDG)CzX0qmHxKOt zs5K*d1`LEeDnCg9(|(U9`ewDYdI1k*L~Yt3^Z7$T_gEfL4xxkd&X8BFE=|Z z>h-S%>34tbN({jH)-_BG*3^8(QT%71fA_1ODW~&se?Y{BbqJY&ECQG|d}|5_qBUXQ zP(;5sSX^ArvL(+v`Q*;>o!kyA73ZM`Jll!vHAW$0`5w~57ufqQz;kJ10YuDH`OsUH zn3|fZ7XD~l27MHiaW{1|cJN4WuC=2Ta(eelFX-rM11i78rv@Mhl$TW3Q-f3q^&`p! zbu_vh-qZ@f-xSM$KWCUU7>xFg#umuw`+SD0X(IpzLjLpstmf(nz|iGrgKLAPRp41D z@v8j~v5IfBkGEdL8rLp2GV4tbfHx(LrFdO>0E6Wvv^Bs!?u5W)hrrX?q)7lA4|yH! zkob^*X+1K#{ffxhQ}RHkV^>-Lk!ojUgVl#stvkP~GjdNbrsO3 z4kZlH)Bq4*kYGx~GB`D)=!92NW1HGZ%C=pp0U*?#8UU8O`C3b?2XZ_HNORlO&QiP5 z=OTD}Q)&RiwmhvSH_RcEX`MkI^2-hgrG@VyS<7ef$WFwKyHWzc!$5ulV7-}VxZRK; z6F_%L039ar@LKdmk{9W;EQ6g4G8iB6W1RtXB?r)CmkKYM8i13-dK`x$8?DH|Fpc#F z&^MF(-!C0rk?4|bT0b#jaUg@XU1|c5x50!&n@2JR0M@7daJcGaWMYIYBNS_~i=AIc z2jHZ%FN1-%k%?*DJ>a~=`J}*;oLvz&j1-jiRp7`WWHJs}<82Dxhs&OM+P}!lQ8wmz z=i^iP3;;n$^N)uh&!q5-)F87+R1pB5GjNlpZ46DBgDe{?`J8rTP(I{oqt0)DJbRV_ zv}}-lcG$^QY97F0MsnHpi@+r0O)5MX8?sqT%*=VUUR<4VNH26XJ+kgy;2#e>J>ch8 z7?H%iK}i6p9kgmaH^}-VKZWC*3t5+e)(smryxMLO*o`afE|Jd|lMet0P!rl-7LWAU zD5?s{E*c?lwL{)GM6~D{+uPgSU-nA|)#ML(6a@e$JE8sbIORl=&@==iJY@OD#f~2? ztm%~spb>n?qbL9zR=}dfKEhunKZ|8vzjB;ZE-Y9ZiOy~nj0)Uf^#K-4a zJv$t*V1?Ha_^w^$NY_37hbEV*df9#vj}q9dBmnr9-Ug+?my>5N`8(8HZ zfkOV!Yckg*l55KJuEc*^-jPy6hOL8N1ZPv!nbM0ia1rPQu{}Mk*ItC_ILoTNW_N* z;I&dkkYoM|B=99ZJmMG{V&BGjOmGhoQ<-?_dtzhYCbP7}1S~=7@RV=Z>w`034pKdo zbw}}Fb{$}H0N^_ys#|O|qQ>Ot-~AJc*@z;@+SvhdsY|Ij57SE&cMxkL>=6iR%v6wC zveY~ZtE$Jw3~|qz15T{sB1-1N#3%bYt|>187bQNHNpf2BVia*lsyD*!Ru};L>77Vd%R1%DV8jO{EJv`$H~@v# zZjfzKFgMz6PwoN%hP471FenUQP;_j}N*{dGqm)dDLhM-Cq-0d8PYfW!T5A!rt5sP5 zLqJ{qQ%J^cgqSe0!qljIP>a`LRSU0Rj?%bxF{lNYVr-v7+y@l5P1KcJk^l0!Cy{I67CVOLJhpF-K(CfcbQk$}_eUZ}s`L_q@st8t9QC5iChpg=B zdGpPGx;6a!6Qg^#cJ=PT=!30YU3-AF=C`(XcXfwnM$dO|?dqNZy8q~pR+?~^@anVO zU0cJWu@~XCfQc(&FOY;p*J!>Eg}a z@apmL^zQ8Q=kWCD^7QWU^zQWW_V)Jj@$mWZ_4)bx`~Ca;`}+L+@$TrWywu>q+Tg_6 z)~nE>wbI$N)5_z;-oDuW{{HCI;@r8`+vwD_-LmG<;H9|JowCv8=H z>E-F{;qC3_;>g|8+2HW$=*!dHx!$$e-Qv99x#Y^;f7qs zfGer z;o{`n%iQ+#@RzF0`1bSq`T4QjuI}aL#n9aD<>>P8>hR_1?&9dUmALNZ?xefQ^6c`> zwavoez3lDd%iGzt(9o#E$GOtcrl!mF^zZfY_2uZ}(cRp@p}^_c;;@OZ{QCT^z}2#h zve4Gx>fGb`_4&}(-j$igyUNqHk+#Ly*1X-h#ofo)=F{-$@VuJ5+2Y={#Mb%v_5A$$ z*5=UF;N7>=#Hq8?=G5WI+19+&)9T>rw2rjS*5K8_*21L1wXMqN-RP*=ru_T)$hp(l z#n|A{;G4m=s>{0S?B$`?qMW0>*v#OirqQT)_(!#i_;N=Hkny&*bCfz(8amI z%CO4l=H<+?%(K7P&AQ36*toi?(9pKcx|zD@>*3pMQA7X$E^kRhK~#8N)ZAr{<4PKa z;rz(Q%nmbinwc4fnHdf3 zdJ-P?!9;!oNWcCSuqdBGPczm;20(T5WLbLF?;is&t0*Qq5Co9A1LWW#0Txx|JQrA# zq%0WBl;Op+EGx75^RrxF8%^y7vyUr)02Z5-Zv?Y*@1@tk;<+mNI5kl|cD6f<(o``? z0u+nrRH+Pp1z3&Wp8uT`2m}_b&5TtBC1nQQZY?er%Wz>TRUdCtO#ntrc&Nic;2+U^ zqs}*}YWwG(_{911YsZc$iwj_IN`aTG#69=1QiYrNtS^#K1s%PmL+EnSq}&8~kl?;{ z@7@|GF$I8WqNim)mj$didhh|L32Huq9;9iC6~G?gB|kRr{bX$nOv*9z5SD@fP*FSr zG}-)UJ_R=cbWhfWuKJq|hl`M7NV8r_fV^DL^tj4P*d}+F3tXRjN2tlqCO+Aj#E>505JGd&nRf!U2FICFrR zP0*o^n*Nps7s7|CLYlO&1rOa6mbm%k!aD*x*MTcVnR14CpL zoi1~dUkdm`;bahP(*Uv@m;O~UiVjsJKG*;`6EyZxw#_mEPIVyy0^#v-&hHPU57$CG zXAU$#Y!fuL_Cc>)fs2Be0;z16<2Zjfi6&Da4lv*unAY_|0Ype{xa7qWXhf59lEiVL zTbG}`aSMn35+7QCR3p;Z0vIXp>2|5tzwsz7@rxkz?bavOgc$0=52;OY$fN&j&Zf z8`A3;>lx*_SZtyKTJs522c_&B`ng>f^f52oP zbheA=FC`8?fT@7Rmdb-lx(EXD@B_?_fXUSYWq>dUsEMHmsFcBEYX?-qX%I;8kzofo zSp}QDEzrnvAR#B7m;kjps7N4D5bIA$09|Z=2Dbnbc`-HI03#;Y?0`_7tm#c13M8_jaM1ZgqBqrMS&ure+Ks$wzgaH;KrP=hz=n3>$h&{m zHa>ZI>19p()-RV{ZXdnUe23y5;Xa7O!%4KvSRhjpK|#;UvZ?}rtr|jI7F1E->vdV? z)x2wfU2W=wrXAO=zjH^se&^y*?cve!iyvLohzmr71PE=138szlL{L`^Sx=R&KC(j# zg+^ZIW!C^p+P6)1ahhGF!|2BhHlz- z-+k)HiZ*rR$Vbng(6lRuM=$()>WX#&WYiSsEQeu%Ty@l*@1My)AGNyg%+qmnS=X9g z(K>Xz@%ZkMC({{_Q|5WYh`^iCKtFlknKMtL+wSV>gBkfF1`IC!P=xCg9v-* z83kQx0-FpLIB_*&C{ z`pk<2Ym*uKQ!|xZxm-27zx*j#hMStr8)JA#&4Y~ZQmga11@sV~`0B)~pj0nl5RVMZ zC?;Cs=397ZJB?oQ#unNPp7OrH<=4G=F z3~J>0wfVb84^`+H~^X)VXK^lQ~SBhDYo0^Z5Q@-erjZlwP=wOyMOC z0BR>CmDKXFn$V!U$8HFo!NxMc`WZMqvHplT9EBksUync99muE$t?@<|E8uE*v%_4gT1(Sl*>yV^Tt404x@S07WURitXy2 zKOipz{V#(@n{Wsa4$3bUCE)|11oEm9kPiNpQeN>Xbmgyi;g&gZDIx zJpi$Cnd8CK0YDkiBrq9G83b@q{-QeS>yq>4 z)hL+OIQ{U~)D$T=hB37OSj-faD5uN`MjEW@W(X52kyZS%p9z2rC*@Qu{21UvJ~QJ2-az z`n8U>nvR<544dOM4R=xR^PanHf_490;bXK-mLLK>m?i)$X)}6wYl6zsLoc3fP^oVI ze}#3`(!BgB`_3DFI@!>9i}i2jy{rFfo4osd7t{KrCwJ~&ZzvuBLJI(h%*F~{7a~e= zc92bt;2S$v#hRt~>s&KpCA!mWLEKP}=g*ZMT*dRN0>ElyaY&@CS&K-8H^5o#Acp%L z0b-niIAk2w`g*jVQV+c7~Fn3jLfXMCk+lFIm z=~hu$6I~AV03<4y_s{V@Cka9aTxaE?iExU$|6iX(x?Mf>=*$4()u1tfsDctuc|vFu zcQ=}u+>v;(li ze0edK>l3W=W839B#fiay{s3H2c1zEm8_^j6i;+JMks zL)8IIQ@}!rDUo|QHI+i1WADMV?Y63dLG3J{I0I`nO}ny$RSbaHlykYZ$}$5*B4#WM z!0anv0j2q;t-DZJI$@SzfixvSi(bs`Qeg~fr*D;5FMtu+WB`Id#092AxuwFCYamjq zICiH=vsI$MB52gC1=8G;a%4>#jcKbXVH6Cc(CfEN7nFi6wYrM|mrc2fOtTiSAfKo- zr3E7-GaZYt&0BQ027aovo@(#ct_H107FyGZgxO>f=|of7V&wt2++5fo=+oU8sQ*OC z682~TAeAwGj1`RJGbc>q*}#SjLfK{?cKdsmFMViNh!{CBz3H}sCe&sJ^m%t!!JpNK zCbOJjaKOFc`yW2Nq%<#E8g)lNVc!vAL}T$Z!vtwg4jL_D0IU*GvnHi5rS7YuzUB+^ zcxwc}#W1?q66VQlW&oJNgmhe4HR(+sHOm9w`v+kd)i>nh&W6xAe}9lYdG4c$2LKE4 zroY${=3S;_E(?6}1@@g%tK6LPMF7#DFbU7codIwNKql*K8Cc>0NO79}p)>#%Rtko4 zS>UMx0)bbsF23C=mvuK2Jxao|f7$0lU(1mw0LsK19bAb#M(2KXAHDTufZ)OkeGUUb zM7_*k7-9fe&3)Zs0Lodx_`BXbw|#@W9&qRnz`56r-BHd~xm0mrNoJ|b2zgw{x0agQ6GJpGuCq!gy~kIzGL2*ON__srp@ zAvPDG6U_j2lPCu*!rWqp4+jkU`}?N>J^vY40cK!61Hb_?b52l*=+%#sizL;Tub=I$ z*mp!1Bmr2CJP89l3eSUoJ{g4AGdl6^DE12oKUw3eYBj}5K=HDOi<(L&)Y>Fv6QL&nN?}SLY)6C9I7|ei+mYz!6R*V%5dd%$oH&Xd z#txZVungp0QLVa zlX_d2uobzHG9KTh9rk6XW!A+;&R9;72l%Rh6Rv#u>LUd2w>3=Gd@4CKI{5Cp&uKev zlrF>=e0LD@4L)bje1qr~J-{B2W#~gcXK0ijcz&%+IGst(=#ipwV}f5Lq*7~Mfqa$# zK-W1?)4)Vnr<-NA-Eyduxec8{0LbjSw#YzW5qeNVDa9^e4uYs2akigFXWC!>-Hyy@ zw~S-j<5oAR+AtviAuc*xA&US%KZKnZZcA7LfvW0}z&W?GT2nu4yuN{c`8-{b^vl*c z)yCe104#FPDa!-MOK2+3u)*2^F9f7iA>kQw(n%xg-Q=Bajckrq)8`h0D)>J+XOD)_7rd|_R^be4m${fa5TEzWBN>i zRo3^tnBxsrNF!QXUEL#7YY#=~bnRc|@^#VGX72vXTB`26a>kLf1ot00|2fy zYKip_g}A9MN!DC!=!jv{w{jIjL_kH2LBOrJn}Fj6VwEqNaG@{$hLg|=0ESbPm*)Vp zyfQ4$6_HCht5xSn&YYY*KddUr>MtA$n;Ef!AlD;B=+IS`6arwhDi{N~2&^KpWMj2n zqkA^*RS?CR0@X#j6^66Wxa@iHh%pTS@G`y6JI#dyWXXEW#AyjuC%(tb$ttJnjd-i_ zh2>awCE@uA%~)6aGk`RX$fVEAHtN(@_<-|87=Oqq~}6tzm2!U9`w1(Y5FVz!Ie?$uaqv#L%=2_sFx%~G-g#a+8k0sL7 z56Iy+w{3ScB#(LAYBTCJRF9N-r{7%x*zj>p#Ww)J$HfcNKZL(&Eg%RJ6;`A%{$Ob; zG-cDW9kp9UqXO|#)ZJei0My$s9^b5lL7&eD@rC$8WMUWSc(QkoB+&=m`^(lnB>;?! zx%f4T8BI;u6>_<{H8dYmrV9cG$#98mRCWp`f?JzWU}z4upf~OZA#E{-Y}`aPZg`R` zkkHtF-Z2(9S0`_%1k^jy@egfk|CO>IC)tNT{QPL#|2T3u@Z`1RK=AWe^1ErX-e{)5pK2t#r`xu4vJU%=@Z z7!M|&R3;4z;W>1N9)cZZ6)=Uft|~S}qSSk9EP(Y%Wm0-H=2Q2n1;7w~d+%ouo0*9o z()k4Uj_6Rj|8YDH=tK9pVTM3X7nqNugZ1mI0r?aiZZ#CDcI+0vZK6(>lzO7QWB?wI z1ER-a*hejF*3WEBfB|6Z<@y;wpRI}YGtAJB6nJc-T09H7*am;1fZg*gDWqYXQBRuO=&vsnZG+KOGfd#eb?pGOo$(HcQ( z`X6R7R8=Yf{|VvK+Y7L8x^f)QA12c%`SS=mqd)f|N*x;=T7l)EjiJ7?-oCT%oc;5+{Q#GZX(a&O z$WDQeKP+x@GEZ&3gHL<@NRzt75M?!5PrU=+_vH_*QlW#4t%p8ov4fOPtgSf7siSX- zEV9LNq`+`HrkTD}rv;E4|1;Vai~(8*@F}E-@U;+`6B(8UfX3x}Txn(J-~LmP(04g> z2H*ouyp{++3N~Nc+8p3II&m%hQYlJE>9i~W6uG#>Z69m5T`0Nr9ig$%hTo{EWvQlP zvZ1E4vv>ebfgub4yumLs(^E`tL0OYi6oG8J3z43-OL)-5OqMlvv*Iav_Pd2K5U~jsd55Nw*coCo3j~U{-L1YJuJcB&t6;z4hhp7i^rc2ksYYHw=I0U!jC?X4KV z`1ZoRaCjvT)l^8x?9x(@Ul{)3k_&gC`GVoBac2KWUI(~&UuVBlkMi1&>OcLRhMt?) zv>lPHTWOC!yS-JY2LI%&|LmzheFtDgVmmo}iSvB?nv6$?pBcY;V|58%Tnu?#*=CIP z;~g^ok!pv|RaIF1XGQOK?Yf9c-&_m;TnLn$l3^`DvZzdm%vfQuSeetc9V>~BfyUKEpeS=V|Ask1=z=W zhyYNY1CokRrqblWn{WN`e<+oH6SXb+S5pu-N-tV1%er;RhnQlNj1(;gl-n*?zK4ts zwo~;LDRt=9834@Y!dkl2^$%csrjW7#OmhHOj7B8{66Ok_vA8$26T%q;qyk1=Is<@m zl-wRw0I*76)ho|kqSq`NEH?>L>Z!L*QmIU8=c_NPY*yD+u`DA3QmYDm>DNStpFANZ zF{d*iSbv=ikt~0TD+-*t)|5PD*WfMvr31h&=k}DgnIT0rl!_1>H=7b{xa7EbC$+~0 zuSAv^p=V{`MO#+3O2#Ff1R9b9pqyc9QV9U;1d*F`V3yItg=X&qG# zwr-tv+iuf5B>`%n7oiWuNTx2oWHD=rG-WEbC|NgO(xGE9^vGlsQ6eolWZ0!hiK~W% z$!2LSWd*6^N=wKRc7Z}0m>Y>u5R@t^!>cC+=$V-_ha3($!(siP|6yh{GoUcvob#Xm zHs8M+KsN{_#AN}1RjWVX6H3Sm$AGa{xfMt1Nh%6SH&4Een6?oZ?ynte?9Tl)y6z~# zvSJZ)8unUS2<7nbpg}qiG`tFkBL_wnfv>hqR0vnilqXml22P^|gq;RB`X=fi3*fgHAA$;aSn);xH-eku$~)*b5q_Sg!L&H0WG}_LC=PEGBODa z+j}8(u(1Q{bIfXFBP-DFUwoREbTQ_&IvE&N_^@5W*S*^^@o5FW!JpY(EWHlOgbGg2 z+rQ$!y1rH5rC+-yHwN60-2hi)JG%^T?N2e_T@62?(f-WpY!uSjxhNza2y=k^u(<2> z7+X*_R0gZ;;(W$uQy~BR4}U;XH>Iox@ci;`;nH3e46P$DR}1iLe=Y)h%t0-nCl04Q zwXnXT1=wFln{^tBkzNu6jAO-_3@k_;ikYR&zOBIYqWl=Jqu94f0I-{piEsHs!dykF zrau>^8kb?6azYioAKQT#O>a(W0krG%b`O02I7CRIQAL#3*lV z7fF|XGSh9uNO1tfLyMU}Xwf8#fJvy4K3eV^AZQ(X)xX7~`_0FnU4A!@EZJh|E{T9M z@`POlA;=_BkOHV>Cb|8Y2&58^0AAaOfExoKrLnPEhB`+}^IDBv=kRa2ror4PfdGOb z{&N`sLLwWdOyR+*In~Ac1WlJ;y3=syXD?nZhY{7Cb#uy?sbk*~wT zFap^W{GBs7vX;_;Q*uii9}wZEbchFF(YF+m839}TZAV!aO2oHVUA7wVzjV^SYs>MX z<+qyY9a05=ch=={0YDTWt>gDjfCvJB-GYlhLoK{@fB^73!OlZOdJ6`C3~1l(G)FD> zUAaZTZ0DZp1R)eq5<*Y+JHB|_?hE`J@uHb=9Sn$#-SR%EzzEcQ z=TCV?whlA~&h<;n=7!6k0s!8Fm6Q&qXn-o${t7@W&2DuY+!a9H^ejX`>rIDw0Hn9D zot`y>8R7(fzUj;ovI|4zQnBGJJFc7wk=`82LcYVUel^ZV%5R7kf^kg5+0xuy1^`-y zVZ0luM0kBto1O|M_N&mjWu_|a)uvw4y0|MqIYCRGp}zzH0O>7lgjAL9vQd-&hL!{*d{ua*=3!AEBzHSg#Hp)lWxJPb>pp`!Y9#2bV>Xd zx#a^Xy?&u>mnm2fcYRx#C0-*0*vYV^E37Xuee&B6FTI{yK&gE=TC#Jn!n(jP${0OfEd(ILBIiN?6CAn)G9 z02G%RG719!TR`h|jyMNfn1xr39LI{F=5g0FZxTzY7#pw2Qin9uUAad9IMAjcGFz3O zEQknyxP(8G-0=cL&{ja;9$teqWh`991Lh|CG^Hv^N>&3r2)_NjOATBzE)|5V7yq>Z zrC0i1ynOE}27nF&GgSr4a|g4ijd=>Pw8y(V>zPaJdpt20#CANJPav)J%uT{->|jR- zBVT!kA^<2aT&41c)5H*{Xb=SEIilQ$eNt+yBJSb!iw$qTee0JWqbI-e-bejS7=ZMw z%Trqg+DS;nAfa9NK#XOK<%C{=N9&OCfHnb)&b68A3)L+nfyEC6gqfZpQ((6Uz(vj9;BK|@iZ&w4b9j$939mM<#tiY5U`oB6-} zFC3Bu?MDY^#>J11(*Gp^fFM8>>}0(u2Er>5?Q+b!n)N=Z-Atvl7y!^sc@O9)lSpeh z0NTh9vM2#S+`T}FU?Dbz)C+EN%-8L{C!PdWF%d{l$+Ys}C$Juc$O=U0EQ2_A~-ZGa~15ZVa{HhX%f%n;` z>wj312*gJ9^vF0ytO?|H!9o%z&K<(~_fLR@W6}nIq)W)Be_$qgsUhVNKWWZzPqXNgQCYPLIBhhCpAx7=Ta4$EbhnD6`3* zp&0#6Q(w_c80qY69aJ#_2*YO4^r3m4@Q$ezOx65{b%jd6aF!ZmKBUT%(V|}-oV!=- zWE+9lDl-M-skP(J%Ooa5O*P-@qAoNQ%mL;%6=y#&n@UgyOQbJ1FNW3V0IcPn`iokH zq&<#q4uv`Vw4A^@CgPV8V@6(E^uwx|U_OgSBr4gs75R4+I+8>qXK?+@6Y zB33p^?RWUFOe0~y$+pwE@E&4TcaAv03r>!A{V(|ws9s{+;jRn-GHE4szqWvU-Gs8C zdGgM~oH=Z3NB?0)fm1_Dngf&R^u{K`hlv48G$typ#Nwz30Fp=bNdUmbvFe4GSx0{D z)12|ot^egH0b~9=aV$W@!$3lJ5R1C94`ONdK&K6RvpP?ZoLD|M2*XHa0FWVIl1u@m z1cBn0CP!X>U-0vL&hwUvS}FiH_x!9(ZG?pbnqhO|8&l+jr?&DDfM2t8A+SqV0kzeH zN9eYGE;4eyUHJ8J<&5cs|$*1Xhc$knl$3LPpQ)+>d_z z5lwPYuM#VeQdc_nlMz1_V-mevaTdD(wpdf23*{l1D4c%O}$&)^upv{iZK zUIDJpvUNzpZ>=-ZWl)i<_Rfb*@@Q!)HC<6HnJDM~{1E6eatPe83H>HWwm>l$A{Ep7 zKk2byg<*V70QgCKjL_`MhD-P!^0b@mo(gy35*_Cd$pT|~{<+%VL?!)v{ zdHGlG8J=WV^P;1nkZim_3k>O5_93HjShktciyu7v{r;PL2fQ2tsAPJCx>*STq?!s( z|C;9r04Y2TOjFl0OZN+S*$jGX0S(izaj21{;BS=SO}N_j?)#`+7wcycqtd}=izX@q z09O&6IC0{bqV`h{Z8b)KW%K)ySaS_n!~=kJmzx{8lM}M_<~_(5xQdvhQ0FTCJ_vdq zcvb<4Q&Xt}bNuOX)UEruOEsPa_h%^IcYkENsTY=d8><4X-4rGMMs}1}42`nNP;f9W zG_}3(!V7= z?ff32@Xl5EB76oQ!K7nejsK7$aR1GfQ~%W^V&6mVA!TT6Hi)>WEc#K3>K?Q74jN=C zt1gPoCy3H9Z7{g9|0sRv0KoC}n&1Aj_3xYi;>iC{M7qa~`8}J4sL`7fR=veAK=o1v zi@_ilKFKFI?4EG~k_XJAbnR^PFal7`4*~n*knP&zu9Dp21C)NyP;W5m4SI`Sfld^g z0@!wdRLZriO~+i!X7JDfK=Q0L-?y_YJks(A3m-0S)wA>8I5DK}Y~_r^VVRZSd`F%u z^(LIjKG;jZRQRox#Nh*g*uKjcc&iB3cY@U>Yf-QkWDY6cM+>jW&P#X4}0(2mnbk*?qice7yt_*&t8HG=Q3}5L$JE)gq}DFxGSQ6_#;o zOcGRocnAR?E_WACZgjpxk(5DDiu!@Gr0?b^b#_dz&)Lk@&dyFY&Dw!z?_2bGaR`Z3 zCWjILOmO%3d;SoMKzs47R3s#hmOx5qzGQ4{q^?sIYP6tnO+QXP+eC>){hD6EZqZoEYjS39f#oL~XCkLwYN)(W+V zW1|!7hQdt>^u#J8F@SfuSyPyB9Xr$j+|S@z^{)ReBOEx;sYefp!NGg#dMlL=098Lx z$nLEPpat!JJG3+o83>h&fxh8LS@w^`!%}*^}KXd?)@Gz3h zJSoU&w@GXP88SMfUOGxX7J$;Du64ww{QOV=Q2lbXI9H4%@T>Va&OwCdQsQ-P9P$Nqd3kgPBPc5P>Yy-hN>%6ge~=mLbg!~z(luR oXxB2@Nl5{?-fbnpMK(V4e^bx|udh($ literal 0 HcmV?d00001 diff --git a/test_rendering/spec/ol/reproj/expected/4326-to-3857.png b/test_rendering/spec/ol/reproj/expected/4326-to-3857.png new file mode 100644 index 0000000000000000000000000000000000000000..8374eb0028c3457858b69c77564a11a7f0d0b9a6 GIT binary patch literal 4315 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 bR^opHKd+Bi1PB{m00000NkvXXu0mjf(N`DR literal 0 HcmV?d00001 diff --git a/test_rendering/spec/ol/reproj/expected/osm4326.png b/test_rendering/spec/ol/reproj/expected/osm4326.png new file mode 100644 index 0000000000000000000000000000000000000000..af1a03b9be543cbbfe723b91b609471a031d0750 GIT binary patch literal 4217 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 P00000NkvXXu0mjfiY?Tv literal 0 HcmV?d00001 diff --git a/test_rendering/spec/ol/reproj/expected/osm5070.png b/test_rendering/spec/ol/reproj/expected/osm5070.png new file mode 100644 index 0000000000000000000000000000000000000000..2de3fa8a3e110f314ce78487b47c933619b32b76 GIT binary patch literal 3528 zcmV;(4L9vC_MS%X9Es6p`KLl}$253(d?N{QO!C+cG!`ooke#c$BjxKnC4Aeag!P495T< z%~QH*pTG?8O;-fyB>S|w8S5K4{Z`7%tj{>pXfm!|fxz@BTSc_ir2UL!m;wra#zCi$ zg*ljD`No+NK!`gjaW5uV@G&gKQDiz}kPW%L*{9RC=#Mid3NC%!<@L)h=Yv^HWs;6F zqDF#UG09VnH{lTX62iS0!!~fr&Y2THe$a3?!Y z76M3cY{V@}R*8~TK*zAQwn@H_v*gW-TfiYpII{u>qpc9rH*stB!zcfzyFHRj^Tve| zfp=J$-KasTGv?fB3}ujxG6q{C*6W+RT73oAaF59XXPEBo0l}P)T{7A3M0~R{;IH4T zVVeeza%*^>1$Ty`Rp5Z$%8;$vF4KiNb-Vgv37|FT1hTQlD#7orcX(^LOton7C>qTd zy&Jq;zQ!X+O>uv`$}8Ryp6R|&79ivygk*fTG34z_Wsahibl48}xK_GyD5V;%!U{}Q zMr+iZDpjle;t7x@Irr`iuxl2}a{))O9&TYNOZ@+U-u)47&6ZcDGqI3&D{s7b0&KQI zhMj~d!=$zx9GMn(;|96b%zHBc~Yx1HCBYFJ39p?8a=iB1*o=TNlgd9CQN824!%FX53~kZgI7E8Q@qq z@s!7uTYtU+^hPNg&5-R@#NxDzZJNwaxTKk;Ah}%-lOBvS%7OK0k%Z|Qe_jKSvEa@g z3W8S2Jm@LJ3a$@si5nagMQU~TmJZASTUjWjI7P%n?`)3f3=--kn^zYK;=fP&eZy9Q z0^A83M5vrb(iZb}3>$zf&ZUqlEch6X5&osN8SgJV3GI(>j#7PjgBC?StIOm}heBJ++tA)#0h6O+} zNYRqCAp(TD7V;h9&j0sSF+yG4G8=NTVAwU3G|4 zEnmNXtw!LQ04FAH9s&wXPw{c*Q{Jz=cc?N{s3Y7kXFc4OW~pvh4+TN_$8jDq&O$UA zg~71vFi~_V`W8nq3^T$wLvr<4V_8siO|C2!*&imPSb^N0$Tl; z-FAZGz*N=2Gz=CdToJ{!4W1L?3SlE*C0*tG^a2Gd;DGM^0hQ(Ak#4XSZb~LNR)BBU z26RUmugrMN)La0fIOp5-ftY=@U@=i~m?+sYiG7Yp+1uRj&~(~V+#>ySh-Z4ZD=u?a zrvSvQW1Yy98DKAJl5@mBB*}8_Z;uI+oJ(_o6edT}?8f32yZbTI6^Bdn0hXzFUgU93 zKOE9BI#{MDQ{+asMdZW`@&TUVQnRb{(*ZfT)B+ow@t+H@wl`*fm`ai2ImQq6>y2K_ zoy{SwUcy}6J$Q+0JnPv(I>^Y=96PY+_WF!VV`kh*Ol3-Ta}D><_-t)2dMJSSQg4)T z{Xw5Mm&+7=lP5$hVzU`a`kSga&t8OFGunty7Bu^9YKvt`L6KvSa{NF5Nm(~m2lC{n zD$dh1I8yPI1B=Bek7rHNPZ{q-wB0s~=jJgDlOvD}EEmsn46;Mm>%^$$-j@F8 zdWV^+%cUio&2UEw1j(l-#5l^N?QL{p_V!~yF;%vis@P1F9emdWkXb;KLt`G~6 z52}=U_;BP)Wd(>-=JnaJ)*=|Qtxc&0oDFDY19& zUMuGHZREL*MdBU+nYn)yHb&+<+x@i(FvWji-ko>M%gUN zZ>vK>hs(U+8klB}p{(l0|zMV=05b z`RN2l5#=!(;WiU?P12^J431-vj_^$9xd?EuAdUA^(lC>3aO9!nzx~gb`i%=^igqBi zcxyQ1-gb{<>=1Z1lVyj=sw2VCwhTF1Y{X42_)9W^KI`ZlhFc?X(ZYg%gbRG~C%v#3 zQLsG}O3c_ZTsck6GMW9+8)vlp2^*~u#bVB-`2x$6RcS(A1hOb&v=iZ09h{1FSb%^0 z?Kk@BMT^B+N&NQa;5Li?9P{ohkFpc(aig(?-8;upy};tM_ri#fCt7w0cF8;}!2Jj7 zy4j7nA}ukpKA!ca@RcWv#z&Bmb7v5*T3ckfzKxbMGBz2~UyAi>B0Dxh0B4d~{R4f2|JiqeWP6gxuR1b7{_J zY1)6T<~~>*p=^`lIhV&lef;?yeRZjTWg0vM5)=OGRs$QGshZ33Y#vc&Trg zOXN3f#gibbv6DJ*=q7z>lP~z^_>su$ZmS)Ml}uM1v69C@$AArwq1WjhOK*j{G7zab z6@aHot-JfNtglT2ezI7?P@vIRhI({!Bu_t&G)~EX{haW`nbuI+w zPGeReO;~p*3sV~11j|xP1Qz}{C$bFoqExC;-!loLRCa*1qGTS~LQAE3nie1@tMLv+ zqaY{gr;Nml-axj+C{wXK=gWs4$0-Hhkn8jQC=;=E_anN)n8nFJM*mewyP$60Th(#} zc0zQLu1t+25T5cmjEuX+IsUZsSkZqNrL1g^87G>OXUgq?x_Ns=FHHKJ6q(MrJ9@x* z?;M`#o?_FJ5D!v4~O6L2E6)p;fWUGB`cyIl*WrBy2?3WryN9 zpQj8pr?Va(q(iyKy;8WyNi*Jw<@D7o7#Nn~Y58C8ls@><04TTs00007A&c89;JyY}ss%|&WSq_ta?#-p*vm_aaspE61Q)F96Z0?Y&n5ZIXwmaGv)o1(^A zNvhdw_PX4yssa}Xj44wSQX<(R^bvd^c+t0hblF-8R4eWEO2S{d_=b zGU{FpSGc@OQ|LUPm?3;+VbVNoNE zQ<=_9K4`dksf6cBrpe|` z!pkQkZe12n^@kf{l2mj1avAZjzgaf_^w}Ky@n~bj&ScE(OH~5tGcB@Q^ZR=p zZZ{$-=L)fZCt_MWd_H9R@raK; ztus^d-bldJ^%2GhZY~vZqzyh-i{f>8A+n>9G4Fliv*rzmxBqgGI&I{Riq+I{N!SpX`n`iA&MkbHV%|Rmn-L^ zJ*GWLHBXxpwt6W5f#=XTquGd?7$>C=I@~N>C6K|v1$dEcQl`qE*J3U=BI`FNjZQ!~ zAY7QFivPa5L!%aQ<6=Z1aQNy`pJpe<4Kfzz3oOl4krF~rl9?QBl+9x{(=EDLpY!2a z&IM=we*rpK4^Oz1+#>&ZXPbZcbdHneWh>^ZZ+pl<+Q{uANMel&1QM97`uzM_g-ph* z3?K57(uYJ&Vc+1VG5mS(4HF&nS>>lVLL#s~KNeuDCgdh3aw7hBrN!sB&T`Up`zfod zeLneF9p94-COKc;>G03Ln#UOIuasB}X5VB=8%?59I(Zii%=&ejNehF)lO83fNX0Gf z6(H3q12sYjNvcv7{8^-soFso*?NRP3Zv3(ifY$K)`#UT(A}%b1fAecDGa0c?P)1t; zKrrjq_#w(DD}pQBJuYA$*v>it*8FF@DknvpDw^Gt)fXdfIWFgK*Vu)%^%0$6&d)BF ztu^d-)19o3K?B$eE69*g1_&VzUJGoc+jzpWW;5^4T0ePPa&0i$;5x#-j5da5CuKOv zsg*qDt3F>p?2!v77hK9?&B9WI8#<)9W^H3+pS#wG@Laj)&?7Zrq{jHt#}y7mr+_1z zH!Fq`ozhC%6r7NP46OgZ4c$@3@{=J+rtuwtl!7ET_^zNFISj@*nSzcDKiw*$4xzpzFZ`Fu`J%_S+i6bNxr)+)ML0XaC6k7~EjbBjnDk!aNus8@i!<2f- z=Z7@A34i*g$MvP6Evxg~Af&*NSfmuzSB(K7{`&W{ouhL_;cN)NZ|}7E^{2D!Vtx8~ zylxL&^C}3eMR62})y-v-Yem_OsCs2yB{w<7{58<5txxEVbAEZPhU-Xc)^V!&=J|-l zd7t@8aHyBddWv{ErhKIc!0*24^5KOd^|EJ6@sXZj&{SP}aE==By9WdrR;!L5&2MZ+tTD_F#t=Qh|jSEFWUve1ov1asQO!ayRfc5Q|?=~h} zYJ`|_2M33;TcqrkI4;D0eSO#5JU2_h_4m1bWmT65D@2^9%=lHdx+!ZLW9mhh8%re{ zC9jo@W!)(!2uoDkxRJ8R~n#n<2Bi+Ga@VS{n3Y|rqs z>lHjl@-}3H9Gx4eYWn#QWwg!MJ}O@0q!E8uerT>XqJ6u*+vyG^Ct?yi-2ZOC=O5R3 zTU4rKJNNj-%x$KDc(8HT%z85voN!NC*~7-Vrw{NYt!dSBeLzIqhP!dThlKs6#!`M zTG+1mQ?h{dyXN~kc{k#swC2wG1|lx=$(0I@6dV;Q)<|E1FaY*5r;S)ydt!E*9Xx(H z;c08k)kcvEb0J5K(O{He(vAsd0-QiHZEP*j?8ZE8#x&|7i}NAJg+2UMNP05bf5cIcO&LXmdg@=~AAb*lvm~n}u_z z1PE;qj=hz-UOj8R;k|TCh!$_e}sp+RJu_BmsWKn}PrbTxmTnRH8_BGNMMvNn@wOLhQd{ z@>TynH;dOOy3vVQ18`zJ0%Oen7BEo>iH<3|WgHCOX|hpQ}x4ZGcOP~z0)?# z1sLUH9>>p__v$S8bBF3R#O=(M$I=z2SAema@MQAhdke549JbkF6sT0$nZc))h{j0L#N~ad8Qp zfQCO$c!&)Utql1g0n6zSwixFPYIYt*~^~|&T z&ybG8)n8mesT8ek5JF0c5H8Y@s60hVAB4bhU0yzFQl2dnRekHXN+)Qgeq=ql&RH3) z@p1XaVVWv-csg2-DJ@3GKpflLLVWQrUzpqfcpHHr9>?~umAMM>FeXbfj4@UKqYPP^ zQC+Aq?vAWrLJIpo0M#>9;!zi+a(>9&{xVmhi`M)P-6$OyRF>ma9Hc8b4*m~dpNJ`d SX4$V^N~f-(6if9gOINCsxYFq7DEELpa-T2i-q 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 Date: Wed, 26 Aug 2015 14:57:21 +0200 Subject: [PATCH 52/80] Add rendering tests for ol.reproj.Image --- .../ol/reproj/expected/image-3857-to-4326.png | Bin 0 -> 4245 bytes .../expected/image-dateline-merc-180.png | Bin 0 -> 4339 bytes .../reproj/expected/image-dateline-pole.png | Bin 0 -> 5686 bytes test_rendering/spec/ol/reproj/image.test.js | 91 ++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 test_rendering/spec/ol/reproj/expected/image-3857-to-4326.png create mode 100644 test_rendering/spec/ol/reproj/expected/image-dateline-merc-180.png create mode 100644 test_rendering/spec/ol/reproj/expected/image-dateline-pole.png create mode 100644 test_rendering/spec/ol/reproj/image.test.js diff --git a/test_rendering/spec/ol/reproj/expected/image-3857-to-4326.png b/test_rendering/spec/ol/reproj/expected/image-3857-to-4326.png new file mode 100644 index 0000000000000000000000000000000000000000..a32f5c026495e82f72dc3a9db20d8093d8688e7e GIT binary patch literal 4245 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 literal 0 HcmV?d00001 diff --git a/test_rendering/spec/ol/reproj/expected/image-dateline-merc-180.png b/test_rendering/spec/ol/reproj/expected/image-dateline-merc-180.png new file mode 100644 index 0000000000000000000000000000000000000000..b99cee834387bb607dfa4e51fa5f876a7dcb9f8a GIT binary patch literal 4339 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 literal 0 HcmV?d00001 diff --git a/test_rendering/spec/ol/reproj/expected/image-dateline-pole.png b/test_rendering/spec/ol/reproj/expected/image-dateline-pole.png new file mode 100644 index 0000000000000000000000000000000000000000..ee93205125c0b890820bb491466bcff569807c0d GIT binary patch literal 5686 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 Date: Wed, 2 Sep 2015 15:38:09 +0200 Subject: [PATCH 53/80] Reprojection code refactoring and cleaning --- src/ol/reproj/image.js | 25 ++----- src/ol/reproj/reproj.js | 129 +++++++++++++++++++-------------- src/ol/reproj/tile.js | 43 ++++------- src/ol/reproj/triangulation.js | 32 ++++---- 4 files changed, 116 insertions(+), 113 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index 4a81022e77..c7e583951d 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -5,7 +5,6 @@ goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('ol.ImageBase'); goog.require('ol.ImageState'); -goog.require('ol.dom'); goog.require('ol.extent'); goog.require('ol.proj'); goog.require('ol.reproj'); @@ -70,9 +69,6 @@ ol.reproj.Image = function(sourceProj, targetProj, */ this.srcImage_ = getImageFunction(srcExtent, sourceResolution, pixelRatio); - var width = ol.extent.getWidth(targetExtent) / targetResolution; - var height = ol.extent.getHeight(targetExtent) / targetResolution; - /** * @private * @type {number} @@ -80,21 +76,11 @@ ol.reproj.Image = function(sourceProj, targetProj, this.srcPixelRatio_ = !goog.isNull(this.srcImage_) ? this.srcImage_.getPixelRatio() : 1; - /** - * @private - * @type {CanvasRenderingContext2D} - */ - this.context_ = ol.dom.createCanvasContext2D( - Math.round(this.srcPixelRatio_ * width), - Math.round(this.srcPixelRatio_ * height)); - this.context_.imageSmoothingEnabled = true; - this.context_.scale(this.srcPixelRatio_, this.srcPixelRatio_); - /** * @private * @type {HTMLCanvasElement} */ - this.canvas_ = this.context_.canvas; + this.canvas_ = null; /** * @private @@ -142,13 +128,16 @@ ol.reproj.Image.prototype.getImage = function(opt_context) { ol.reproj.Image.prototype.reproject_ = function() { var srcState = this.srcImage_.getState(); if (srcState == ol.ImageState.LOADED) { - // render the reprojected content - ol.reproj.renderTriangles(this.context_, + var width = ol.extent.getWidth(this.targetExtent_) / this.targetResolution_; + var height = + ol.extent.getHeight(this.targetExtent_) / this.targetResolution_; + + this.canvas_ = ol.reproj.render(width, height, this.srcPixelRatio_, this.srcImage_.getResolution(), this.maxSourceExtent_, this.targetResolution_, this.targetExtent_, this.triangulation_, [{ extent: this.srcImage_.getExtent(), image: this.srcImage_.getImage() - }], this.srcPixelRatio_); + }]); } this.state = srcState; this.changed(); diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index db42c15f68..0b1af19ee2 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -10,6 +10,20 @@ goog.require('ol.math'); goog.require('ol.proj'); +/** + * We need to employ more sophisticated solution + * if the web browser antialiases clipping edges on canvas. + * + * Currently only Chrome does not antialias the edges, but this is probably + * going to be "fixed" in the future: http://crbug.com/424291 + * + * @type {boolean} + * @private + */ +ol.reproj.browserAntialiasesClip_ = !goog.labs.userAgent.browser.isChrome() || + goog.labs.userAgent.platform.isIos(); + + /** * Calculates ideal resolution to use from the source in order to achieve * pixel mapping as close as possible to 1:1 during reprojection. @@ -27,16 +41,20 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, var sourceCenter = ol.proj.transform(targetCenter, targetProj, sourceProj); - var targetMPU = targetProj.getMetersPerUnit(); - var sourceMPU = sourceProj.getMetersPerUnit(); - // calculate the ideal resolution of the source data var sourceResolution = targetProj.getPointResolution(targetResolution, targetCenter); - if (goog.isDef(targetMPU)) sourceResolution *= targetMPU; - if (goog.isDef(sourceMPU)) sourceResolution /= sourceMPU; - // based on the projection properties, the point resolution at the specified + var targetMPU = targetProj.getMetersPerUnit(); + if (goog.isDef(targetMPU)) { + sourceResolution *= targetMPU; + } + var sourceMPU = sourceProj.getMetersPerUnit(); + if (goog.isDef(sourceMPU)) { + sourceResolution /= sourceMPU; + } + + // Based on the projection properties, the point resolution at the specified // coordinates may be slightly different. We need to reverse-compensate this // in order to achieve optimal results. @@ -53,34 +71,27 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, /** - * Type of solution used to solve the wrapX issue. - * @enum {number} + * Enlarge the clipping triangle point by 1 pixel to ensure the edges overlap + * in order to mask gaps caused by antialiasing. + * @param {number} centroidX Centroid of the triangle. + * @param {number} centroidY Centroid of the triangle. + * @param {number} u + * @param {number} v + * @return {ol.Coordinate} * @private */ -ol.reproj.WrapXRendering_ = { - NONE: 0, - STITCH_SHIFT: 1, - STITCH_EXTENDED: 2 +ol.reproj.enlargeClipPoint_ = function(centroidX, centroidY, u, v) { + var dX = u - centroidX, dY = v - centroidY; + var distance = Math.sqrt(dX * dX + dY * dY); + return [Math.round(u + dX / distance), Math.round(v + dY / distance)]; }; -/** - * We need to employ more sophisticated solution - * if the web browser antialiases clipping edges on canvas. - * - * Currently only Chrome does not antialias the edges, but this is probably - * going to be "fixed" in the future: http://crbug.com/424291 - * - * @type {boolean} - * @private - */ -ol.reproj.browserAntialiasesClip_ = !goog.labs.userAgent.browser.isChrome() || - goog.labs.userAgent.platform.isIos(); - - /** * Renders the source into the canvas based on the triangulation. - * @param {CanvasRenderingContext2D} context + * @param {number} width + * @param {number} height + * @param {number} pixelRatio * @param {number} sourceResolution * @param {ol.Extent} sourceExtent * @param {number} targetResolution @@ -88,12 +99,21 @@ ol.reproj.browserAntialiasesClip_ = !goog.labs.userAgent.browser.isChrome() || * @param {ol.reproj.Triangulation} triangulation * @param {Array.<{extent: ol.Extent, * image: (HTMLCanvasElement|Image)}>} sources - * @param {number} sourcePixelRatio * @param {boolean=} opt_renderEdges + * @return {HTMLCanvasElement} */ -ol.reproj.renderTriangles = function(context, +ol.reproj.render = function(width, height, pixelRatio, sourceResolution, sourceExtent, targetResolution, targetExtent, - triangulation, sources, sourcePixelRatio, opt_renderEdges) { + triangulation, sources, opt_renderEdges) { + + var context = ol.dom.createCanvasContext2D(Math.round(pixelRatio * width), + Math.round(pixelRatio * height)); + + if (sources.length === 0) { + return context.canvas; + } + + context.scale(pixelRatio, pixelRatio); var wrapXShiftDistance = !goog.isNull(sourceExtent) ? ol.extent.getWidth(sourceExtent) : 0; @@ -148,11 +168,11 @@ ol.reproj.renderTriangles = function(context, } var stitchContext = ol.dom.createCanvasContext2D( - Math.round(sourcePixelRatio * canvasWidthInUnits / sourceResolution), - Math.round(sourcePixelRatio * srcDataHeight / sourceResolution)); + Math.round(pixelRatio * canvasWidthInUnits / sourceResolution), + Math.round(pixelRatio * srcDataHeight / sourceResolution)); - stitchContext.scale(sourcePixelRatio / sourceResolution, - sourcePixelRatio / sourceResolution); + stitchContext.scale(pixelRatio / sourceResolution, + pixelRatio / sourceResolution); stitchContext.translate(-srcDataExtent[0], srcDataExtent[3]); goog.array.forEach(sources, function(src, i, arr) { @@ -208,15 +228,13 @@ ol.reproj.renderTriangles = function(context, var u2 = (tgt[2][0] - targetTL[0]) / targetResolution, v2 = -(tgt[2][1] - targetTL[1]) / targetResolution; - var performWrapXShift = false; - if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) { - performWrapXShift = true; - } else if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) { + var performWrapXShift = wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT; + if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) { var minX = Math.min(x0, x1, x2); var maxX = Math.max(x0, x1, x2); - performWrapXShift = (maxX - minX) > wrapXShiftDistance / 2 || - minX <= sourceExtent[0]; + performWrapXShift = minX <= sourceExtent[0] || + (maxX - minX) > wrapXShiftDistance / 2; } if (performWrapXShift) { @@ -249,18 +267,10 @@ ol.reproj.renderTriangles = function(context, context.beginPath(); if (ol.reproj.browserAntialiasesClip_) { - // Enlarge the clipping triangle by 1 pixel to ensure the edges overlap - // in order to mask gaps caused by antialiasing. var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3; - var calcClipPoint = function(u, v) { - var dX = u - centroidX, dY = v - centroidY; - var distance = Math.sqrt(dX * dX + dY * dY); - return [Math.round(u + dX / distance), Math.round(v + dY / distance)]; - }; - - var p0 = calcClipPoint(u0, v0); - var p1 = calcClipPoint(u1, v1); - var p2 = calcClipPoint(u2, v2); + var p0 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u0, v0); + var p1 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u1, v1); + var p2 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u2, v2); context.moveTo(p0[0], p0[1]); context.lineTo(p1[0], p1[1]); @@ -278,8 +288,8 @@ ol.reproj.renderTriangles = function(context, context.translate(srcDataExtent[0] - srcNumericalShiftX, srcDataExtent[3] - srcNumericalShiftY); - context.scale(sourceResolution / sourcePixelRatio, - -sourceResolution / sourcePixelRatio); + context.scale(sourceResolution / pixelRatio, + -sourceResolution / pixelRatio); context.drawImage(stitchContext.canvas, 0, 0); context.restore(); @@ -311,4 +321,17 @@ ol.reproj.renderTriangles = function(context, context.restore(); } + return context.canvas; +}; + + +/** + * Type of solution used to solve wrapX in source during rendering. + * @enum {number} + * @private + */ +ol.reproj.WrapXRendering_ = { + NONE: 0, + STITCH_SHIFT: 1, + STITCH_EXTENDED: 2 }; diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index 17ed887718..650b7ab092 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -8,7 +8,6 @@ goog.require('goog.math'); goog.require('goog.object'); goog.require('ol.Tile'); goog.require('ol.TileState'); -goog.require('ol.dom'); goog.require('ol.extent'); goog.require('ol.proj'); goog.require('ol.reproj'); @@ -247,28 +246,20 @@ ol.reproj.Tile.prototype.reproject_ = function() { } }, this); - // create the canvas var tileCoord = this.getTileCoord(); var z = tileCoord[0]; var size = this.targetTileGrid_.getTileSize(z); + var width = goog.isNumber(size) ? size : size[0]; + var height = goog.isNumber(size) ? size : size[1]; var targetResolution = this.targetTileGrid_.getResolution(z); - var srcResolution = this.sourceTileGrid_.getResolution(this.srcZ_); + var sourceResolution = this.sourceTileGrid_.getResolution(this.srcZ_); - var width = this.pixelRatio_ * (goog.isNumber(size) ? size : size[0]); - var height = this.pixelRatio_ * (goog.isNumber(size) ? size : size[1]); - var context = ol.dom.createCanvasContext2D(width, height); - context.imageSmoothingEnabled = true; - context.scale(this.pixelRatio_, this.pixelRatio_); + var targetExtent = this.targetTileGrid_.getTileCoordExtent(tileCoord); + this.canvas_ = ol.reproj.render(width, height, this.pixelRatio_, + sourceResolution, this.sourceTileGrid_.getExtent(), + targetResolution, targetExtent, this.triangulation_, sources, + this.renderEdges_); - if (sources.length > 0) { - var targetExtent = this.targetTileGrid_.getTileCoordExtent(tileCoord); - ol.reproj.renderTriangles(context, - srcResolution, this.sourceTileGrid_.getExtent(), - targetResolution, targetExtent, this.triangulation_, sources, - this.pixelRatio_, this.renderEdges_); - } - - this.canvas_ = context.canvas; this.state = ol.TileState.LOADED; this.changed(); }; @@ -283,14 +274,6 @@ ol.reproj.Tile.prototype.load = function() { this.changed(); var leftToLoad = 0; - var onSingleSourceLoaded = goog.bind(function() { - leftToLoad--; - goog.asserts.assert(leftToLoad >= 0, 'leftToLoad should not be negative'); - if (leftToLoad <= 0) { - this.unlistenSources_(); - this.reproject_(); - } - }, this); goog.asserts.assert(goog.isNull(this.sourcesListenerKeys_), 'this.sourcesListenerKeys_ should be null'); @@ -308,10 +291,16 @@ ol.reproj.Tile.prototype.load = function() { if (state == ol.TileState.LOADED || state == ol.TileState.ERROR || state == ol.TileState.EMPTY) { - onSingleSourceLoaded(); goog.events.unlistenByKey(sourceListenKey); + leftToLoad--; + goog.asserts.assert(leftToLoad >= 0, + 'leftToLoad should not be negative'); + if (leftToLoad <= 0) { + this.unlistenSources_(); + this.reproject_(); + } } - }); + }, false, this); this.sourcesListenerKeys_.push(sourceListenKey); } }, this); diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 4b34d172b5..ee4a143220 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -120,9 +120,9 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, var brDstSrc = this.transformInv_(brDst); var blDstSrc = this.transformInv_(blDst); - this.addQuadIfValid_(tlDst, trDst, brDst, blDst, - tlDstSrc, trDstSrc, brDstSrc, blDstSrc, - ol.RASTER_REPROJ_MAX_SUBDIVISION); + this.addQuad_(tlDst, trDst, brDst, blDst, + tlDstSrc, trDstSrc, brDstSrc, blDstSrc, + ol.RASTER_REPROJ_MAX_SUBDIVISION); transformInvCache = {}; }; @@ -139,7 +139,7 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, * @private */ ol.reproj.Triangulation.prototype.addTriangle_ = function(a, b, c, - aSrc, bSrc, cSrc) { + aSrc, bSrc, cSrc) { this.triangles_.push({ source: [aSrc, bSrc, cSrc], target: [a, b, c] @@ -161,7 +161,7 @@ ol.reproj.Triangulation.prototype.addTriangle_ = function(a, b, c, * @param {number} maxSubdiv Maximal allowed subdivision of the quad. * @private */ -ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, +ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, aSrc, bSrc, cSrc, dSrc, maxSubdiv) { var srcQuadExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc, dSrc]); @@ -229,25 +229,23 @@ ol.reproj.Triangulation.prototype.addQuadIfValid_ = function(a, b, c, d, } if (needsSubdivision) { if (Math.abs(a[0] - c[0]) <= Math.abs(a[1] - c[1])) { + // split horizontally (top & bottom) var bc = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2]; var bcSrc = this.transformInv_(bc); var da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2]; var daSrc = this.transformInv_(da); - this.addQuadIfValid_(a, b, bc, da, - aSrc, bSrc, bcSrc, daSrc, maxSubdiv - 1); - this.addQuadIfValid_(da, bc, c, d, - daSrc, bcSrc, cSrc, dSrc, maxSubdiv - 1); + this.addQuad_(a, b, bc, da, aSrc, bSrc, bcSrc, daSrc, maxSubdiv - 1); + this.addQuad_(da, bc, c, d, daSrc, bcSrc, cSrc, dSrc, maxSubdiv - 1); } else { + // split vertically (left & right) var ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; var abSrc = this.transformInv_(ab); var cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2]; var cdSrc = this.transformInv_(cd); - this.addQuadIfValid_(a, ab, cd, d, - aSrc, abSrc, cdSrc, dSrc, maxSubdiv - 1); - this.addQuadIfValid_(ab, b, c, cd, - abSrc, bSrc, cSrc, cdSrc, maxSubdiv - 1); + this.addQuad_(a, ab, cd, d, aSrc, abSrc, cdSrc, dSrc, maxSubdiv - 1); + this.addQuad_(ab, b, c, cd, abSrc, bSrc, cSrc, cdSrc, maxSubdiv - 1); } return; } @@ -293,8 +291,12 @@ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { var sourceProjExtent = this.sourceProj_.getExtent(); var right = sourceProjExtent[2]; - if (extent[0] > right) extent[0] -= this.sourceWorldWidth_; - if (extent[2] > right) extent[2] -= this.sourceWorldWidth_; + if (extent[0] > right) { + extent[0] -= this.sourceWorldWidth_; + } + if (extent[2] > right) { + extent[2] -= this.sourceWorldWidth_; + } } else { goog.array.forEach(this.triangles_, function(triangle, i, arr) { var src = triangle.source; From 59bce75d2af2c4116a48aceb633cebb3723639f1 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 4 Sep 2015 13:47:25 +0200 Subject: [PATCH 54/80] Improved documentation --- src/ol/reproj/image.js | 15 ++++++---- src/ol/reproj/reproj.js | 50 +++++++++++++++++--------------- src/ol/reproj/tile.js | 25 +++++++++------- src/ol/reproj/triangulation.js | 26 ++++++++++++----- src/ol/source/tileimagesource.js | 13 +++++++-- src/ol/source/tilesource.js | 6 ++-- 6 files changed, 83 insertions(+), 52 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index c7e583951d..c68fdcee1e 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -13,14 +13,19 @@ goog.require('ol.reproj.Triangulation'); /** + * @classdesc + * Class encapsulating single reprojected image. + * See {@link ol.source.Image}. + * * @constructor * @extends {ol.ImageBase} - * @param {ol.proj.Projection} sourceProj - * @param {ol.proj.Projection} targetProj - * @param {ol.Extent} targetExtent - * @param {number} targetResolution - * @param {number} pixelRatio + * @param {ol.proj.Projection} sourceProj Source projection (of the data). + * @param {ol.proj.Projection} targetProj Target projection. + * @param {ol.Extent} targetExtent Target extent. + * @param {number} targetResolution Target resolution. + * @param {number} pixelRatio Pixel ratio. * @param {function(ol.Extent, number, number):ol.ImageBase} getImageFunction + * Function returning source images (extent, resolution, pixelRatio). */ ol.reproj.Image = function(sourceProj, targetProj, targetExtent, targetResolution, pixelRatio, getImageFunction) { diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 0b1af19ee2..a66bb1fc25 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -30,10 +30,10 @@ ol.reproj.browserAntialiasesClip_ = !goog.labs.userAgent.browser.isChrome() || * The resolution is calculated regardless on what resolutions * are actually available in the dataset (TileGrid, Image, ...). * - * @param {ol.proj.Projection} sourceProj - * @param {ol.proj.Projection} targetProj - * @param {ol.Coordinate} targetCenter - * @param {number} targetResolution + * @param {ol.proj.Projection} sourceProj Source projection. + * @param {ol.proj.Projection} targetProj Target projection. + * @param {ol.Coordinate} targetCenter Target center. + * @param {number} targetResolution Target resolution. * @return {number} The best resolution to use. Can be +-Infinity, NaN or 0. */ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, @@ -73,34 +73,36 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, /** * Enlarge the clipping triangle point by 1 pixel to ensure the edges overlap * in order to mask gaps caused by antialiasing. - * @param {number} centroidX Centroid of the triangle. - * @param {number} centroidY Centroid of the triangle. - * @param {number} u - * @param {number} v - * @return {ol.Coordinate} + * + * @param {number} centroidX Centroid of the triangle (x coordinate in pixels). + * @param {number} centroidY Centroid of the triangle (y coordinate in pixels). + * @param {number} x X coordinate of the point (in pixels). + * @param {number} y Y coordinate of the point (in pixels). + * @return {ol.Coordinate} New point 1 px farther from the centroid. * @private */ -ol.reproj.enlargeClipPoint_ = function(centroidX, centroidY, u, v) { - var dX = u - centroidX, dY = v - centroidY; +ol.reproj.enlargeClipPoint_ = function(centroidX, centroidY, x, y) { + var dX = x - centroidX, dY = y - centroidY; var distance = Math.sqrt(dX * dX + dY * dY); - return [Math.round(u + dX / distance), Math.round(v + dY / distance)]; + return [Math.round(x + dX / distance), Math.round(y + dY / distance)]; }; /** - * Renders the source into the canvas based on the triangulation. - * @param {number} width - * @param {number} height - * @param {number} pixelRatio - * @param {number} sourceResolution - * @param {ol.Extent} sourceExtent - * @param {number} targetResolution - * @param {ol.Extent} targetExtent - * @param {ol.reproj.Triangulation} triangulation + * Renders the source data into new canvas based on the triangulation. + * + * @param {number} width Width of the canvas. + * @param {number} height Height of the canvas. + * @param {number} pixelRatio Pixel ratio. + * @param {number} sourceResolution Source resolution. + * @param {ol.Extent} sourceExtent Extent of the data source. + * @param {number} targetResolution Target resolution. + * @param {ol.Extent} targetExtent Target extent. + * @param {ol.reproj.Triangulation} triangulation Calculated triangulation. * @param {Array.<{extent: ol.Extent, - * image: (HTMLCanvasElement|Image)}>} sources - * @param {boolean=} opt_renderEdges - * @return {HTMLCanvasElement} + * image: (HTMLCanvasElement|Image)}>} sources Array of sources. + * @param {boolean=} opt_renderEdges Render reprojection edges. + * @return {HTMLCanvasElement} Canvas with reprojected data. */ ol.reproj.render = function(width, height, pixelRatio, sourceResolution, sourceExtent, targetResolution, targetExtent, diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index 650b7ab092..fb1070b7ea 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -16,19 +16,24 @@ goog.require('ol.reproj.Triangulation'); /** + * @classdesc + * Class encapsulating single reprojected tile. + * See {@link ol.source.TileImage}. + * * @constructor * @extends {ol.Tile} - * @param {ol.proj.Projection} sourceProj - * @param {ol.tilegrid.TileGrid} sourceTileGrid - * @param {ol.proj.Projection} targetProj - * @param {ol.tilegrid.TileGrid} targetTileGrid - * @param {number} z - * @param {number} x - * @param {number} y - * @param {number} pixelRatio + * @param {ol.proj.Projection} sourceProj Source projection. + * @param {ol.tilegrid.TileGrid} sourceTileGrid Source tile grid. + * @param {ol.proj.Projection} targetProj Target projection. + * @param {ol.tilegrid.TileGrid} targetTileGrid Target tile grid. + * @param {number} z Zoom level. + * @param {number} x X. + * @param {number} y Y. + * @param {number} pixelRatio Pixel ratio. * @param {function(number, number, number, number) : ol.Tile} getTileFunction - * @param {number=} opt_errorThreshold - * @param {boolean=} opt_renderEdges + * Function returning source tiles (z, x, y, pixelRatio). + * @param {number=} opt_errorThreshold Acceptable reprojection error (in px). + * @param {boolean=} opt_renderEdges Render reprojection edges. */ ol.reproj.Tile = function(sourceProj, sourceTileGrid, targetProj, targetTileGrid, z, x, y, pixelRatio, getTileFunction, diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index ee4a143220..2e0cc278b4 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -18,10 +18,14 @@ ol.reproj.Triangle; /** - * @param {ol.proj.Projection} sourceProj - * @param {ol.proj.Projection} targetProj - * @param {ol.Extent} targetExtent - * @param {ol.Extent} maxSourceExtent + * @classdesc + * Class containing triangulation of the given target extent. + * Used for determining source data and the reprojection itself. + * + * @param {ol.proj.Projection} sourceProj Source projection. + * @param {ol.proj.Projection} targetProj Target projection. + * @param {ol.Extent} targetExtent Target extent to triangulate. + * @param {ol.Extent} maxSourceExtent Maximal source extent that can be used. * @param {number} errorThreshold Acceptable error (in source units). * @constructor */ @@ -150,6 +154,8 @@ ol.reproj.Triangulation.prototype.addTriangle_ = function(a, b, c, /** * Adds quad (points in clock-wise order) to the triangulation * (and reprojects the vertices) if valid. + * Performs quad subdivision if needed to increase precision. + * * @param {ol.Coordinate} a * @param {ol.Coordinate} b * @param {ol.Coordinate} c @@ -264,7 +270,12 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, /** - * @return {ol.Extent} + * Calculates extent of the 'source' coordinates from all the triangles. + * The left bound of the returned extent can be higher than the right bound, + * if the triangulation wraps X in source (see + * {@link ol.reproj.Triangulation#getWrapsXInSource}). + * + * @return {ol.Extent} Calculated extent. */ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { if (!goog.isNull(this.trianglesSourceExtent_)) { @@ -312,7 +323,8 @@ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { /** - * @return {boolean} + * @return {boolean} Whether the source coordinates are wrapped in X + * (the triangulation "crosses the dateline"). */ ol.reproj.Triangulation.prototype.getWrapsXInSource = function() { return this.wrapsXInSource_; @@ -320,7 +332,7 @@ ol.reproj.Triangulation.prototype.getWrapsXInSource = function() { /** - * @return {Array.} + * @return {Array.} Array of the calculated triangles. */ ol.reproj.Triangulation.prototype.getTriangles = function() { return this.triangles_; diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index a991f1f071..14f416517c 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -303,7 +303,7 @@ ol.source.TileImage.prototype.handleTileChange_ = function(event) { /** * Sets whether to render reprojection edges or not (usually for debugging). - * @param {boolean} render + * @param {boolean} render Render the edges. * @api */ ol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) { @@ -317,8 +317,15 @@ ol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) { /** - * @param {ol.proj.ProjectionLike} projection - * @param {ol.tilegrid.TileGrid} tilegrid + * Sets the tile grid to use when reprojecting the tiles to the given + * projection instead of the default tile grid for the projection. + * + * This can be useful when the default tile grid cannot be created + * (e.g. projection has no extent defined) or + * for optimization reasons (custom tile size, resolutions, ...). + * + * @param {ol.proj.ProjectionLike} projection Projection. + * @param {ol.tilegrid.TileGrid} tilegrid Tile grid to use for the projection. * @api */ ol.source.TileImage.prototype.setTileGridForProjection = diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index 5c0b92af51..cda201384c 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -98,7 +98,7 @@ ol.source.Tile.prototype.canExpireCache = function() { /** - * @param {ol.proj.Projection} projection + * @param {ol.proj.Projection} projection Projection. * @param {Object.} usedTiles Used tiles. */ ol.source.Tile.prototype.expireCache = function(projection, usedTiles) { @@ -110,7 +110,7 @@ ol.source.Tile.prototype.expireCache = function(projection, usedTiles) { /** - * @param {ol.proj.Projection} projection + * @param {ol.proj.Projection} projection Projection. * @param {number} z Zoom level. * @param {ol.TileRange} tileRange Tile range. * @param {function(ol.Tile):(boolean|undefined)} callback Called with each @@ -231,7 +231,7 @@ ol.source.Tile.prototype.getTileCacheForProjection = function(projection) { /** - * @return {number} + * @return {number} Tile pixel ratio. */ ol.source.Tile.prototype.getTilePixelRatio = function() { return this.tilePixelRatio_; From 76974a588853c6c543ba6194a40698c632f6a8f2 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 4 Sep 2015 14:18:54 +0200 Subject: [PATCH 55/80] Minor improvements of reprojection examples --- examples/reprojection-by-code.html | 6 +++--- examples/reprojection-image.html | 7 +++---- examples/reprojection-image.js | 10 ++++------ examples/reprojection.html | 13 ++++++------- examples/reprojection.js | 13 ++++--------- 5 files changed, 20 insertions(+), 29 deletions(-) diff --git a/examples/reprojection-by-code.html b/examples/reprojection-by-code.html index c4db7ec985..ab784c4607 100644 --- a/examples/reprojection-by-code.html +++ b/examples/reprojection-by-code.html @@ -1,10 +1,10 @@ --- template: example.html title: Reprojection with EPSG.io database search -shortdesc: Demonstrates client-side raster reprojection of MapQuest OSM to any projection +shortdesc: Demonstrates client-side raster reprojection of MapQuest OSM to arbitrary projection docs: > - This example shows client-side raster reprojection capabilities of - OpenLayers 3 from MapQuest OSM (EPSG:3857) to any projection by searching + This example shows client-side raster reprojection capabilities from + MapQuest OSM (EPSG:3857) to arbitrary projection by searching in EPSG.io database. tags: "reprojection, projection, proj4js, mapquest, epsg.io" resources: diff --git a/examples/reprojection-image.html b/examples/reprojection-image.html index f6ca664fb4..4b505605ad 100644 --- a/examples/reprojection-image.html +++ b/examples/reprojection-image.html @@ -1,11 +1,10 @@ --- template: example.html title: Image reprojection example -shortdesc: Demonstrates client-side reprojection of single image. +shortdesc: Demonstrates client-side reprojection of single image source. docs: > - This example shows client-side single-image reprojection capabilities of - OpenLayers 3. -tags: "reprojection, projection, proj4js, mapquest, image" + This example shows client-side reprojection of single image source. +tags: "reprojection, projection, proj4js, mapquest, image, imagestatic" resources: - http://cdnjs.cloudflare.com/ajax/libs/proj4js/2.3.6/proj4.js --- diff --git a/examples/reprojection-image.js b/examples/reprojection-image.js index 0143246cb1..fb58cacdc6 100644 --- a/examples/reprojection-image.js +++ b/examples/reprojection-image.js @@ -12,9 +12,7 @@ proj4.defs('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' + '+x_0=400000 +y_0=-100000 +ellps=airy ' + '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + '+units=m +no_defs'); -var extent = [0, 0, 700000, 1300000]; -var proj27700 = ol.proj.get('EPSG:27700'); -proj27700.setExtent(extent); +var imageExtent = [0, 0, 700000, 1300000]; var map = new ol.Map({ layers: [ @@ -26,15 +24,15 @@ var map = new ol.Map({ url: 'http://upload.wikimedia.org/wikipedia/commons/thumb/1/18/' + 'British_National_Grid.svg/2000px-British_National_Grid.svg.png', projection: 'EPSG:27700', - imageExtent: extent + imageExtent: imageExtent }) }) ], renderer: common.getRendererFromQueryString(), target: 'map', view: new ol.View({ - center: ol.proj.transform(ol.extent.getCenter(extent), - proj27700, 'EPSG:3857'), + center: ol.proj.transform(ol.extent.getCenter(imageExtent), + 'EPSG:27700', 'EPSG:3857'), zoom: 4 }) }); diff --git a/examples/reprojection.html b/examples/reprojection.html index 52ddf0d862..af09326e19 100644 --- a/examples/reprojection.html +++ b/examples/reprojection.html @@ -3,9 +3,8 @@ template: example.html title: Raster reprojection example shortdesc: Demonstrates client-side raster reprojection between various projections. docs: > - This example shows client-side raster reprojection capabilities of - OpenLayers 3 between various projections. -tags: "reprojection, projection, proj4js, mapquest, wms" + This example shows client-side raster reprojection between various projections. +tags: "reprojection, projection, proj4js, mapquest, wms, wmts, hidpi" resources: - http://cdnjs.cloudflare.com/ajax/libs/proj4js/2.3.6/proj4.js --- @@ -17,14 +16,14 @@ resources:
- + diff --git a/examples/reprojection.js b/examples/reprojection.js index fb0cf70d09..f6d05b1d43 100644 --- a/examples/reprojection.js +++ b/examples/reprojection.js @@ -31,11 +31,6 @@ proj4.defs('EPSG:5479', '+proj=lcc +lat_1=-76.66666666666667 +lat_2=' + var proj5479 = ol.proj.get('EPSG:5479'); proj5479.setExtent([6825737.53, 4189159.80, 9633741.96, 5782472.71]); -proj4.defs('EPSG:5041', '+proj=stere +lat_0=90 +lat_ts=90 +lon_0=0 +k=0.994 ' + - '+x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs'); -var proj5041 = ol.proj.get('EPSG:5041'); -proj5041.setExtent([1994055.62, 5405875.53, 2000969.46, 2555456.55]); - proj4.defs('EPSG:21781', '+proj=somerc +lat_0=46.95240555555556 ' + '+lon_0=7.439583333333333 +k_0=1 +x_0=600000 +y_0=200000 +ellps=bessel ' + '+towgs84=674.4,15.1,405.3,0,0,0,0 +units=m +no_defs'); @@ -60,7 +55,7 @@ proj54009.setExtent([-18e6, -9e6, 18e6, 9e6]); var layers = []; -layers['OS'] = new ol.layer.Tile({ +layers['bng'] = new ol.layer.Tile({ source: new ol.source.XYZ({ projection: 'EPSG:27700', url: 'https://googledrive.com/host/0B0bm2WdRuvICflNqUmxEdUNOV0ZRUFQ3cXNXR' + @@ -69,7 +64,7 @@ layers['OS'] = new ol.layer.Tile({ }) }); -layers['MapQuest'] = new ol.layer.Tile({ +layers['mapquest'] = new ol.layer.Tile({ source: new ol.source.MapQuest({layer: 'osm'}) }); @@ -150,8 +145,8 @@ layers['states'] = new ol.layer.Tile({ var map = new ol.Map({ layers: [ - layers['MapQuest'], - layers['OS'] + layers['mapquest'], + layers['bng'] ], renderer: common.getRendererFromQueryString(), target: 'map', From 043b20670553bb8bfa58afaa2140582d58d83132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Jan=C3=A1k?= Date: Wed, 9 Sep 2015 11:00:10 +0200 Subject: [PATCH 56/80] OpenStreetMap reprojection example --- examples/reprojection-wgs84.html | 13 +++++++++++++ examples/reprojection-wgs84.js | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 examples/reprojection-wgs84.html create mode 100644 examples/reprojection-wgs84.js diff --git a/examples/reprojection-wgs84.html b/examples/reprojection-wgs84.html new file mode 100644 index 0000000000..f6f1ec3754 --- /dev/null +++ b/examples/reprojection-wgs84.html @@ -0,0 +1,13 @@ +--- +template: example.html +title: OpenStreetMap reprojection example +shortdesc: Demonstrates client-side reprojection of OpenStreetMap in WGS84. +docs: > + This example shows client-side reprojection of OpenStreetMap in WGS84. +tags: "reprojection, projection, openstreetmap, wgs84, tile" +--- +
+
+
+
+
diff --git a/examples/reprojection-wgs84.js b/examples/reprojection-wgs84.js new file mode 100644 index 0000000000..6f85d7d219 --- /dev/null +++ b/examples/reprojection-wgs84.js @@ -0,0 +1,19 @@ +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.layer.Tile'); +goog.require('ol.source.OSM'); + +var map = new ol.Map({ + layers: [ + new ol.layer.Tile({ + source: new ol.source.OSM() + }) + ], + renderer: common.getRendererFromQueryString(), + target: 'map', + view: new ol.View({ + projection: 'EPSG:4326', + center: [0, 0], + zoom: 1 + }) +}); From 4bcea473ebb16d598f9f2953700f8a3fbd570404 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 9 Sep 2015 10:11:22 +0200 Subject: [PATCH 57/80] Fix reprojection-by-code example in IE9 by using JSONP --- examples/reprojection-by-code.js | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/examples/reprojection-by-code.js b/examples/reprojection-by-code.js index 07a94a3175..9f24efcb8d 100644 --- a/examples/reprojection-by-code.js +++ b/examples/reprojection-by-code.js @@ -65,25 +65,29 @@ function setProjection(code, name, proj4def, bbox) { function search(query) { resultSpan.innerHTML = 'Searching...'; - $.ajax('http://epsg.io/?format=json&q=' + query).then(function(response) { - if (response) { - var results = response['results']; - if (results && results.length > 0) { - for (var i = 0; i < results.length; i++) { - var result = results[i]; - if (result) { - var code = result['code'], name = result['name'], - proj4def = result['proj4'], bbox = result['bbox']; - if (code && code.length > 0 && proj4def && proj4def.length > 0 && - bbox && bbox.length == 4) { - setProjection(code, name, proj4def, bbox); - return; + $.ajax({ + url: 'http://epsg.io/?format=json&q=' + query, + dataType: 'jsonp', + success: function(response) { + if (response) { + var results = response['results']; + if (results && results.length > 0) { + for (var i = 0; i < results.length; i++) { + var result = results[i]; + if (result) { + var code = result['code'], name = result['name'], + proj4def = result['proj4'], bbox = result['bbox']; + if (code && code.length > 0 && proj4def && proj4def.length > 0 && + bbox && bbox.length == 4) { + setProjection(code, name, proj4def, bbox); + return; + } } } } } + setProjection(null, null, null, null); } - setProjection(null, null, null, null); }); } From 402697c369598b8f1f2d3d3683c46adbe1a96df3 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 9 Sep 2015 10:14:48 +0200 Subject: [PATCH 58/80] Specify crossOrigin in reprojection examples to fix WebGL errors --- examples/reprojection-image.js | 1 + examples/reprojection.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/examples/reprojection-image.js b/examples/reprojection-image.js index fb58cacdc6..3688097980 100644 --- a/examples/reprojection-image.js +++ b/examples/reprojection-image.js @@ -23,6 +23,7 @@ var map = new ol.Map({ source: new ol.source.ImageStatic({ url: 'http://upload.wikimedia.org/wikipedia/commons/thumb/1/18/' + 'British_National_Grid.svg/2000px-British_National_Grid.svg.png', + crossOrigin: '', projection: 'EPSG:27700', imageExtent: imageExtent }) diff --git a/examples/reprojection.js b/examples/reprojection.js index f6d05b1d43..b1aaeb5e0c 100644 --- a/examples/reprojection.js +++ b/examples/reprojection.js @@ -60,6 +60,7 @@ layers['bng'] = new ol.layer.Tile({ projection: 'EPSG:27700', url: 'https://googledrive.com/host/0B0bm2WdRuvICflNqUmxEdUNOV0ZRUFQ3cXNXR' + 'FlOTm9MWmJxSDAxM2V5M1ZJX2lITE9oejA/{z}/{x}/{y}.png', + crossOrigin: '', maxZoom: 3 }) }); @@ -71,6 +72,7 @@ layers['mapquest'] = new ol.layer.Tile({ layers['wms4326'] = new ol.layer.Tile({ source: new ol.source.TileWMS({ url: 'http://demo.boundlessgeo.com/geoserver/wms', + crossOrigin: '', params: { 'LAYERS': 'ne:NE1_HR_LC_SR_W_DR' }, @@ -102,6 +104,7 @@ $.ajax('http://map1.vis.earthdata.nasa.gov/wmts-arctic/' + var result = parser.read(response); var options = ol.source.WMTS.optionsFromCapabilities(result, {layer: 'OSM_Land_Mask', matrixSet: 'EPSG3413_250m'}); + options.crossOrigin = ''; options.projection = 'EPSG:3413'; options.wrapX = false; layers['wmts3413'] = new ol.layer.Tile({ @@ -112,6 +115,7 @@ $.ajax('http://map1.vis.earthdata.nasa.gov/wmts-arctic/' + layers['grandcanyon'] = new ol.layer.Tile({ source: new ol.source.XYZ({ url: 'http://tileserver.maptiler.com/grandcanyon@2x/{z}/{x}/{y}.png', + crossOrigin: '', tilePixelRatio: 2, maxZoom: 15, attributions: [new ol.Attribution({ @@ -131,6 +135,7 @@ for (var i = 0, ii = resolutions.length; i < ii; ++i) { layers['states'] = new ol.layer.Tile({ source: new ol.source.TileWMS({ url: 'http://demo.boundlessgeo.com/geoserver/wms', + crossOrigin: '', params: {'LAYERS': 'topp:states', 'TILED': true}, serverType: 'geoserver', tileGrid: new ol.tilegrid.TileGrid({ From 3353eeb0c30114fe1a4a0b3d4f9279445bb2baa7 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 9 Sep 2015 11:03:35 +0200 Subject: [PATCH 59/80] Fix IE9 test timeouts by using different pixel placeholder IE9 seems to have occasional problems with decoding the data protocol url used before (single pixel placeholder). --- test/spec/ol/reproj/image.test.js | 3 ++- test/spec/ol/reproj/tile.test.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/spec/ol/reproj/image.test.js b/test/spec/ol/reproj/image.test.js index 08b3b6d409..13d8a8627a 100644 --- a/test/spec/ol/reproj/image.test.js +++ b/test/spec/ol/reproj/image.test.js @@ -7,7 +7,8 @@ describe('ol.reproj.Image', function() { [-180, -85, 180, 85], 10, pixelRatio, function(extent, resolution, pixelRatio) { return new ol.Image(extent, resolution, pixelRatio, [], - 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=', '', + 'data:image/gif;base64,' + + 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=', null, function(image, src) { image.getImage().src = src; }); diff --git a/test/spec/ol/reproj/tile.test.js b/test/spec/ol/reproj/tile.test.js index a209a1c471..1354b804fb 100644 --- a/test/spec/ol/reproj/tile.test.js +++ b/test/spec/ol/reproj/tile.test.js @@ -23,7 +23,8 @@ describe('ol.reproj.Tile', function() { ol.tilegrid.createForProjection(proj4326, 3, opt_tileSize), 3, 2, -2, pixelRatio, function(z, x, y, pixelRatio) { return new ol.ImageTile([z, x, y], ol.TileState.IDLE, - 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=', '', + 'data:image/gif;base64,' + + 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=', null, function(tile, src) { tile.getImage().src = src; }); From 5388f9655123d5305ddef8d79a0e35ca95c91691 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 9 Sep 2015 11:49:00 +0200 Subject: [PATCH 60/80] Reproject image sources only if actually needed --- src/ol/reproj/image.js | 14 ++++++++++++++ src/ol/source/imagesource.js | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index c68fdcee1e..ac0ea5d403 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -30,6 +30,12 @@ goog.require('ol.reproj.Triangulation'); ol.reproj.Image = function(sourceProj, targetProj, targetExtent, targetResolution, pixelRatio, getImageFunction) { + /** + * @private + * @type {ol.proj.Projection} + */ + this.targetProj_ = targetProj; + /** * @private * @type {ol.Extent} @@ -127,6 +133,14 @@ ol.reproj.Image.prototype.getImage = function(opt_context) { }; +/** + * @return {ol.proj.Projection} Projection. + */ +ol.reproj.Image.prototype.getProjection = function() { + return this.targetProj_; +}; + + /** * @private */ diff --git a/src/ol/source/imagesource.js b/src/ol/source/imagesource.js index d4fad482b1..e0ac8486a2 100644 --- a/src/ol/source/imagesource.js +++ b/src/ol/source/imagesource.js @@ -5,9 +5,9 @@ goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.events.Event'); goog.require('ol.Attribution'); -goog.require('ol.Extent'); goog.require('ol.ImageState'); goog.require('ol.array'); +goog.require('ol.extent'); goog.require('ol.proj'); goog.require('ol.reproj.Image'); goog.require('ol.source.Source'); @@ -58,6 +58,20 @@ ol.source.Image = function(options) { return b - a; }, true), 'resolutions must be null or sorted in descending order'); + + /** + * @private + * @type {ol.reproj.Image} + */ + this.reprojectedImage_ = null; + + + /** + * @private + * @type {number} + */ + this.reprojectedRevision_ = 0; + }; goog.inherits(ol.source.Image, ol.source.Source); @@ -104,14 +118,28 @@ ol.source.Image.prototype.getImage = } return this.getImageInternal(extent, resolution, pixelRatio, projection); } else { - var image = new ol.reproj.Image( + if (!goog.isNull(this.reprojectedImage_)) { + if (this.reprojectedRevision_ == this.getRevision() && + ol.proj.equivalent( + this.reprojectedImage_.getProjection(), projection) && + this.reprojectedImage_.getResolution() == resolution && + this.reprojectedImage_.getPixelRatio() == pixelRatio && + ol.extent.equals(this.reprojectedImage_.getExtent(), extent)) { + return this.reprojectedImage_; + } + this.reprojectedImage_.dispose(); + this.reprojectedImage_ = null; + } + + this.reprojectedImage_ = new ol.reproj.Image( sourceProjection, projection, extent, resolution, pixelRatio, goog.bind(function(extent, resolution, pixelRatio) { return this.getImageInternal(extent, resolution, pixelRatio, sourceProjection); }, this)); + this.reprojectedRevision_ = this.getRevision(); - return image; + return this.reprojectedImage_; } }; From 94caa0716852a430d8e3d5ab2bffacb32164e36d Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 9 Sep 2015 13:37:45 +0200 Subject: [PATCH 61/80] Minor code style, documentation and typo fixes --- examples/reprojection-by-code.js | 2 +- externs/olx.js | 33 +++++++++++++++++++++----------- src/ol/ol.js | 7 ++++--- src/ol/reproj/triangulation.js | 4 ++-- src/ol/source/tileimagesource.js | 4 +++- 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/examples/reprojection-by-code.js b/examples/reprojection-by-code.js index 9f24efcb8d..96203a4177 100644 --- a/examples/reprojection-by-code.js +++ b/examples/reprojection-by-code.js @@ -72,7 +72,7 @@ function search(query) { if (response) { var results = response['results']; if (results && results.length > 0) { - for (var i = 0; i < results.length; i++) { + for (var i = 0, ii = results.length; i < ii; i++) { var result = results[i]; if (result) { var code = result['code'], name = result['name'], diff --git a/externs/olx.js b/externs/olx.js index e2cd7a93f5..60da9abb15 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -3644,7 +3644,8 @@ olx.source.BingMapsOptions.prototype.maxZoom; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ @@ -3830,7 +3831,8 @@ olx.source.TileImageOptions.prototype.projection; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ @@ -4111,7 +4113,8 @@ olx.source.MapQuestOptions.prototype.layer; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ @@ -4211,7 +4214,8 @@ olx.source.OSMOptions.prototype.maxZoom; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ @@ -4615,7 +4619,8 @@ olx.source.StamenOptions.prototype.opaque; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ @@ -4801,7 +4806,8 @@ olx.source.TileArcGISRestOptions.prototype.projection; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ @@ -4877,7 +4883,8 @@ olx.source.TileJSONOptions.prototype.crossOrigin; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ @@ -5020,7 +5027,8 @@ olx.source.TileWMSOptions.prototype.projection; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ @@ -5259,7 +5267,8 @@ olx.source.WMTSOptions.prototype.projection; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ @@ -5445,7 +5454,8 @@ olx.source.XYZOptions.prototype.projection; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ @@ -5580,7 +5590,8 @@ olx.source.ZoomifyOptions.prototype.logo; /** - * Maximum allowed reprojection error. + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. * @type {number|undefined} * @api */ diff --git a/src/ol/ol.js b/src/ol/ol.js index 48d80a1416..0e441ae844 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -174,7 +174,8 @@ ol.OVERVIEWMAP_MIN_RATIO = 0.1; /** - * @define {number} Maximum number of source tiles for raster reprojection. + * @define {number} Maximum number of source tiles for raster reprojection of + * a single tile. * If too many source tiles are determined to be loaded to create a single * reprojected tile the browser can become unresponsive or even crash. * This can happen if the developer defines projections improperly and/or @@ -188,7 +189,7 @@ ol.RASTER_REPROJ_MAX_SOURCE_TILES = 100; /** * @define {number} Maximum number of subdivision steps during raster * reprojection triangulation. Prevents high memory usage and large - * number of proj4 calls when for certain transformations and areas. + * number of proj4 calls (for certain transformations and areas). * At most `2*(2^this)` triangles are created. Default is `10`. */ ol.RASTER_REPROJ_MAX_SUBDIVISION = 10; @@ -197,7 +198,7 @@ ol.RASTER_REPROJ_MAX_SUBDIVISION = 10; /** * @define {number} Maximum allowed size of triangle relative to world width. * When transforming corners of world extent between certain projections, - * The resulting triangulation seems to have zero error and no subdivision + * the resulting triangulation seems to have zero error and no subdivision * is performed. * If the triangle width is more than this (relative to world width; 0-1), * subdivison is forced (respecting `ol.RASTER_REPROJ_MAX_SUBDIVISION`). diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 2e0cc278b4..2edd3d455d 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -83,7 +83,7 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, this.trianglesSourceExtent_ = null; /** - * Indicates that source coordinates has to be shifted during reprojection. + * Indicates that source coordinates have to be shifted during reprojection. * This is needed when the triangulation crosses * edge of the source projection (dateline). * @type {boolean} @@ -286,7 +286,7 @@ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { if (this.wrapsXInSource_) { // although only some of the triangles are crossing the dateline, - // all coordiantes need to be "shifted" to be positive + // all coordinates need to be "shifted" to be positive // to properly calculate the extent (and then possibly shifted back) goog.array.forEach(this.triangles_, function(triangle, i, arr) { diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 14f416517c..4cf2ae41af 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -307,7 +307,9 @@ ol.source.TileImage.prototype.handleTileChange_ = function(event) { * @api */ ol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) { - if (this.renderReprojectionEdges_ == render) return; + if (this.renderReprojectionEdges_ == render) { + return; + } this.renderReprojectionEdges_ = render; goog.object.forEach(this.tileCacheForProjection, function(tileCache) { tileCache.clear(); From 0f096077250184fc844515a703b9c748d7af5be3 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 9 Sep 2015 14:18:02 +0200 Subject: [PATCH 62/80] Slightly decrease build size when reprojection code is disabled --- src/ol/source/tileimagesource.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index 4cf2ae41af..e94df1793c 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -307,7 +307,8 @@ ol.source.TileImage.prototype.handleTileChange_ = function(event) { * @api */ ol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) { - if (this.renderReprojectionEdges_ == render) { + if (!ol.ENABLE_RASTER_REPROJECTION || + this.renderReprojectionEdges_ == render) { return; } this.renderReprojectionEdges_ = render; From 00a8b4da16d4b8d5b738f139a1e70dff6f9686eb Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 9 Sep 2015 19:27:44 +0200 Subject: [PATCH 63/80] Increase readability of reprojection example Use map/layer terminology more clearly and clarify checkbox usage --- examples/reprojection.html | 6 +++--- examples/reprojection.js | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/reprojection.html b/examples/reprojection.html index af09326e19..365c612945 100644 --- a/examples/reprojection.html +++ b/examples/reprojection.html @@ -15,14 +15,14 @@ resources:
-
- @@ -43,6 +43,6 @@ resources:
- + (only displayed on reprojected data)
diff --git a/examples/reprojection.js b/examples/reprojection.js index b1aaeb5e0c..c65d3aa164 100644 --- a/examples/reprojection.js +++ b/examples/reprojection.js @@ -163,8 +163,8 @@ var map = new ol.Map({ }); -var baseMapSelect = document.getElementById('base-map'); -var overlayMapSelect = document.getElementById('overlay-map'); +var baseLayerSelect = document.getElementById('base-layer'); +var overlayLayerSelect = document.getElementById('overlay-layer'); var viewProjSelect = document.getElementById('view-projection'); var renderEdgesCheckbox = document.getElementById('render-edges'); var renderEdges = false; @@ -204,8 +204,8 @@ var updateRenderEdgesOnLayer = function(layer) { /** * @param {Event} e Change event. */ -baseMapSelect.onchange = function(e) { - var layer = layers[baseMapSelect.value]; +baseLayerSelect.onchange = function(e) { + var layer = layers[baseLayerSelect.value]; if (layer) { layer.setOpacity(1); updateRenderEdgesOnLayer(layer); @@ -217,8 +217,8 @@ baseMapSelect.onchange = function(e) { /** * @param {Event} e Change event. */ -overlayMapSelect.onchange = function(e) { - var layer = layers[overlayMapSelect.value]; +overlayLayerSelect.onchange = function(e) { + var layer = layers[overlayLayerSelect.value]; if (layer) { layer.setOpacity(0.7); updateRenderEdgesOnLayer(layer); From 6431622881deba607e3bdcb640c3e3817c0be0f0 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 17 Sep 2015 10:53:35 +0200 Subject: [PATCH 64/80] Use better URL for BNG tiles in reprojection example --- examples/reprojection.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/reprojection.js b/examples/reprojection.js index c65d3aa164..4093e6693f 100644 --- a/examples/reprojection.js +++ b/examples/reprojection.js @@ -58,10 +58,9 @@ var layers = []; layers['bng'] = new ol.layer.Tile({ source: new ol.source.XYZ({ projection: 'EPSG:27700', - url: 'https://googledrive.com/host/0B0bm2WdRuvICflNqUmxEdUNOV0ZRUFQ3cXNXR' + - 'FlOTm9MWmJxSDAxM2V5M1ZJX2lITE9oejA/{z}/{x}/{y}.png', + url: 'http://tileserver.maptiler.com/miniscale/{z}/{x}/{y}.png', crossOrigin: '', - maxZoom: 3 + maxZoom: 6 }) }); From c6e2c6a16e33b78783c36b1f7474e6dbf5b7109f Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 17 Sep 2015 11:16:26 +0200 Subject: [PATCH 65/80] Add demonstration of layer extent setting to reprojection example --- examples/reprojection.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/reprojection.js b/examples/reprojection.js index 4093e6693f..dec799b8eb 100644 --- a/examples/reprojection.js +++ b/examples/reprojection.js @@ -178,6 +178,13 @@ function updateViewProjection() { extent: newProjExtent || undefined }); map.setView(newView); + + // Example how to prevent double occurence of map by limiting layer extent + if (newProj == ol.proj.get('EPSG:3857')) { + layers['bng'].setExtent([-1057216, 6405988, 404315, 8759696]); + } else { + layers['bng'].setExtent(undefined); + } } From 3f897cfb792b351352ab4cf70a0a4eec45fd6472 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Tue, 29 Sep 2015 13:15:48 +0200 Subject: [PATCH 66/80] Minor typo fix --- src/ol/reproj/reproj.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index a66bb1fc25..84b55b8c12 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -27,7 +27,7 @@ ol.reproj.browserAntialiasesClip_ = !goog.labs.userAgent.browser.isChrome() || /** * Calculates ideal resolution to use from the source in order to achieve * pixel mapping as close as possible to 1:1 during reprojection. - * The resolution is calculated regardless on what resolutions + * The resolution is calculated regardless of what resolutions * are actually available in the dataset (TileGrid, Image, ...). * * @param {ol.proj.Projection} sourceProj Source projection. From 783acfa96148c022862e57f42871c5ea69d008d7 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Tue, 29 Sep 2015 13:47:00 +0200 Subject: [PATCH 67/80] Use ol.math.clamp instead of goog.math.clamp --- src/ol/reproj/reproj.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 84b55b8c12..20904a9f4a 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -149,14 +149,14 @@ ol.reproj.render = function(width, height, pixelRatio, }); if (!goog.isNull(sourceExtent)) { if (wrapXType == ol.reproj.WrapXRendering_.NONE) { - srcDataExtent[0] = goog.math.clamp( + srcDataExtent[0] = ol.math.clamp( srcDataExtent[0], sourceExtent[0], sourceExtent[2]); - srcDataExtent[2] = goog.math.clamp( + srcDataExtent[2] = ol.math.clamp( srcDataExtent[2], sourceExtent[0], sourceExtent[2]); } - srcDataExtent[1] = goog.math.clamp( + srcDataExtent[1] = ol.math.clamp( srcDataExtent[1], sourceExtent[1], sourceExtent[3]); - srcDataExtent[3] = goog.math.clamp( + srcDataExtent[3] = ol.math.clamp( srcDataExtent[3], sourceExtent[1], sourceExtent[3]); } From f3d5d16a823b4460a8d16f2c993ac0c18967317e Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Tue, 29 Sep 2015 13:53:24 +0200 Subject: [PATCH 68/80] Remove use of goog.isDef --- src/ol/reproj/reproj.js | 4 ++-- src/ol/reproj/tile.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 20904a9f4a..67abb0234e 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -46,11 +46,11 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, targetProj.getPointResolution(targetResolution, targetCenter); var targetMPU = targetProj.getMetersPerUnit(); - if (goog.isDef(targetMPU)) { + if (targetMPU !== undefined) { sourceResolution *= targetMPU; } var sourceMPU = sourceProj.getMetersPerUnit(); - if (goog.isDef(sourceMPU)) { + if (sourceMPU !== undefined) { sourceResolution /= sourceMPU; } diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index fb1070b7ea..0de6c3bf20 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -45,7 +45,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, * @private * @type {boolean} */ - this.renderEdges_ = goog.isDef(opt_renderEdges) ? opt_renderEdges : false; + this.renderEdges_ = opt_renderEdges !== undefined ? opt_renderEdges : false; /** * @private @@ -132,7 +132,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, return; } - var errorThresholdInPixels = goog.isDef(opt_errorThreshold) ? + var errorThresholdInPixels = opt_errorThreshold !== undefined ? opt_errorThreshold : ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; /** @@ -219,7 +219,7 @@ ol.reproj.Tile.prototype.disposeInternal = function() { * @inheritDoc */ ol.reproj.Tile.prototype.getImage = function(opt_context) { - if (goog.isDef(opt_context)) { + if (opt_context !== undefined) { var image; var key = goog.getUid(opt_context); if (key in this.canvasByContext_) { From 8fb1d1f244aed62d6ab9a74293425d8e7c018a35 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Wed, 14 Oct 2015 11:06:19 +0200 Subject: [PATCH 69/80] Remove usage of various goog.* functions goog.isNull, goog.isDefAndNotNull and goog.array.* --- src/ol/reproj/image.js | 11 +++---- src/ol/reproj/reproj.js | 17 +++++----- src/ol/reproj/tile.js | 53 +++++++++++++++++--------------- src/ol/reproj/triangulation.js | 27 ++++++++-------- src/ol/source/imagesource.js | 8 ++--- src/ol/source/tileimagesource.js | 14 ++++----- src/ol/source/tilesource.js | 6 ++-- 7 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index ac0ea5d403..a4abd4760b 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -43,8 +43,8 @@ ol.reproj.Image = function(sourceProj, targetProj, this.maxSourceExtent_ = sourceProj.getExtent(); var maxTargetExtent = targetProj.getExtent(); - var limitedTargetExtent = goog.isNull(maxTargetExtent) ? - targetExtent : ol.extent.getIntersection(targetExtent, maxTargetExtent); + var limitedTargetExtent = maxTargetExtent ? + ol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent; var targetCenter = ol.extent.getCenter(limitedTargetExtent); var sourceResolution = ol.reproj.calculateSourceResolution( @@ -84,8 +84,7 @@ ol.reproj.Image = function(sourceProj, targetProj, * @private * @type {number} */ - this.srcPixelRatio_ = - !goog.isNull(this.srcImage_) ? this.srcImage_.getPixelRatio() : 1; + this.srcPixelRatio_ = this.srcImage_ ? this.srcImage_.getPixelRatio() : 1; /** * @private @@ -103,7 +102,7 @@ ol.reproj.Image = function(sourceProj, targetProj, var state = ol.ImageState.LOADED; var attributions = []; - if (!goog.isNull(this.srcImage_)) { + if (this.srcImage_) { state = ol.ImageState.IDLE; attributions = this.srcImage_.getAttributions(); } @@ -195,7 +194,7 @@ ol.reproj.Image.prototype.load = function() { * @private */ ol.reproj.Image.prototype.unlistenSource_ = function() { - goog.asserts.assert(!goog.isNull(this.sourceListenerKey_), + goog.asserts.assert(this.sourceListenerKey_, 'this.sourceListenerKey_ should not be null'); goog.events.unlistenByKey(this.sourceListenerKey_); this.sourceListenerKey_ = null; diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 67abb0234e..4de2495b0d 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -1,6 +1,5 @@ goog.provide('ol.reproj'); -goog.require('goog.array'); goog.require('goog.labs.userAgent.browser'); goog.require('goog.labs.userAgent.platform'); goog.require('goog.math'); @@ -117,8 +116,7 @@ ol.reproj.render = function(width, height, pixelRatio, context.scale(pixelRatio, pixelRatio); - var wrapXShiftDistance = !goog.isNull(sourceExtent) ? - ol.extent.getWidth(sourceExtent) : 0; + var wrapXShiftDistance = sourceExtent ? ol.extent.getWidth(sourceExtent) : 0; var wrapXType = ol.reproj.WrapXRendering_.NONE; @@ -137,7 +135,7 @@ ol.reproj.render = function(width, height, pixelRatio, } var srcDataExtent = ol.extent.createEmpty(); - goog.array.forEach(sources, function(src, i, arr) { + sources.forEach(function(src, i, arr) { if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) { var srcW = src.extent[2] - src.extent[0]; var srcX = goog.math.modulo(src.extent[0], wrapXShiftDistance); @@ -147,7 +145,7 @@ ol.reproj.render = function(width, height, pixelRatio, ol.extent.extend(srcDataExtent, src.extent); } }); - if (!goog.isNull(sourceExtent)) { + if (sourceExtent) { if (wrapXType == ol.reproj.WrapXRendering_.NONE) { srcDataExtent[0] = ol.math.clamp( srcDataExtent[0], sourceExtent[0], sourceExtent[2]); @@ -177,7 +175,7 @@ ol.reproj.render = function(width, height, pixelRatio, pixelRatio / sourceResolution); stitchContext.translate(-srcDataExtent[0], srcDataExtent[3]); - goog.array.forEach(sources, function(src, i, arr) { + sources.forEach(function(src, i, arr) { var xPos = src.extent[0]; var yPos = -src.extent[3]; var srcWidth = ol.extent.getWidth(src.extent); @@ -196,7 +194,7 @@ ol.reproj.render = function(width, height, pixelRatio, var targetTL = ol.extent.getTopLeft(targetExtent); - goog.array.forEach(triangulation.getTriangles(), function(tri, i, arr) { + triangulation.getTriangles().forEach(function(tri, i, arr) { context.save(); /* Calculate affine transform (src -> dst) @@ -263,7 +261,7 @@ ol.reproj.render = function(width, height, pixelRatio, [0, 0, x2, y2, v2 - v0] ]; var coefs = ol.math.solveLinearSystem(augmentedMatrix); - if (goog.isNull(coefs)) { + if (!coefs) { return; } @@ -303,8 +301,7 @@ ol.reproj.render = function(width, height, pixelRatio, context.strokeStyle = 'black'; context.lineWidth = 1; - goog.array.forEach(triangulation.getTriangles(), function(tri, i, arr) { - + triangulation.getTriangles().forEach(function(tri, i, arr) { var tgt = tri.target; var u0 = (tgt[0][0] - targetTL[0]) / targetResolution, v0 = -(tgt[0][1] - targetTL[1]) / targetResolution; diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index 0de6c3bf20..cca4031593 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -1,6 +1,5 @@ goog.provide('ol.reproj.Tile'); -goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.events'); goog.require('goog.events.EventType'); @@ -99,8 +98,8 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, var maxTargetExtent = this.targetTileGrid_.getExtent(); var maxSourceExtent = this.sourceTileGrid_.getExtent(); - var limitedTargetExtent = goog.isNull(maxTargetExtent) ? - targetExtent : ol.extent.getIntersection(targetExtent, maxTargetExtent); + var limitedTargetExtent = maxTargetExtent ? + ol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent; if (ol.extent.getArea(limitedTargetExtent) === 0) { // Tile is completely outside range -> EMPTY @@ -110,8 +109,8 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, } var sourceProjExtent = sourceProj.getExtent(); - if (!goog.isNull(sourceProjExtent)) { - if (goog.isNull(maxSourceExtent)) { + if (sourceProjExtent) { + if (!maxSourceExtent) { maxSourceExtent = sourceProjExtent; } else { maxSourceExtent = ol.extent.getIntersection( @@ -152,7 +151,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, this.srcZ_ = sourceTileGrid.getZForResolution(sourceResolution); var srcExtent = this.triangulation_.calculateSourceExtent(); - if (!goog.isNull(maxSourceExtent) && + if (maxSourceExtent && !this.triangulation_.getWrapsXInSource() && !ol.extent.intersects(maxSourceExtent, srcExtent)) { this.state = ol.TileState.EMPTY; @@ -160,25 +159,31 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, var srcRange = sourceTileGrid.getTileRangeForExtentAndZ( srcExtent, this.srcZ_); - var xRange; + var xRange = []; var srcFullRange = sourceTileGrid.getFullTileRange(this.srcZ_); - if (!goog.isNull(srcFullRange)) { + if (srcFullRange) { srcRange.minY = Math.max(srcRange.minY, srcFullRange.minY); srcRange.maxY = Math.min(srcRange.maxY, srcFullRange.maxY); if (srcRange.minX > srcRange.maxX) { - xRange = goog.array.concat( - goog.array.range(srcRange.minX, srcFullRange.maxX + 1), - goog.array.range(srcFullRange.minX, srcRange.maxX + 1) - ); + var i; + for (i = srcRange.minX; i <= srcFullRange.maxX; i++) { + xRange.push(i); + } + for (i = srcFullRange.minX; i <= srcRange.maxX; i++) { + xRange.push(i); + } } else { - xRange = goog.array.range( - Math.max(srcRange.minX, srcFullRange.minX), - Math.min(srcRange.maxX, srcFullRange.maxX) + 1 - ); + var first = Math.max(srcRange.minX, srcFullRange.minX); + var last = Math.min(srcRange.maxX, srcFullRange.maxX); + for (var j = first; j <= last; j++) { + xRange.push(j); + } } } else { - xRange = goog.array.range(srcRange.minX, srcRange.maxX + 1); + for (var k = srcRange.minX; k <= srcRange.maxX; k++) { + xRange.push(k); + } } var tilesRequired = xRange.length * srcRange.getHeight(); @@ -187,7 +192,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, this.state = ol.TileState.ERROR; return; } - goog.array.forEach(xRange, function(srcX, i, arr) { + xRange.forEach(function(srcX, i, arr) { for (var srcY = srcRange.minY; srcY <= srcRange.maxY; srcY++) { var tile = getTileFunction(this.srcZ_, srcX, srcY, pixelRatio); if (tile) { @@ -242,7 +247,7 @@ ol.reproj.Tile.prototype.getImage = function(opt_context) { */ ol.reproj.Tile.prototype.reproject_ = function() { var sources = []; - goog.array.forEach(this.srcTiles_, function(tile, i, arr) { + this.srcTiles_.forEach(function(tile, i, arr) { if (tile && tile.getState() == ol.TileState.LOADED) { sources.push({ extent: this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord), @@ -280,11 +285,11 @@ ol.reproj.Tile.prototype.load = function() { var leftToLoad = 0; - goog.asserts.assert(goog.isNull(this.sourcesListenerKeys_), + goog.asserts.assert(!this.sourcesListenerKeys_, 'this.sourcesListenerKeys_ should be null'); this.sourcesListenerKeys_ = []; - goog.array.forEach(this.srcTiles_, function(tile, i, arr) { + this.srcTiles_.forEach(function(tile, i, arr) { var state = tile.getState(); if (state == ol.TileState.IDLE || state == ol.TileState.LOADING) { leftToLoad++; @@ -310,7 +315,7 @@ ol.reproj.Tile.prototype.load = function() { } }, this); - goog.array.forEach(this.srcTiles_, function(tile, i, arr) { + this.srcTiles_.forEach(function(tile, i, arr) { var state = tile.getState(); if (state == ol.TileState.IDLE) { tile.load(); @@ -328,8 +333,8 @@ ol.reproj.Tile.prototype.load = function() { * @private */ ol.reproj.Tile.prototype.unlistenSources_ = function() { - goog.asserts.assert(!goog.isNull(this.sourcesListenerKeys_), + goog.asserts.assert(this.sourcesListenerKeys_, 'this.sourcesListenerKeys_ should not be null'); - goog.array.forEach(this.sourcesListenerKeys_, goog.events.unlistenByKey); + this.sourcesListenerKeys_.forEach(goog.events.unlistenByKey); this.sourcesListenerKeys_ = null; }; diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 2edd3d455d..dc95b77b6d 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -1,6 +1,5 @@ goog.provide('ol.reproj.Triangulation'); -goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.math'); goog.require('ol.extent'); @@ -96,8 +95,8 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, * @private */ this.canWrapXInSource_ = this.sourceProj_.canWrapX() && - !goog.isNull(maxSourceExtent) && - !goog.isNull(this.sourceProj_.getExtent()) && + !!maxSourceExtent && + !!this.sourceProj_.getExtent() && (ol.extent.getWidth(maxSourceExtent) == ol.extent.getWidth(this.sourceProj_.getExtent())); @@ -105,14 +104,14 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, * @type {?number} * @private */ - this.sourceWorldWidth_ = !goog.isNull(this.sourceProj_.getExtent()) ? + this.sourceWorldWidth_ = this.sourceProj_.getExtent() ? ol.extent.getWidth(this.sourceProj_.getExtent()) : null; /** * @type {?number} * @private */ - this.targetWorldWidth_ = !goog.isNull(this.targetProj_.getExtent()) ? + this.targetWorldWidth_ = this.targetProj_.getExtent() ? ol.extent.getWidth(this.targetProj_.getExtent()) : null; var tlDst = ol.extent.getTopLeft(targetExtent); @@ -171,7 +170,7 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, aSrc, bSrc, cSrc, dSrc, maxSubdiv) { var srcQuadExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc, dSrc]); - var srcCoverageX = !goog.isNull(this.sourceWorldWidth_) ? + var srcCoverageX = this.sourceWorldWidth_ ? ol.extent.getWidth(srcQuadExtent) / this.sourceWorldWidth_ : null; // when the quad is wrapped in the source projection @@ -182,18 +181,18 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, var needsSubdivision = false; if (maxSubdiv > 0) { - if (this.targetProj_.isGlobal() && !goog.isNull(this.targetWorldWidth_)) { + if (this.targetProj_.isGlobal() && this.targetWorldWidth_) { var tgtQuadExtent = ol.extent.boundingExtent([a, b, c, d]); var tgtCoverageX = ol.extent.getWidth(tgtQuadExtent) / this.targetWorldWidth_; needsSubdivision |= tgtCoverageX > ol.RASTER_REPROJ_MAX_TRIANGLE_WIDTH; } - if (!wrapsX && this.sourceProj_.isGlobal() && !goog.isNull(srcCoverageX)) { + if (!wrapsX && this.sourceProj_.isGlobal() && srcCoverageX) { needsSubdivision |= srcCoverageX > ol.RASTER_REPROJ_MAX_TRIANGLE_WIDTH; } } - if (!needsSubdivision && !goog.isNull(this.maxSourceExtent_)) { + if (!needsSubdivision && this.maxSourceExtent_) { if (!ol.extent.intersects(srcQuadExtent, this.maxSourceExtent_)) { // whole quad outside source projection extent -> ignore return; @@ -220,7 +219,7 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, var dx; if (wrapsX) { - goog.asserts.assert(!goog.isNull(this.sourceWorldWidth_)); + goog.asserts.assert(this.sourceWorldWidth_); var centerSrcEstimX = (goog.math.modulo(aSrc[0], this.sourceWorldWidth_) + goog.math.modulo(cSrc[0], this.sourceWorldWidth_)) / 2; @@ -278,7 +277,7 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, * @return {ol.Extent} Calculated extent. */ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { - if (!goog.isNull(this.trianglesSourceExtent_)) { + if (this.trianglesSourceExtent_) { return this.trianglesSourceExtent_; } @@ -289,8 +288,8 @@ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { // all coordinates need to be "shifted" to be positive // to properly calculate the extent (and then possibly shifted back) - goog.array.forEach(this.triangles_, function(triangle, i, arr) { - goog.asserts.assert(!goog.isNull(this.sourceWorldWidth_)); + this.triangles_.forEach(function(triangle, i, arr) { + goog.asserts.assert(this.sourceWorldWidth_); var src = triangle.source; ol.extent.extendCoordinate(extent, [goog.math.modulo(src[0][0], this.sourceWorldWidth_), src[0][1]]); @@ -309,7 +308,7 @@ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { extent[2] -= this.sourceWorldWidth_; } } else { - goog.array.forEach(this.triangles_, function(triangle, i, arr) { + this.triangles_.forEach(function(triangle, i, arr) { var src = triangle.source; ol.extent.extendCoordinate(extent, src[0]); ol.extent.extendCoordinate(extent, src[1]); diff --git a/src/ol/source/imagesource.js b/src/ol/source/imagesource.js index e0ac8486a2..a0a4b4cb09 100644 --- a/src/ol/source/imagesource.js +++ b/src/ol/source/imagesource.js @@ -110,15 +110,15 @@ ol.source.Image.prototype.getImage = function(extent, resolution, pixelRatio, projection) { var sourceProjection = this.getProjection(); if (!ol.ENABLE_RASTER_REPROJECTION || - !goog.isDefAndNotNull(sourceProjection) || - !goog.isDefAndNotNull(projection) || + !sourceProjection || + !projection || ol.proj.equivalent(sourceProjection, projection)) { - if (!goog.isNull(sourceProjection)) { + if (sourceProjection) { projection = sourceProjection; } return this.getImageInternal(extent, resolution, pixelRatio, projection); } else { - if (!goog.isNull(this.reprojectedImage_)) { + if (this.reprojectedImage_) { if (this.reprojectedRevision_ == this.getRevision() && ol.proj.equivalent( this.reprojectedImage_.getProjection(), projection) && diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index e94df1793c..964e7d7690 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -152,8 +152,8 @@ ol.source.TileImage.prototype.getTileGridForProjection = function(projection) { return goog.base(this, 'getTileGridForProjection', projection); } var thisProj = this.getProjection(); - if (!goog.isNull(this.tileGrid) && - (goog.isNull(thisProj) || ol.proj.equivalent(thisProj, projection))) { + if (this.tileGrid && + (!thisProj || ol.proj.equivalent(thisProj, projection))) { return this.tileGrid; } else { var projKey = goog.getUid(projection).toString(); @@ -174,7 +174,7 @@ ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) { return goog.base(this, 'getTileCacheForProjection', projection); } var thisProj = this.getProjection(); - if (goog.isNull(thisProj) || ol.proj.equivalent(thisProj, projection)) { + if (!thisProj || ol.proj.equivalent(thisProj, projection)) { return this.tileCache; } else { var projKey = goog.getUid(projection).toString(); @@ -192,8 +192,8 @@ ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) { ol.source.TileImage.prototype.getTile = function(z, x, y, pixelRatio, projection) { if (!ol.ENABLE_RASTER_REPROJECTION || - !goog.isDefAndNotNull(this.getProjection()) || - !goog.isDefAndNotNull(projection) || + !this.getProjection() || + !projection || ol.proj.equivalent(this.getProjection(), projection)) { return this.getTileInternal(z, x, y, pixelRatio, projection); } else { @@ -335,7 +335,7 @@ ol.source.TileImage.prototype.setTileGridForProjection = function(projection, tilegrid) { if (ol.ENABLE_RASTER_REPROJECTION) { var proj = ol.proj.get(projection); - if (!goog.isNull(proj)) { + if (proj) { var projKey = goog.getUid(proj).toString(); if (!(projKey in this.tileGridForProjection)) { this.tileGridForProjection[projKey] = tilegrid; @@ -380,7 +380,7 @@ ol.source.TileImage.prototype.setTileUrlFunction = function(tileUrlFunction) { ol.source.TileImage.prototype.useTile = function(z, x, y, projection) { var tileCache = this.getTileCacheForProjection(projection); var tileCoordKey = this.getKeyZXY(z, x, y); - if (!goog.isNull(tileCache) && tileCache.containsKey(tileCoordKey)) { + if (tileCache && tileCache.containsKey(tileCoordKey)) { tileCache.get(tileCoordKey); } }; diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index cda201384c..e47afe0d3f 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -103,7 +103,7 @@ ol.source.Tile.prototype.canExpireCache = function() { */ ol.source.Tile.prototype.expireCache = function(projection, usedTiles) { var tileCache = this.getTileCacheForProjection(projection); - if (!goog.isNull(tileCache)) { + if (tileCache) { tileCache.expireCache(usedTiles); } }; @@ -121,7 +121,7 @@ ol.source.Tile.prototype.expireCache = function(projection, usedTiles) { ol.source.Tile.prototype.forEachLoadedTile = function(projection, z, tileRange, callback) { var tileCache = this.getTileCacheForProjection(projection); - if (goog.isNull(tileCache)) { + if (!tileCache) { return false; } @@ -222,7 +222,7 @@ ol.source.Tile.prototype.getTileGridForProjection = function(projection) { */ ol.source.Tile.prototype.getTileCacheForProjection = function(projection) { var thisProj = this.getProjection(); - if (!goog.isNull(thisProj) && !ol.proj.equivalent(thisProj, projection)) { + if (thisProj && !ol.proj.equivalent(thisProj, projection)) { return null; } else { return this.tileCache; From 9f8ab48f1f6ce7546e50609eda08e6d2030b4563 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 15 Oct 2015 10:27:12 +0200 Subject: [PATCH 70/80] Minor improvements based on pull request comments --- src/ol/reproj/tile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index cca4031593..36ae0d51ab 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -255,6 +255,7 @@ ol.reproj.Tile.prototype.reproject_ = function() { }); } }, this); + this.srcTiles_.length = 0; var tileCoord = this.getTileCoord(); var z = tileCoord[0]; @@ -305,7 +306,7 @@ ol.reproj.Tile.prototype.load = function() { leftToLoad--; goog.asserts.assert(leftToLoad >= 0, 'leftToLoad should not be negative'); - if (leftToLoad <= 0) { + if (leftToLoad === 0) { this.unlistenSources_(); this.reproject_(); } From f52f9b6817e3daa74cf45222ed304ad815d636ff Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 15 Oct 2015 10:41:36 +0200 Subject: [PATCH 71/80] Rename ol.RASTER_REPROJ_* constants to full ol.RASTER_REPROJECTION_* --- src/ol/ol.js | 10 +++++----- src/ol/reproj/image.js | 2 +- src/ol/reproj/tile.js | 5 +++-- src/ol/reproj/triangulation.js | 8 +++++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/ol/ol.js b/src/ol/ol.js index 0e441ae844..9a14b339eb 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -32,7 +32,7 @@ ol.DEFAULT_MIN_ZOOM = 0; * @define {number} Default maximum allowed threshold (in pixels) for * reprojection triangulation. Default is `0.5`. */ -ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD = 0.5; +ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD = 0.5; /** @@ -183,7 +183,7 @@ ol.OVERVIEWMAP_MIN_RATIO = 0.1; * If too many tiles are required, no tiles are loaded and * `ol.TileState.ERROR` state is set. Default is `100`. */ -ol.RASTER_REPROJ_MAX_SOURCE_TILES = 100; +ol.RASTER_REPROJECTION_MAX_SOURCE_TILES = 100; /** @@ -192,7 +192,7 @@ ol.RASTER_REPROJ_MAX_SOURCE_TILES = 100; * number of proj4 calls (for certain transformations and areas). * At most `2*(2^this)` triangles are created. Default is `10`. */ -ol.RASTER_REPROJ_MAX_SUBDIVISION = 10; +ol.RASTER_REPROJECTION_MAX_SUBDIVISION = 10; /** @@ -201,10 +201,10 @@ ol.RASTER_REPROJ_MAX_SUBDIVISION = 10; * the resulting triangulation seems to have zero error and no subdivision * is performed. * If the triangle width is more than this (relative to world width; 0-1), - * subdivison is forced (respecting `ol.RASTER_REPROJ_MAX_SUBDIVISION`). + * subdivison is forced (up to `ol.RASTER_REPROJECTION_MAX_SUBDIVISION`). * Default is `0.25`. */ -ol.RASTER_REPROJ_MAX_TRIANGLE_WIDTH = 0.25; +ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH = 0.25; /** diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index a4abd4760b..b1ce6e9c71 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -50,7 +50,7 @@ ol.reproj.Image = function(sourceProj, targetProj, var sourceResolution = ol.reproj.calculateSourceResolution( sourceProj, targetProj, targetCenter, targetResolution); - var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; + var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD; /** * @private diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index 36ae0d51ab..5e1c1ebf6b 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -132,7 +132,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, } var errorThresholdInPixels = opt_errorThreshold !== undefined ? - opt_errorThreshold : ol.DEFAULT_RASTER_REPROJ_ERROR_THRESHOLD; + opt_errorThreshold : ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD; /** * @private @@ -187,7 +187,8 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, } var tilesRequired = xRange.length * srcRange.getHeight(); - if (!goog.asserts.assert(tilesRequired < ol.RASTER_REPROJ_MAX_SOURCE_TILES, + if (!goog.asserts.assert( + tilesRequired < ol.RASTER_REPROJECTION_MAX_SOURCE_TILES, 'reasonable number of tiles is required')) { this.state = ol.TileState.ERROR; return; diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index dc95b77b6d..009ee84bd0 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -125,7 +125,7 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, this.addQuad_(tlDst, trDst, brDst, blDst, tlDstSrc, trDstSrc, brDstSrc, blDstSrc, - ol.RASTER_REPROJ_MAX_SUBDIVISION); + ol.RASTER_REPROJECTION_MAX_SUBDIVISION); transformInvCache = {}; }; @@ -185,10 +185,12 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, var tgtQuadExtent = ol.extent.boundingExtent([a, b, c, d]); var tgtCoverageX = ol.extent.getWidth(tgtQuadExtent) / this.targetWorldWidth_; - needsSubdivision |= tgtCoverageX > ol.RASTER_REPROJ_MAX_TRIANGLE_WIDTH; + needsSubdivision |= + tgtCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH; } if (!wrapsX && this.sourceProj_.isGlobal() && srcCoverageX) { - needsSubdivision |= srcCoverageX > ol.RASTER_REPROJ_MAX_TRIANGLE_WIDTH; + needsSubdivision |= + srcCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH; } } From 52a7c5e5820b77f42ec22e2ab777cc9a43af7a8b Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Thu, 15 Oct 2015 11:25:18 +0200 Subject: [PATCH 72/80] Rename some private/local variables to increase readability --- src/ol/reproj/image.js | 44 +++++++------- src/ol/reproj/reproj.js | 106 ++++++++++++++++----------------- src/ol/reproj/tile.js | 54 ++++++++--------- src/ol/reproj/triangulation.js | 68 +++++++++++---------- 4 files changed, 140 insertions(+), 132 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index b1ce6e9c71..88c699a62a 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -72,19 +72,21 @@ ol.reproj.Image = function(sourceProj, targetProj, */ this.targetExtent_ = targetExtent; - var srcExtent = this.triangulation_.calculateSourceExtent(); + var sourceExtent = this.triangulation_.calculateSourceExtent(); /** * @private * @type {ol.ImageBase} */ - this.srcImage_ = getImageFunction(srcExtent, sourceResolution, pixelRatio); + this.sourceImage_ = + getImageFunction(sourceExtent, sourceResolution, pixelRatio); /** * @private * @type {number} */ - this.srcPixelRatio_ = this.srcImage_ ? this.srcImage_.getPixelRatio() : 1; + this.sourcePixelRatio_ = + this.sourceImage_ ? this.sourceImage_.getPixelRatio() : 1; /** * @private @@ -102,12 +104,12 @@ ol.reproj.Image = function(sourceProj, targetProj, var state = ol.ImageState.LOADED; var attributions = []; - if (this.srcImage_) { + if (this.sourceImage_) { state = ol.ImageState.IDLE; - attributions = this.srcImage_.getAttributions(); + attributions = this.sourceImage_.getAttributions(); } - goog.base(this, targetExtent, targetResolution, this.srcPixelRatio_, + goog.base(this, targetExtent, targetResolution, this.sourcePixelRatio_, state, attributions); }; goog.inherits(ol.reproj.Image, ol.ImageBase); @@ -144,20 +146,20 @@ ol.reproj.Image.prototype.getProjection = function() { * @private */ ol.reproj.Image.prototype.reproject_ = function() { - var srcState = this.srcImage_.getState(); - if (srcState == ol.ImageState.LOADED) { + var sourceState = this.sourceImage_.getState(); + if (sourceState == ol.ImageState.LOADED) { var width = ol.extent.getWidth(this.targetExtent_) / this.targetResolution_; var height = ol.extent.getHeight(this.targetExtent_) / this.targetResolution_; - this.canvas_ = ol.reproj.render(width, height, this.srcPixelRatio_, - this.srcImage_.getResolution(), this.maxSourceExtent_, + this.canvas_ = ol.reproj.render(width, height, this.sourcePixelRatio_, + this.sourceImage_.getResolution(), this.maxSourceExtent_, this.targetResolution_, this.targetExtent_, this.triangulation_, [{ - extent: this.srcImage_.getExtent(), - image: this.srcImage_.getImage() + extent: this.sourceImage_.getExtent(), + image: this.sourceImage_.getImage() }]); } - this.state = srcState; + this.state = sourceState; this.changed(); }; @@ -170,21 +172,21 @@ ol.reproj.Image.prototype.load = function() { this.state = ol.ImageState.LOADING; this.changed(); - var srcState = this.srcImage_.getState(); - if (srcState == ol.ImageState.LOADED || - srcState == ol.ImageState.ERROR) { + var sourceState = this.sourceImage_.getState(); + if (sourceState == ol.ImageState.LOADED || + sourceState == ol.ImageState.ERROR) { this.reproject_(); } else { - this.sourceListenerKey_ = this.srcImage_.listen( + this.sourceListenerKey_ = this.sourceImage_.listen( goog.events.EventType.CHANGE, function(e) { - var srcState = this.srcImage_.getState(); - if (srcState == ol.ImageState.LOADED || - srcState == ol.ImageState.ERROR) { + var sourceState = this.sourceImage_.getState(); + if (sourceState == ol.ImageState.LOADED || + sourceState == ol.ImageState.ERROR) { this.unlistenSource_(); this.reproject_(); } }, false, this); - this.srcImage_.load(); + this.sourceImage_.load(); } } }; diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 4de2495b0d..95856191f1 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -44,13 +44,13 @@ ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, var sourceResolution = targetProj.getPointResolution(targetResolution, targetCenter); - var targetMPU = targetProj.getMetersPerUnit(); - if (targetMPU !== undefined) { - sourceResolution *= targetMPU; + var targetMetersPerUnit = targetProj.getMetersPerUnit(); + if (targetMetersPerUnit !== undefined) { + sourceResolution *= targetMetersPerUnit; } - var sourceMPU = sourceProj.getMetersPerUnit(); - if (sourceMPU !== undefined) { - sourceResolution /= sourceMPU; + var sourceMetersPerUnit = sourceProj.getMetersPerUnit(); + if (sourceMetersPerUnit !== undefined) { + sourceResolution /= sourceMetersPerUnit; } // Based on the projection properties, the point resolution at the specified @@ -134,46 +134,45 @@ ol.reproj.render = function(width, height, pixelRatio, } } - var srcDataExtent = ol.extent.createEmpty(); + var sourceDataExtent = ol.extent.createEmpty(); sources.forEach(function(src, i, arr) { if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) { var srcW = src.extent[2] - src.extent[0]; var srcX = goog.math.modulo(src.extent[0], wrapXShiftDistance); - ol.extent.extend(srcDataExtent, [srcX, src.extent[1], - srcX + srcW, src.extent[3]]); + ol.extent.extend(sourceDataExtent, [srcX, src.extent[1], + srcX + srcW, src.extent[3]]); } else { - ol.extent.extend(srcDataExtent, src.extent); + ol.extent.extend(sourceDataExtent, src.extent); } }); if (sourceExtent) { if (wrapXType == ol.reproj.WrapXRendering_.NONE) { - srcDataExtent[0] = ol.math.clamp( - srcDataExtent[0], sourceExtent[0], sourceExtent[2]); - srcDataExtent[2] = ol.math.clamp( - srcDataExtent[2], sourceExtent[0], sourceExtent[2]); + sourceDataExtent[0] = ol.math.clamp( + sourceDataExtent[0], sourceExtent[0], sourceExtent[2]); + sourceDataExtent[2] = ol.math.clamp( + sourceDataExtent[2], sourceExtent[0], sourceExtent[2]); } - srcDataExtent[1] = ol.math.clamp( - srcDataExtent[1], sourceExtent[1], sourceExtent[3]); - srcDataExtent[3] = ol.math.clamp( - srcDataExtent[3], sourceExtent[1], sourceExtent[3]); + sourceDataExtent[1] = ol.math.clamp( + sourceDataExtent[1], sourceExtent[1], sourceExtent[3]); + sourceDataExtent[3] = ol.math.clamp( + sourceDataExtent[3], sourceExtent[1], sourceExtent[3]); } - var srcDataWidth = ol.extent.getWidth(srcDataExtent); - var srcDataHeight = ol.extent.getHeight(srcDataExtent); var canvasWidthInUnits; if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) { canvasWidthInUnits = 2 * wrapXShiftDistance; } else { - canvasWidthInUnits = srcDataWidth; + canvasWidthInUnits = ol.extent.getWidth(sourceDataExtent); } + var canvasHeightInUnits = ol.extent.getHeight(sourceDataExtent); var stitchContext = ol.dom.createCanvasContext2D( Math.round(pixelRatio * canvasWidthInUnits / sourceResolution), - Math.round(pixelRatio * srcDataHeight / sourceResolution)); + Math.round(pixelRatio * canvasHeightInUnits / sourceResolution)); stitchContext.scale(pixelRatio / sourceResolution, pixelRatio / sourceResolution); - stitchContext.translate(-srcDataExtent[0], srcDataExtent[3]); + stitchContext.translate(-sourceDataExtent[0], sourceDataExtent[3]); sources.forEach(function(src, i, arr) { var xPos = src.extent[0]; @@ -192,9 +191,9 @@ ol.reproj.render = function(width, height, pixelRatio, } }); - var targetTL = ol.extent.getTopLeft(targetExtent); + var targetTopLeft = ol.extent.getTopLeft(targetExtent); - triangulation.getTriangles().forEach(function(tri, i, arr) { + triangulation.getTriangles().forEach(function(triangle, i, arr) { context.save(); /* Calculate affine transform (src -> dst) @@ -217,16 +216,16 @@ ol.reproj.render = function(width, height, pixelRatio, * | 0 0 0 x1 y1 1 | |a11| |v1| * | 0 0 0 x2 y2 1 | |a12| |v2| */ - var src = tri.source, tgt = tri.target; - var x0 = src[0][0], y0 = src[0][1], - x1 = src[1][0], y1 = src[1][1], - x2 = src[2][0], y2 = src[2][1]; - var u0 = (tgt[0][0] - targetTL[0]) / targetResolution, - v0 = -(tgt[0][1] - targetTL[1]) / targetResolution; - var u1 = (tgt[1][0] - targetTL[0]) / targetResolution, - v1 = -(tgt[1][1] - targetTL[1]) / targetResolution; - var u2 = (tgt[2][0] - targetTL[0]) / targetResolution, - v2 = -(tgt[2][1] - targetTL[1]) / targetResolution; + var source = triangle.source, target = triangle.target; + var x0 = source[0][0], y0 = source[0][1], + x1 = source[1][0], y1 = source[1][1], + x2 = source[2][0], y2 = source[2][1]; + var u0 = (target[0][0] - targetTopLeft[0]) / targetResolution, + v0 = -(target[0][1] - targetTopLeft[1]) / targetResolution; + var u1 = (target[1][0] - targetTopLeft[0]) / targetResolution, + v1 = -(target[1][1] - targetTopLeft[1]) / targetResolution; + var u2 = (target[2][0] - targetTopLeft[0]) / targetResolution, + v2 = -(target[2][1] - targetTopLeft[1]) / targetResolution; var performWrapXShift = wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT; if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) { @@ -246,13 +245,13 @@ ol.reproj.render = function(width, height, pixelRatio, // Shift all the source points to improve numerical stability // of all the subsequent calculations. The [x0, y0] is used here. // This is also used to simplify the linear system. - var srcNumericalShiftX = x0, srcNumericalShiftY = y0; + var sourceNumericalShiftX = x0, sourceNumericalShiftY = y0; x0 = 0; y0 = 0; - x1 -= srcNumericalShiftX; - y1 -= srcNumericalShiftY; - x2 -= srcNumericalShiftX; - y2 -= srcNumericalShiftY; + x1 -= sourceNumericalShiftX; + y1 -= sourceNumericalShiftY; + x2 -= sourceNumericalShiftX; + y2 -= sourceNumericalShiftY; var augmentedMatrix = [ [x1, y1, 0, 0, u1 - u0], @@ -260,8 +259,8 @@ ol.reproj.render = function(width, height, pixelRatio, [0, 0, x1, y1, v1 - v0], [0, 0, x2, y2, v2 - v0] ]; - var coefs = ol.math.solveLinearSystem(augmentedMatrix); - if (!coefs) { + var affineCoefs = ol.math.solveLinearSystem(augmentedMatrix); + if (!affineCoefs) { return; } @@ -283,10 +282,11 @@ ol.reproj.render = function(width, height, pixelRatio, context.closePath(); context.clip(); - context.transform(coefs[0], coefs[2], coefs[1], coefs[3], u0, v0); + context.transform( + affineCoefs[0], affineCoefs[2], affineCoefs[1], affineCoefs[3], u0, v0); - context.translate(srcDataExtent[0] - srcNumericalShiftX, - srcDataExtent[3] - srcNumericalShiftY); + context.translate(sourceDataExtent[0] - sourceNumericalShiftX, + sourceDataExtent[3] - sourceNumericalShiftY); context.scale(sourceResolution / pixelRatio, -sourceResolution / pixelRatio); @@ -301,14 +301,14 @@ ol.reproj.render = function(width, height, pixelRatio, context.strokeStyle = 'black'; context.lineWidth = 1; - triangulation.getTriangles().forEach(function(tri, i, arr) { - var tgt = tri.target; - var u0 = (tgt[0][0] - targetTL[0]) / targetResolution, - v0 = -(tgt[0][1] - targetTL[1]) / targetResolution; - var u1 = (tgt[1][0] - targetTL[0]) / targetResolution, - v1 = -(tgt[1][1] - targetTL[1]) / targetResolution; - var u2 = (tgt[2][0] - targetTL[0]) / targetResolution, - v2 = -(tgt[2][1] - targetTL[1]) / targetResolution; + triangulation.getTriangles().forEach(function(triangle, i, arr) { + var target = triangle.target; + var u0 = (target[0][0] - targetTopLeft[0]) / targetResolution, + v0 = -(target[0][1] - targetTopLeft[1]) / targetResolution; + var u1 = (target[1][0] - targetTopLeft[0]) / targetResolution, + v1 = -(target[1][1] - targetTopLeft[1]) / targetResolution; + var u2 = (target[2][0] - targetTopLeft[0]) / targetResolution, + v2 = -(target[2][1] - targetTopLeft[1]) / targetResolution; context.beginPath(); context.moveTo(u0, v0); diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index 5e1c1ebf6b..cc20abc8db 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -80,7 +80,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, * @private * @type {!Array.} */ - this.srcTiles_ = []; + this.sourceTiles_ = []; /** * @private @@ -92,7 +92,7 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, * @private * @type {number} */ - this.srcZ_ = 0; + this.sourceZ_ = 0; var targetExtent = targetTileGrid.getTileCoordExtent(this.getTileCoord()); var maxTargetExtent = this.targetTileGrid_.getExtent(); @@ -148,45 +148,45 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, return; } - this.srcZ_ = sourceTileGrid.getZForResolution(sourceResolution); - var srcExtent = this.triangulation_.calculateSourceExtent(); + this.sourceZ_ = sourceTileGrid.getZForResolution(sourceResolution); + var sourceExtent = this.triangulation_.calculateSourceExtent(); if (maxSourceExtent && !this.triangulation_.getWrapsXInSource() && - !ol.extent.intersects(maxSourceExtent, srcExtent)) { + !ol.extent.intersects(maxSourceExtent, sourceExtent)) { this.state = ol.TileState.EMPTY; } else { - var srcRange = sourceTileGrid.getTileRangeForExtentAndZ( - srcExtent, this.srcZ_); + var sourceRange = sourceTileGrid.getTileRangeForExtentAndZ( + sourceExtent, this.sourceZ_); var xRange = []; - var srcFullRange = sourceTileGrid.getFullTileRange(this.srcZ_); - if (srcFullRange) { - srcRange.minY = Math.max(srcRange.minY, srcFullRange.minY); - srcRange.maxY = Math.min(srcRange.maxY, srcFullRange.maxY); + var sourceFullRange = sourceTileGrid.getFullTileRange(this.sourceZ_); + if (sourceFullRange) { + sourceRange.minY = Math.max(sourceRange.minY, sourceFullRange.minY); + sourceRange.maxY = Math.min(sourceRange.maxY, sourceFullRange.maxY); - if (srcRange.minX > srcRange.maxX) { + if (sourceRange.minX > sourceRange.maxX) { var i; - for (i = srcRange.minX; i <= srcFullRange.maxX; i++) { + for (i = sourceRange.minX; i <= sourceFullRange.maxX; i++) { xRange.push(i); } - for (i = srcFullRange.minX; i <= srcRange.maxX; i++) { + for (i = sourceFullRange.minX; i <= sourceRange.maxX; i++) { xRange.push(i); } } else { - var first = Math.max(srcRange.minX, srcFullRange.minX); - var last = Math.min(srcRange.maxX, srcFullRange.maxX); + var first = Math.max(sourceRange.minX, sourceFullRange.minX); + var last = Math.min(sourceRange.maxX, sourceFullRange.maxX); for (var j = first; j <= last; j++) { xRange.push(j); } } } else { - for (var k = srcRange.minX; k <= srcRange.maxX; k++) { + for (var k = sourceRange.minX; k <= sourceRange.maxX; k++) { xRange.push(k); } } - var tilesRequired = xRange.length * srcRange.getHeight(); + var tilesRequired = xRange.length * sourceRange.getHeight(); if (!goog.asserts.assert( tilesRequired < ol.RASTER_REPROJECTION_MAX_SOURCE_TILES, 'reasonable number of tiles is required')) { @@ -194,15 +194,15 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, return; } xRange.forEach(function(srcX, i, arr) { - for (var srcY = srcRange.minY; srcY <= srcRange.maxY; srcY++) { - var tile = getTileFunction(this.srcZ_, srcX, srcY, pixelRatio); + for (var srcY = sourceRange.minY; srcY <= sourceRange.maxY; srcY++) { + var tile = getTileFunction(this.sourceZ_, srcX, srcY, pixelRatio); if (tile) { - this.srcTiles_.push(tile); + this.sourceTiles_.push(tile); } } }, this); - if (this.srcTiles_.length === 0) { + if (this.sourceTiles_.length === 0) { this.state = ol.TileState.EMPTY; } } @@ -248,7 +248,7 @@ ol.reproj.Tile.prototype.getImage = function(opt_context) { */ ol.reproj.Tile.prototype.reproject_ = function() { var sources = []; - this.srcTiles_.forEach(function(tile, i, arr) { + this.sourceTiles_.forEach(function(tile, i, arr) { if (tile && tile.getState() == ol.TileState.LOADED) { sources.push({ extent: this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord), @@ -256,7 +256,7 @@ ol.reproj.Tile.prototype.reproject_ = function() { }); } }, this); - this.srcTiles_.length = 0; + this.sourceTiles_.length = 0; var tileCoord = this.getTileCoord(); var z = tileCoord[0]; @@ -264,7 +264,7 @@ ol.reproj.Tile.prototype.reproject_ = function() { var width = goog.isNumber(size) ? size : size[0]; var height = goog.isNumber(size) ? size : size[1]; var targetResolution = this.targetTileGrid_.getResolution(z); - var sourceResolution = this.sourceTileGrid_.getResolution(this.srcZ_); + var sourceResolution = this.sourceTileGrid_.getResolution(this.sourceZ_); var targetExtent = this.targetTileGrid_.getTileCoordExtent(tileCoord); this.canvas_ = ol.reproj.render(width, height, this.pixelRatio_, @@ -291,7 +291,7 @@ ol.reproj.Tile.prototype.load = function() { 'this.sourcesListenerKeys_ should be null'); this.sourcesListenerKeys_ = []; - this.srcTiles_.forEach(function(tile, i, arr) { + this.sourceTiles_.forEach(function(tile, i, arr) { var state = tile.getState(); if (state == ol.TileState.IDLE || state == ol.TileState.LOADING) { leftToLoad++; @@ -317,7 +317,7 @@ ol.reproj.Tile.prototype.load = function() { } }, this); - this.srcTiles_.forEach(function(tile, i, arr) { + this.sourceTiles_.forEach(function(tile, i, arr) { var state = tile.getState(); if (state == ol.TileState.IDLE) { tile.load(); diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 009ee84bd0..1f0d22662b 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -114,18 +114,20 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, this.targetWorldWidth_ = this.targetProj_.getExtent() ? ol.extent.getWidth(this.targetProj_.getExtent()) : null; - var tlDst = ol.extent.getTopLeft(targetExtent); - var trDst = ol.extent.getTopRight(targetExtent); - var brDst = ol.extent.getBottomRight(targetExtent); - var blDst = ol.extent.getBottomLeft(targetExtent); - var tlDstSrc = this.transformInv_(tlDst); - var trDstSrc = this.transformInv_(trDst); - var brDstSrc = this.transformInv_(brDst); - var blDstSrc = this.transformInv_(blDst); + var destinationTopLeft = ol.extent.getTopLeft(targetExtent); + var destinationTopRight = ol.extent.getTopRight(targetExtent); + var destinationBottomRight = ol.extent.getBottomRight(targetExtent); + var destinationBottomLeft = ol.extent.getBottomLeft(targetExtent); + var sourceTopLeft = this.transformInv_(destinationTopLeft); + var sourceTopRight = this.transformInv_(destinationTopRight); + var sourceBottomRight = this.transformInv_(destinationBottomRight); + var sourceBottomLeft = this.transformInv_(destinationBottomLeft); - this.addQuad_(tlDst, trDst, brDst, blDst, - tlDstSrc, trDstSrc, brDstSrc, blDstSrc, - ol.RASTER_REPROJECTION_MAX_SUBDIVISION); + this.addQuad_( + destinationTopLeft, destinationTopRight, + destinationBottomRight, destinationBottomLeft, + sourceTopLeft, sourceTopRight, sourceBottomRight, sourceBottomLeft, + ol.RASTER_REPROJECTION_MAX_SUBDIVISION); transformInvCache = {}; }; @@ -163,39 +165,39 @@ ol.reproj.Triangulation.prototype.addTriangle_ = function(a, b, c, * @param {ol.Coordinate} bSrc * @param {ol.Coordinate} cSrc * @param {ol.Coordinate} dSrc - * @param {number} maxSubdiv Maximal allowed subdivision of the quad. + * @param {number} maxSubdivision Maximal allowed subdivision of the quad. * @private */ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, - aSrc, bSrc, cSrc, dSrc, maxSubdiv) { + aSrc, bSrc, cSrc, dSrc, maxSubdivision) { - var srcQuadExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc, dSrc]); - var srcCoverageX = this.sourceWorldWidth_ ? - ol.extent.getWidth(srcQuadExtent) / this.sourceWorldWidth_ : null; + var sourceQuadExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc, dSrc]); + var sourceCoverageX = this.sourceWorldWidth_ ? + ol.extent.getWidth(sourceQuadExtent) / this.sourceWorldWidth_ : null; // when the quad is wrapped in the source projection // it covers most of the projection extent, but not fully var wrapsX = this.sourceProj_.canWrapX() && - srcCoverageX > 0.5 && srcCoverageX < 1; + sourceCoverageX > 0.5 && sourceCoverageX < 1; var needsSubdivision = false; - if (maxSubdiv > 0) { + if (maxSubdivision > 0) { if (this.targetProj_.isGlobal() && this.targetWorldWidth_) { - var tgtQuadExtent = ol.extent.boundingExtent([a, b, c, d]); - var tgtCoverageX = - ol.extent.getWidth(tgtQuadExtent) / this.targetWorldWidth_; + var targetQuadExtent = ol.extent.boundingExtent([a, b, c, d]); + var targetCoverageX = + ol.extent.getWidth(targetQuadExtent) / this.targetWorldWidth_; needsSubdivision |= - tgtCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH; + targetCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH; } - if (!wrapsX && this.sourceProj_.isGlobal() && srcCoverageX) { + if (!wrapsX && this.sourceProj_.isGlobal() && sourceCoverageX) { needsSubdivision |= - srcCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH; + sourceCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH; } } if (!needsSubdivision && this.maxSourceExtent_) { - if (!ol.extent.intersects(srcQuadExtent, this.maxSourceExtent_)) { + if (!ol.extent.intersects(sourceQuadExtent, this.maxSourceExtent_)) { // whole quad outside source projection extent -> ignore return; } @@ -206,7 +208,7 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, !isFinite(bSrc[0]) || !isFinite(bSrc[1]) || !isFinite(cSrc[0]) || !isFinite(cSrc[1]) || !isFinite(dSrc[0]) || !isFinite(dSrc[1])) { - if (maxSubdiv > 0) { + if (maxSubdivision > 0) { needsSubdivision = true; } else { return; @@ -214,7 +216,7 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, } } - if (maxSubdiv > 0) { + if (maxSubdivision > 0) { if (!needsSubdivision) { var center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2]; var centerSrc = this.transformInv_(center); @@ -242,8 +244,10 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, var da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2]; var daSrc = this.transformInv_(da); - this.addQuad_(a, b, bc, da, aSrc, bSrc, bcSrc, daSrc, maxSubdiv - 1); - this.addQuad_(da, bc, c, d, daSrc, bcSrc, cSrc, dSrc, maxSubdiv - 1); + this.addQuad_( + a, b, bc, da, aSrc, bSrc, bcSrc, daSrc, maxSubdivision - 1); + this.addQuad_( + da, bc, c, d, daSrc, bcSrc, cSrc, dSrc, maxSubdivision - 1); } else { // split vertically (left & right) var ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; @@ -251,8 +255,10 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, var cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2]; var cdSrc = this.transformInv_(cd); - this.addQuad_(a, ab, cd, d, aSrc, abSrc, cdSrc, dSrc, maxSubdiv - 1); - this.addQuad_(ab, b, c, cd, abSrc, bSrc, cSrc, cdSrc, maxSubdiv - 1); + this.addQuad_( + a, ab, cd, d, aSrc, abSrc, cdSrc, dSrc, maxSubdivision - 1); + this.addQuad_( + ab, b, c, cd, abSrc, bSrc, cSrc, cdSrc, maxSubdivision - 1); } return; } From a7cde960566d15fa902067543f1d675393964bfd Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 16 Oct 2015 09:18:55 +0200 Subject: [PATCH 73/80] Simplified wrapX handling "Unwrap" the coordinates obtained from transformations and utilize wrapX capabilities of the sources to handle calculations of TileRanges and unwrapped tile extents. --- src/ol/reproj/reproj.js | 82 +------------------------- src/ol/reproj/tile.js | 49 +++++----------- src/ol/reproj/triangulation.js | 104 ++++++++++++++++----------------- 3 files changed, 67 insertions(+), 168 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 95856191f1..ed4e4a6940 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -116,55 +116,12 @@ ol.reproj.render = function(width, height, pixelRatio, context.scale(pixelRatio, pixelRatio); - var wrapXShiftDistance = sourceExtent ? ol.extent.getWidth(sourceExtent) : 0; - - var wrapXType = ol.reproj.WrapXRendering_.NONE; - - if (triangulation.getWrapsXInSource() && wrapXShiftDistance > 0) { - // If possible, stitch the sources shifted to solve the wrapX issue here. - // This is not possible if crossing both "dateline" and "prime meridian". - var triangulationSrcExtent = triangulation.calculateSourceExtent(); - var triangulationSrcWidth = goog.math.modulo( - ol.extent.getWidth(triangulationSrcExtent), wrapXShiftDistance); - - if (triangulationSrcWidth < wrapXShiftDistance / 2) { - wrapXType = ol.reproj.WrapXRendering_.STITCH_SHIFT; - } else { - wrapXType = ol.reproj.WrapXRendering_.STITCH_EXTENDED; - } - } - var sourceDataExtent = ol.extent.createEmpty(); sources.forEach(function(src, i, arr) { - if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) { - var srcW = src.extent[2] - src.extent[0]; - var srcX = goog.math.modulo(src.extent[0], wrapXShiftDistance); - ol.extent.extend(sourceDataExtent, [srcX, src.extent[1], - srcX + srcW, src.extent[3]]); - } else { - ol.extent.extend(sourceDataExtent, src.extent); - } + ol.extent.extend(sourceDataExtent, src.extent); }); - if (sourceExtent) { - if (wrapXType == ol.reproj.WrapXRendering_.NONE) { - sourceDataExtent[0] = ol.math.clamp( - sourceDataExtent[0], sourceExtent[0], sourceExtent[2]); - sourceDataExtent[2] = ol.math.clamp( - sourceDataExtent[2], sourceExtent[0], sourceExtent[2]); - } - sourceDataExtent[1] = ol.math.clamp( - sourceDataExtent[1], sourceExtent[1], sourceExtent[3]); - sourceDataExtent[3] = ol.math.clamp( - sourceDataExtent[3], sourceExtent[1], sourceExtent[3]); - } - - var canvasWidthInUnits; - if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) { - canvasWidthInUnits = 2 * wrapXShiftDistance; - } else { - canvasWidthInUnits = ol.extent.getWidth(sourceDataExtent); - } + var canvasWidthInUnits = ol.extent.getWidth(sourceDataExtent); var canvasHeightInUnits = ol.extent.getHeight(sourceDataExtent); var stitchContext = ol.dom.createCanvasContext2D( Math.round(pixelRatio * canvasWidthInUnits / sourceResolution), @@ -180,15 +137,7 @@ ol.reproj.render = function(width, height, pixelRatio, var srcWidth = ol.extent.getWidth(src.extent); var srcHeight = ol.extent.getHeight(src.extent); - if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) { - xPos = goog.math.modulo(xPos, wrapXShiftDistance); - } stitchContext.drawImage(src.image, xPos, yPos, srcWidth, srcHeight); - - if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) { - stitchContext.drawImage(src.image, wrapXShiftDistance + xPos, yPos, - srcWidth, srcHeight); - } }); var targetTopLeft = ol.extent.getTopLeft(targetExtent); @@ -227,21 +176,6 @@ ol.reproj.render = function(width, height, pixelRatio, var u2 = (target[2][0] - targetTopLeft[0]) / targetResolution, v2 = -(target[2][1] - targetTopLeft[1]) / targetResolution; - var performWrapXShift = wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT; - if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) { - var minX = Math.min(x0, x1, x2); - var maxX = Math.max(x0, x1, x2); - - performWrapXShift = minX <= sourceExtent[0] || - (maxX - minX) > wrapXShiftDistance / 2; - } - - if (performWrapXShift) { - x0 = goog.math.modulo(x0, wrapXShiftDistance); - x1 = goog.math.modulo(x1, wrapXShiftDistance); - x2 = goog.math.modulo(x2, wrapXShiftDistance); - } - // Shift all the source points to improve numerical stability // of all the subsequent calculations. The [x0, y0] is used here. // This is also used to simplify the linear system. @@ -322,15 +256,3 @@ ol.reproj.render = function(width, height, pixelRatio, } return context.canvas; }; - - -/** - * Type of solution used to solve wrapX in source during rendering. - * @enum {number} - * @private - */ -ol.reproj.WrapXRendering_ = { - NONE: 0, - STITCH_SHIFT: 1, - STITCH_EXTENDED: 2 -}; diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index cc20abc8db..7e5192e525 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -8,6 +8,7 @@ goog.require('goog.object'); goog.require('ol.Tile'); goog.require('ol.TileState'); goog.require('ol.extent'); +goog.require('ol.math'); goog.require('ol.proj'); goog.require('ol.reproj'); goog.require('ol.reproj.Triangulation'); @@ -151,56 +152,38 @@ ol.reproj.Tile = function(sourceProj, sourceTileGrid, this.sourceZ_ = sourceTileGrid.getZForResolution(sourceResolution); var sourceExtent = this.triangulation_.calculateSourceExtent(); - if (maxSourceExtent && - !this.triangulation_.getWrapsXInSource() && - !ol.extent.intersects(maxSourceExtent, sourceExtent)) { + if (maxSourceExtent) { + if (sourceProj.canWrapX()) { + sourceExtent[1] = ol.math.clamp( + sourceExtent[1], maxSourceExtent[1], maxSourceExtent[3]); + sourceExtent[3] = ol.math.clamp( + sourceExtent[3], maxSourceExtent[1], maxSourceExtent[3]); + } else { + sourceExtent = ol.extent.getIntersection(sourceExtent, maxSourceExtent); + } + } + + if (!ol.extent.getArea(sourceExtent)) { this.state = ol.TileState.EMPTY; } else { var sourceRange = sourceTileGrid.getTileRangeForExtentAndZ( sourceExtent, this.sourceZ_); - var xRange = []; - var sourceFullRange = sourceTileGrid.getFullTileRange(this.sourceZ_); - if (sourceFullRange) { - sourceRange.minY = Math.max(sourceRange.minY, sourceFullRange.minY); - sourceRange.maxY = Math.min(sourceRange.maxY, sourceFullRange.maxY); - - if (sourceRange.minX > sourceRange.maxX) { - var i; - for (i = sourceRange.minX; i <= sourceFullRange.maxX; i++) { - xRange.push(i); - } - for (i = sourceFullRange.minX; i <= sourceRange.maxX; i++) { - xRange.push(i); - } - } else { - var first = Math.max(sourceRange.minX, sourceFullRange.minX); - var last = Math.min(sourceRange.maxX, sourceFullRange.maxX); - for (var j = first; j <= last; j++) { - xRange.push(j); - } - } - } else { - for (var k = sourceRange.minX; k <= sourceRange.maxX; k++) { - xRange.push(k); - } - } - - var tilesRequired = xRange.length * sourceRange.getHeight(); + var tilesRequired = sourceRange.getWidth() * sourceRange.getHeight(); if (!goog.asserts.assert( tilesRequired < ol.RASTER_REPROJECTION_MAX_SOURCE_TILES, 'reasonable number of tiles is required')) { this.state = ol.TileState.ERROR; return; } - xRange.forEach(function(srcX, i, arr) { + for (var srcX = sourceRange.minX; srcX <= sourceRange.maxX; srcX++) { for (var srcY = sourceRange.minY; srcY <= sourceRange.maxY; srcY++) { var tile = getTileFunction(this.sourceZ_, srcX, srcY, pixelRatio); if (tile) { this.sourceTiles_.push(tile); } } - }, this); + } if (this.sourceTiles_.length === 0) { this.state = ol.TileState.EMPTY; diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index 1f0d22662b..b69006fb01 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -75,12 +75,6 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, */ this.triangles_ = []; - /** - * @type {ol.Extent} - * @private - */ - this.trianglesSourceExtent_ = null; - /** * Indicates that source coordinates have to be shifted during reprojection. * This is needed when the triangulation crosses @@ -129,6 +123,49 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, sourceTopLeft, sourceTopRight, sourceBottomRight, sourceBottomLeft, ol.RASTER_REPROJECTION_MAX_SUBDIVISION); + if (this.wrapsXInSource_) { + // Fix coordinates (ol.proj returns wrapped coordinates, "unwrap" here). + // This significantly simplifies the rest of the reprojection process. + + goog.asserts.assert(this.sourceWorldWidth_ !== null); + var leftBound = Infinity; + this.triangles_.forEach(function(triangle, i, arr) { + leftBound = Math.min(leftBound, + triangle.source[0][0], triangle.source[1][0], triangle.source[2][0]); + }); + + // Shift triangles to be as close to `leftBound` as possible + // (if the distance is more than `worldWidth / 2` it can be closer. + this.triangles_.forEach(function(triangle) { + if (Math.max(triangle.source[0][0], triangle.source[1][0], + triangle.source[2][0]) - leftBound > this.sourceWorldWidth_ / 2) { + var newTriangle = [[triangle.source[0][0], triangle.source[0][1]], + [triangle.source[1][0], triangle.source[1][1]], + [triangle.source[2][0], triangle.source[2][1]]]; + if ((newTriangle[0][0] - leftBound) > this.sourceWorldWidth_ / 2) { + newTriangle[0][0] -= this.sourceWorldWidth_; + } + if ((newTriangle[1][0] - leftBound) > this.sourceWorldWidth_ / 2) { + newTriangle[1][0] -= this.sourceWorldWidth_; + } + if ((newTriangle[2][0] - leftBound) > this.sourceWorldWidth_ / 2) { + newTriangle[2][0] -= this.sourceWorldWidth_; + } + + // Rarely (if the extent contains both the dateline and prime meridian) + // the shift can in turn break some triangles. + // Detect this here and don't shift in such cases. + var minX = Math.min( + newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]); + var maxX = Math.max( + newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]); + if ((maxX - minX) < this.sourceWorldWidth_ / 2) { + triangle.source = newTriangle; + } + } + }, this); + } + transformInvCache = {}; }; @@ -278,66 +315,23 @@ ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, /** * Calculates extent of the 'source' coordinates from all the triangles. - * The left bound of the returned extent can be higher than the right bound, - * if the triangulation wraps X in source (see - * {@link ol.reproj.Triangulation#getWrapsXInSource}). * * @return {ol.Extent} Calculated extent. */ ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { - if (this.trianglesSourceExtent_) { - return this.trianglesSourceExtent_; - } - var extent = ol.extent.createEmpty(); - if (this.wrapsXInSource_) { - // although only some of the triangles are crossing the dateline, - // all coordinates need to be "shifted" to be positive - // to properly calculate the extent (and then possibly shifted back) + this.triangles_.forEach(function(triangle, i, arr) { + var src = triangle.source; + ol.extent.extendCoordinate(extent, src[0]); + ol.extent.extendCoordinate(extent, src[1]); + ol.extent.extendCoordinate(extent, src[2]); + }); - this.triangles_.forEach(function(triangle, i, arr) { - goog.asserts.assert(this.sourceWorldWidth_); - var src = triangle.source; - ol.extent.extendCoordinate(extent, - [goog.math.modulo(src[0][0], this.sourceWorldWidth_), src[0][1]]); - ol.extent.extendCoordinate(extent, - [goog.math.modulo(src[1][0], this.sourceWorldWidth_), src[1][1]]); - ol.extent.extendCoordinate(extent, - [goog.math.modulo(src[2][0], this.sourceWorldWidth_), src[2][1]]); - }, this); - - var sourceProjExtent = this.sourceProj_.getExtent(); - var right = sourceProjExtent[2]; - if (extent[0] > right) { - extent[0] -= this.sourceWorldWidth_; - } - if (extent[2] > right) { - extent[2] -= this.sourceWorldWidth_; - } - } else { - this.triangles_.forEach(function(triangle, i, arr) { - var src = triangle.source; - ol.extent.extendCoordinate(extent, src[0]); - ol.extent.extendCoordinate(extent, src[1]); - ol.extent.extendCoordinate(extent, src[2]); - }); - } - - this.trianglesSourceExtent_ = extent; return extent; }; -/** - * @return {boolean} Whether the source coordinates are wrapped in X - * (the triangulation "crosses the dateline"). - */ -ol.reproj.Triangulation.prototype.getWrapsXInSource = function() { - return this.wrapsXInSource_; -}; - - /** * @return {Array.} Array of the calculated triangles. */ From 32fa3501df44f0fe6542ae262c7189d823400b86 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 16 Oct 2015 09:24:01 +0200 Subject: [PATCH 74/80] Update tests --- test/spec/ol/reproj/triangulation.test.js | 14 ---------- test_rendering/spec/ol/reproj/image.test.js | 30 --------------------- test_rendering/spec/ol/reproj/tile.test.js | 4 +-- 3 files changed, 2 insertions(+), 46 deletions(-) diff --git a/test/spec/ol/reproj/triangulation.test.js b/test/spec/ol/reproj/triangulation.test.js index f99fcf67ed..3142498ff9 100644 --- a/test/spec/ol/reproj/triangulation.test.js +++ b/test/spec/ol/reproj/triangulation.test.js @@ -36,20 +36,6 @@ describe('ol.reproj.Triangulation', function() { [20, 20, 30, 30], null, 0); expect(triangulation.getTriangles().length).to.be(2); }); - - it('can handle wrapX in source', function() { - proj4.defs('merc_180', '+proj=merc +lon_0=180 +units=m +no_defs'); - var proj_ = ol.proj.get('merc_180'); - proj_.setExtent([-20026376.39, -20048966.10, 20026376.39, 20048966.10]); - - var proj4326 = ol.proj.get('EPSG:4326'); - var triangulation = new ol.reproj.Triangulation(proj4326, proj_, - proj_.getExtent(), [-180, -90, 180, 90], 0); - expect(triangulation.getWrapsXInSource()).to.be(true); - var triExtent = triangulation.calculateSourceExtent(); - expect(triExtent[2] < triExtent[0]).to.be(true); - }); - }); }); diff --git a/test_rendering/spec/ol/reproj/image.test.js b/test_rendering/spec/ol/reproj/image.test.js index 379318e8cf..2413e945dc 100644 --- a/test_rendering/spec/ol/reproj/image.test.js +++ b/test_rendering/spec/ol/reproj/image.test.js @@ -51,36 +51,6 @@ describe('ol.rendering.reproj.Image', function() { 'spec/ol/reproj/expected/image-3857-to-4326.png', done); }); }); - - describe('dateline wrapping', function() { - beforeEach(function() { - source = new ol.source.ImageStatic({ - url: 'spec/ol/data/tiles/4326/0/0/0.png', - imageExtent: [-180, -270, 180, 90], - projection: ol.proj.get('EPSG:4326') - }); - }); - - it('wraps X when prime meridian is shifted', function(done) { - proj4.defs('merc_180', '+proj=merc +lon_0=180 +units=m +no_defs'); - var projExtent = [-20026376.39, -20048966.10, 20026376.39, 20048966.10]; - - testSingleImage(source, 'merc_180', projExtent, - ol.extent.getWidth(projExtent) / 64, 1, - 'spec/ol/reproj/expected/image-dateline-merc-180.png', done); - }); - - it('displays north pole correctly (EPSG:3413)', function(done) { - proj4.defs('EPSG:3413', '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 ' + - '+k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); - - var projExtent = [-4194304, -4194304, 4194304, 4194304]; - - testSingleImage(source, 'EPSG:3413', projExtent, - ol.extent.getWidth(projExtent) / 64, 1, - 'spec/ol/reproj/expected/image-dateline-pole.png', done); - }); - }); }); goog.require('ol.extent'); diff --git a/test_rendering/spec/ol/reproj/tile.test.js b/test_rendering/spec/ol/reproj/tile.test.js index 7085baea0d..2102b2f7f4 100644 --- a/test_rendering/spec/ol/reproj/tile.test.js +++ b/test_rendering/spec/ol/reproj/tile.test.js @@ -156,7 +156,7 @@ describe('ol.rendering.reproj.Tile', function() { var tileGrid = ol.tilegrid.createForProjection('merc_180', 0, [64, 64]); testSingleTile(source, 'merc_180', tileGrid, 0, 0, -1, 1, - 'spec/ol/reproj/expected/dateline-merc-180.png', 1, done); + 'spec/ol/reproj/expected/dateline-merc-180.png', 2, done); }); it('displays north pole correctly (EPSG:3413)', function(done) { @@ -167,7 +167,7 @@ describe('ol.rendering.reproj.Tile', function() { var tileGrid = ol.tilegrid.createForProjection('EPSG:3413', 0, [64, 64]); testSingleTile(source, 'EPSG:3413', tileGrid, 0, 0, -1, 1, - 'spec/ol/reproj/expected/dateline-pole.png', 1, done); + 'spec/ol/reproj/expected/dateline-pole.png', 2, done); }); }); }); From 70021be919d11c27544e57e4afa1053d0848d66e Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 16 Oct 2015 15:45:34 +0200 Subject: [PATCH 75/80] Use lower-case parameter names even for matrices --- src/ol/math.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ol/math.js b/src/ol/math.js index 2ca041d2b3..6884b965eb 100644 --- a/src/ol/math.js +++ b/src/ol/math.js @@ -81,16 +81,16 @@ ol.math.squaredDistance = function(x1, y1, x2, y2) { /** * Solves system of linear equations using Gaussian elimination method. * - * @param {Array.>} A Augmented matrix (n x n + 1 column) - * in row-major order. + * @param {Array.>} mat Augmented matrix (n x n + 1 column) + * in row-major order. * @return {Array.} The resulting vector. */ -ol.math.solveLinearSystem = function(A) { - var n = A.length; +ol.math.solveLinearSystem = function(mat) { + var n = mat.length; if (goog.asserts.ENABLE_ASSERTS) { for (var row = 0; row < n; row++) { - goog.asserts.assert(A[row].length == n + 1, + goog.asserts.assert(mat[row].length == n + 1, 'every row should have correct number of columns'); } } @@ -98,9 +98,9 @@ ol.math.solveLinearSystem = function(A) { for (var i = 0; i < n; i++) { // Find max in the i-th column (ignoring i - 1 first rows) var maxRow = i; - var maxEl = Math.abs(A[i][i]); + var maxEl = Math.abs(mat[i][i]); for (var r = i + 1; r < n; r++) { - var absValue = Math.abs(A[r][i]); + var absValue = Math.abs(mat[r][i]); if (absValue > maxEl) { maxEl = absValue; maxRow = r; @@ -112,29 +112,29 @@ ol.math.solveLinearSystem = function(A) { } // Swap max row with i-th (current) row - var tmp = A[maxRow]; - A[maxRow] = A[i]; - A[i] = tmp; + var tmp = mat[maxRow]; + mat[maxRow] = mat[i]; + mat[i] = tmp; // Subtract the i-th row to make all the remaining rows 0 in the i-th column for (var j = i + 1; j < n; j++) { - var coef = -A[j][i] / A[i][i]; + var coef = -mat[j][i] / mat[i][i]; for (var k = i; k < n + 1; k++) { if (i == k) { - A[j][k] = 0; + mat[j][k] = 0; } else { - A[j][k] += coef * A[i][k]; + mat[j][k] += coef * mat[i][k]; } } } } - // Solve Ax=b for upper triangular matrix A + // Solve Ax=b for upper triangular matrix A (mat) var x = new Array(n); for (var l = n - 1; l >= 0; l--) { - x[l] = A[l][n] / A[l][l]; + x[l] = mat[l][n] / mat[l][l]; for (var m = l - 1; m >= 0; m--) { - A[m][n] -= A[m][l] * x[l]; + mat[m][n] -= mat[m][l] * x[l]; } } return x; From b81612872898e1dda63922a731b0cbd7155a70cd Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 16 Oct 2015 15:46:51 +0200 Subject: [PATCH 76/80] Avoid assignment in return statement --- src/ol/reproj/triangulation.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index b69006fb01..bc4cbe1571 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -54,7 +54,10 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, */ this.transformInv_ = function(c) { var key = c[0] + '/' + c[1]; - return transformInvCache[key] || (transformInvCache[key] = transformInv(c)); + if (!transformInvCache[key]) { + transformInvCache[key] = transformInv(c); + } + return transformInvCache[key]; }; /** From 6f9fa4c12ebe6ceae5d809e46ba85d347a5163d5 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 16 Oct 2015 16:14:41 +0200 Subject: [PATCH 77/80] Minor documentation improvements --- src/ol/ol.js | 3 ++- src/ol/reproj/triangulation.js | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ol/ol.js b/src/ol/ol.js index 9a14b339eb..23c754f49a 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -190,7 +190,8 @@ ol.RASTER_REPROJECTION_MAX_SOURCE_TILES = 100; * @define {number} Maximum number of subdivision steps during raster * reprojection triangulation. Prevents high memory usage and large * number of proj4 calls (for certain transformations and areas). - * At most `2*(2^this)` triangles are created. Default is `10`. + * At most `2*(2^this)` triangles are created for each triangulated + * extent (tile/image). Default is `10`. */ ol.RASTER_REPROJECTION_MAX_SUBDIVISION = 10; diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js index bc4cbe1571..571c676c6a 100644 --- a/src/ol/reproj/triangulation.js +++ b/src/ol/reproj/triangulation.js @@ -79,9 +79,7 @@ ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, this.triangles_ = []; /** - * Indicates that source coordinates have to be shifted during reprojection. - * This is needed when the triangulation crosses - * edge of the source projection (dateline). + * Indicates that the triangulation crosses edge of the source projection. * @type {boolean} * @private */ From e49f529fe6d1c10d718d6f387abd075a06115d07 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 16 Oct 2015 16:18:11 +0200 Subject: [PATCH 78/80] Add typedefs for functions returning tiles/images --- src/ol/reproj/image.js | 9 ++++++++- src/ol/reproj/tile.js | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js index 88c699a62a..0292d7168e 100644 --- a/src/ol/reproj/image.js +++ b/src/ol/reproj/image.js @@ -1,4 +1,5 @@ goog.provide('ol.reproj.Image'); +goog.provide('ol.reproj.ImageFunctionType'); goog.require('goog.asserts'); goog.require('goog.events'); @@ -11,6 +12,12 @@ goog.require('ol.reproj'); goog.require('ol.reproj.Triangulation'); +/** + * @typedef {function(ol.Extent, number, number) : ol.ImageBase} + */ +ol.reproj.ImageFunctionType; + + /** * @classdesc @@ -24,7 +31,7 @@ goog.require('ol.reproj.Triangulation'); * @param {ol.Extent} targetExtent Target extent. * @param {number} targetResolution Target resolution. * @param {number} pixelRatio Pixel ratio. - * @param {function(ol.Extent, number, number):ol.ImageBase} getImageFunction + * @param {ol.reproj.ImageFunctionType} getImageFunction * Function returning source images (extent, resolution, pixelRatio). */ ol.reproj.Image = function(sourceProj, targetProj, diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js index 7e5192e525..a1c819d620 100644 --- a/src/ol/reproj/tile.js +++ b/src/ol/reproj/tile.js @@ -1,4 +1,5 @@ goog.provide('ol.reproj.Tile'); +goog.provide('ol.reproj.TileFunctionType'); goog.require('goog.asserts'); goog.require('goog.events'); @@ -14,6 +15,12 @@ goog.require('ol.reproj'); goog.require('ol.reproj.Triangulation'); +/** + * @typedef {function(number, number, number, number) : ol.Tile} + */ +ol.reproj.TileFunctionType; + + /** * @classdesc @@ -30,7 +37,7 @@ goog.require('ol.reproj.Triangulation'); * @param {number} x X. * @param {number} y Y. * @param {number} pixelRatio Pixel ratio. - * @param {function(number, number, number, number) : ol.Tile} getTileFunction + * @param {ol.reproj.TileFunctionType} getTileFunction * Function returning source tiles (z, x, y, pixelRatio). * @param {number=} opt_errorThreshold Acceptable reprojection error (in px). * @param {boolean=} opt_renderEdges Render reprojection edges. From d950dada31c4c0a57830b92120f1fbeb0b3e2e15 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 16 Oct 2015 16:19:54 +0200 Subject: [PATCH 79/80] Minor type fix --- src/ol/reproj/reproj.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index ed4e4a6940..7609942c61 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -99,7 +99,8 @@ ol.reproj.enlargeClipPoint_ = function(centroidX, centroidY, x, y) { * @param {ol.Extent} targetExtent Target extent. * @param {ol.reproj.Triangulation} triangulation Calculated triangulation. * @param {Array.<{extent: ol.Extent, - * image: (HTMLCanvasElement|Image)}>} sources Array of sources. + * image: (HTMLCanvasElement|Image|HTMLVideoElement)}>} sources + * Array of sources. * @param {boolean=} opt_renderEdges Render reprojection edges. * @return {HTMLCanvasElement} Canvas with reprojected data. */ From 965b88d7c8e71a0385aca422ed168ca2987ed210 Mon Sep 17 00:00:00 2001 From: Petr Sloup Date: Fri, 16 Oct 2015 16:20:34 +0200 Subject: [PATCH 80/80] Save context state only when it's actually necessary --- src/ol/reproj/reproj.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js index 7609942c61..fbca15fa90 100644 --- a/src/ol/reproj/reproj.js +++ b/src/ol/reproj/reproj.js @@ -144,8 +144,6 @@ ol.reproj.render = function(width, height, pixelRatio, var targetTopLeft = ol.extent.getTopLeft(targetExtent); triangulation.getTriangles().forEach(function(triangle, i, arr) { - context.save(); - /* Calculate affine transform (src -> dst) * Resulting matrix can be used to transform coordinate * from `sourceProjection` to destination pixels. @@ -199,6 +197,7 @@ ol.reproj.render = function(width, height, pixelRatio, return; } + context.save(); context.beginPath(); if (ol.reproj.browserAntialiasesClip_) { var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3;