Add ol.reproj

This commit is contained in:
Petr Sloup
2015-05-25 10:55:14 +02:00
parent 44a64ba451
commit 1222287f22
4 changed files with 669 additions and 0 deletions

181
src/ol/reproj/image.js Normal file
View File

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

119
src/ol/reproj/reproj.js Normal file
View File

@@ -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();
});
};

265
src/ol/reproj/tile.js Normal file
View File

@@ -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.<number, HTMLCanvasElement>}
*/
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.<ol.Tile>}
*/
this.srcTiles_ = [];
/**
* @private
* @type {Array.<goog.events.Key>}
*/
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;
};

View File

@@ -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.<Array.<Array.<ol.Coordinate>>>}
*/
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;
};