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_; +};