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