Merge pull request #4122 from klokantech/rasterreproj

Raster reprojection
This commit is contained in:
Andreas Hocevar
2015-10-16 21:47:02 +02:00
66 changed files with 2890 additions and 67 deletions

View File

@@ -78,6 +78,69 @@ ol.math.squaredDistance = function(x1, y1, x2, y2) {
};
/**
* Solves system of linear equations using Gaussian elimination method.
*
* @param {Array.<Array.<number>>} mat Augmented matrix (n x n + 1 column)
* in row-major order.
* @return {Array.<number>} The resulting vector.
*/
ol.math.solveLinearSystem = function(mat) {
var n = mat.length;
if (goog.asserts.ENABLE_ASSERTS) {
for (var row = 0; row < n; row++) {
goog.asserts.assert(mat[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(mat[i][i]);
for (var r = i + 1; r < n; r++) {
var absValue = Math.abs(mat[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 = 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 = -mat[j][i] / mat[i][i];
for (var k = i; k < n + 1; k++) {
if (i == k) {
mat[j][k] = 0;
} else {
mat[j][k] += coef * mat[i][k];
}
}
}
}
// Solve Ax=b for upper triangular matrix A (mat)
var x = new Array(n);
for (var l = n - 1; l >= 0; l--) {
x[l] = mat[l][n] / mat[l][l];
for (var m = l - 1; m >= 0; m--) {
mat[m][n] -= mat[m][l] * x[l];
}
}
return x;
};
/**
* Converts radians to to degrees.
*

View File

@@ -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_REPROJECTION_ERROR_THRESHOLD = 0.5;
/**
* @define {number} Default high water mark.
*/
@@ -94,6 +101,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
@@ -159,6 +173,41 @@ ol.OVERVIEWMAP_MAX_RATIO = 0.75;
ol.OVERVIEWMAP_MIN_RATIO = 0.1;
/**
* @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
* 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_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 for each triangulated
* extent (tile/image). Default is `10`.
*/
ol.RASTER_REPROJECTION_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
* is performed.
* If the triangle width is more than this (relative to world width; 0-1),
* subdivison is forced (up to `ol.RASTER_REPROJECTION_MAX_SUBDIVISION`).
* Default is `0.25`.
*/
ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH = 0.25;
/**
* @define {number} Tolerance for geometry simplification in device pixels.
*/

View File

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

View File

@@ -309,7 +309,8 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame =
/** @type {Array.<ol.Tile>} */
var tilesToClear = [];
var findLoadedTiles = this.createLoadedTileFinder(tileSource, tilesToDrawByZ);
var findLoadedTiles = this.createLoadedTileFinder(
tileSource, projection, tilesToDrawByZ);
var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();

View File

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

View File

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

View File

@@ -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.<number, Object.<string, ol.Tile>>} 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;
});
});
};
@@ -156,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;
};
@@ -191,7 +195,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));
}
};
@@ -324,7 +329,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);
}
}
}

View File

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

View File

@@ -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.<number, Object.<string, ol.Tile>>} 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;

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

@@ -0,0 +1,210 @@
goog.provide('ol.reproj.Image');
goog.provide('ol.reproj.ImageFunctionType');
goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol.ImageBase');
goog.require('ol.ImageState');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.reproj');
goog.require('ol.reproj.Triangulation');
/**
* @typedef {function(ol.Extent, number, number) : ol.ImageBase}
*/
ol.reproj.ImageFunctionType;
/**
* @classdesc
* Class encapsulating single reprojected image.
* See {@link ol.source.Image}.
*
* @constructor
* @extends {ol.ImageBase}
* @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 {ol.reproj.ImageFunctionType} getImageFunction
* Function returning source images (extent, resolution, pixelRatio).
*/
ol.reproj.Image = function(sourceProj, targetProj,
targetExtent, targetResolution, pixelRatio, getImageFunction) {
/**
* @private
* @type {ol.proj.Projection}
*/
this.targetProj_ = targetProj;
/**
* @private
* @type {ol.Extent}
*/
this.maxSourceExtent_ = sourceProj.getExtent();
var maxTargetExtent = targetProj.getExtent();
var limitedTargetExtent = maxTargetExtent ?
ol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent;
var targetCenter = ol.extent.getCenter(limitedTargetExtent);
var sourceResolution = ol.reproj.calculateSourceResolution(
sourceProj, targetProj, targetCenter, targetResolution);
var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD;
/**
* @private
* @type {!ol.reproj.Triangulation}
*/
this.triangulation_ = new ol.reproj.Triangulation(
sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_,
sourceResolution * errorThresholdInPixels);
/**
* @private
* @type {number}
*/
this.targetResolution_ = targetResolution;
/**
* @private
* @type {ol.Extent}
*/
this.targetExtent_ = targetExtent;
var sourceExtent = this.triangulation_.calculateSourceExtent();
/**
* @private
* @type {ol.ImageBase}
*/
this.sourceImage_ =
getImageFunction(sourceExtent, sourceResolution, pixelRatio);
/**
* @private
* @type {number}
*/
this.sourcePixelRatio_ =
this.sourceImage_ ? this.sourceImage_.getPixelRatio() : 1;
/**
* @private
* @type {HTMLCanvasElement}
*/
this.canvas_ = null;
/**
* @private
* @type {goog.events.Key}
*/
this.sourceListenerKey_ = null;
var state = ol.ImageState.LOADED;
var attributions = [];
if (this.sourceImage_) {
state = ol.ImageState.IDLE;
attributions = this.sourceImage_.getAttributions();
}
goog.base(this, targetExtent, targetResolution, this.sourcePixelRatio_,
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_;
};
/**
* @return {ol.proj.Projection} Projection.
*/
ol.reproj.Image.prototype.getProjection = function() {
return this.targetProj_;
};
/**
* @private
*/
ol.reproj.Image.prototype.reproject_ = function() {
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.sourcePixelRatio_,
this.sourceImage_.getResolution(), this.maxSourceExtent_,
this.targetResolution_, this.targetExtent_, this.triangulation_, [{
extent: this.sourceImage_.getExtent(),
image: this.sourceImage_.getImage()
}]);
}
this.state = sourceState;
this.changed();
};
/**
* @inheritDoc
*/
ol.reproj.Image.prototype.load = function() {
if (this.state == ol.ImageState.IDLE) {
this.state = ol.ImageState.LOADING;
this.changed();
var sourceState = this.sourceImage_.getState();
if (sourceState == ol.ImageState.LOADED ||
sourceState == ol.ImageState.ERROR) {
this.reproject_();
} else {
this.sourceListenerKey_ = this.sourceImage_.listen(
goog.events.EventType.CHANGE, function(e) {
var sourceState = this.sourceImage_.getState();
if (sourceState == ol.ImageState.LOADED ||
sourceState == ol.ImageState.ERROR) {
this.unlistenSource_();
this.reproject_();
}
}, false, this);
this.sourceImage_.load();
}
}
};
/**
* @private
*/
ol.reproj.Image.prototype.unlistenSource_ = function() {
goog.asserts.assert(this.sourceListenerKey_,
'this.sourceListenerKey_ should not be null');
goog.events.unlistenByKey(this.sourceListenerKey_);
this.sourceListenerKey_ = null;
};

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

@@ -0,0 +1,258 @@
goog.provide('ol.reproj');
goog.require('goog.labs.userAgent.browser');
goog.require('goog.labs.userAgent.platform');
goog.require('goog.math');
goog.require('ol.dom');
goog.require('ol.extent');
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.
* The resolution is calculated regardless of what resolutions
* are actually available in the dataset (TileGrid, Image, ...).
*
* @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,
targetCenter, targetResolution) {
var sourceCenter = ol.proj.transform(targetCenter, targetProj, sourceProj);
// calculate the ideal resolution of the source data
var sourceResolution =
targetProj.getPointResolution(targetResolution, targetCenter);
var targetMetersPerUnit = targetProj.getMetersPerUnit();
if (targetMetersPerUnit !== undefined) {
sourceResolution *= targetMetersPerUnit;
}
var sourceMetersPerUnit = sourceProj.getMetersPerUnit();
if (sourceMetersPerUnit !== undefined) {
sourceResolution /= sourceMetersPerUnit;
}
// 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;
};
/**
* 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 (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, x, y) {
var dX = x - centroidX, dY = y - centroidY;
var distance = Math.sqrt(dX * dX + dY * dY);
return [Math.round(x + dX / distance), Math.round(y + dY / distance)];
};
/**
* 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|HTMLVideoElement)}>} 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,
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 sourceDataExtent = ol.extent.createEmpty();
sources.forEach(function(src, i, arr) {
ol.extent.extend(sourceDataExtent, src.extent);
});
var canvasWidthInUnits = ol.extent.getWidth(sourceDataExtent);
var canvasHeightInUnits = ol.extent.getHeight(sourceDataExtent);
var stitchContext = ol.dom.createCanvasContext2D(
Math.round(pixelRatio * canvasWidthInUnits / sourceResolution),
Math.round(pixelRatio * canvasHeightInUnits / sourceResolution));
stitchContext.scale(pixelRatio / sourceResolution,
pixelRatio / sourceResolution);
stitchContext.translate(-sourceDataExtent[0], sourceDataExtent[3]);
sources.forEach(function(src, i, arr) {
var xPos = src.extent[0];
var yPos = -src.extent[3];
var srcWidth = ol.extent.getWidth(src.extent);
var srcHeight = ol.extent.getHeight(src.extent);
stitchContext.drawImage(src.image, xPos, yPos, srcWidth, srcHeight);
});
var targetTopLeft = ol.extent.getTopLeft(targetExtent);
triangulation.getTriangles().forEach(function(triangle, i, arr) {
/* 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 so [ui, vi] are pixel coordinates.
*
* 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 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;
// 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 sourceNumericalShiftX = x0, sourceNumericalShiftY = y0;
x0 = 0;
y0 = 0;
x1 -= sourceNumericalShiftX;
y1 -= sourceNumericalShiftY;
x2 -= sourceNumericalShiftX;
y2 -= sourceNumericalShiftY;
var augmentedMatrix = [
[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 affineCoefs = ol.math.solveLinearSystem(augmentedMatrix);
if (!affineCoefs) {
return;
}
context.save();
context.beginPath();
if (ol.reproj.browserAntialiasesClip_) {
var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3;
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]);
context.lineTo(p2[0], p2[1]);
} else {
context.moveTo(u0, v0);
context.lineTo(u1, v1);
context.lineTo(u2, v2);
}
context.closePath();
context.clip();
context.transform(
affineCoefs[0], affineCoefs[2], affineCoefs[1], affineCoefs[3], u0, v0);
context.translate(sourceDataExtent[0] - sourceNumericalShiftX,
sourceDataExtent[3] - sourceNumericalShiftY);
context.scale(sourceResolution / pixelRatio,
-sourceResolution / pixelRatio);
context.drawImage(stitchContext.canvas, 0, 0);
context.restore();
});
if (opt_renderEdges) {
context.save();
context.strokeStyle = 'black';
context.lineWidth = 1;
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);
context.lineTo(u1, v1);
context.lineTo(u2, v2);
context.closePath();
context.stroke();
});
context.restore();
}
return context.canvas;
};

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

@@ -0,0 +1,332 @@
goog.provide('ol.reproj.Tile');
goog.provide('ol.reproj.TileFunctionType');
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');
goog.require('ol.extent');
goog.require('ol.math');
goog.require('ol.proj');
goog.require('ol.reproj');
goog.require('ol.reproj.Triangulation');
/**
* @typedef {function(number, number, number, number) : ol.Tile}
*/
ol.reproj.TileFunctionType;
/**
* @classdesc
* Class encapsulating single reprojected tile.
* See {@link ol.source.TileImage}.
*
* @constructor
* @extends {ol.Tile}
* @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 {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.
*/
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);
/**
* @private
* @type {boolean}
*/
this.renderEdges_ = opt_renderEdges !== undefined ? opt_renderEdges : false;
/**
* @private
* @type {number}
*/
this.pixelRatio_ = pixelRatio;
/**
* @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;
/**
* @private
* @type {!Array.<ol.Tile>}
*/
this.sourceTiles_ = [];
/**
* @private
* @type {Array.<goog.events.Key>}
*/
this.sourcesListenerKeys_ = null;
/**
* @private
* @type {number}
*/
this.sourceZ_ = 0;
var targetExtent = targetTileGrid.getTileCoordExtent(this.getTileCoord());
var maxTargetExtent = this.targetTileGrid_.getExtent();
var maxSourceExtent = this.sourceTileGrid_.getExtent();
var limitedTargetExtent = maxTargetExtent ?
ol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent;
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;
}
var sourceProjExtent = sourceProj.getExtent();
if (sourceProjExtent) {
if (!maxSourceExtent) {
maxSourceExtent = sourceProjExtent;
} else {
maxSourceExtent = ol.extent.getIntersection(
maxSourceExtent, sourceProjExtent);
}
}
var targetResolution = targetTileGrid.getResolution(z);
var targetCenter = ol.extent.getCenter(limitedTargetExtent);
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;
}
var errorThresholdInPixels = opt_errorThreshold !== undefined ?
opt_errorThreshold : ol.DEFAULT_RASTER_REPROJECTION_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.sourceZ_ = sourceTileGrid.getZForResolution(sourceResolution);
var sourceExtent = this.triangulation_.calculateSourceExtent();
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 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;
}
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);
}
}
}
if (this.sourceTiles_.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 (opt_context !== undefined) {
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 = [];
this.sourceTiles_.forEach(function(tile, i, arr) {
if (tile && tile.getState() == ol.TileState.LOADED) {
sources.push({
extent: this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord),
image: tile.getImage()
});
}
}, this);
this.sourceTiles_.length = 0;
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 sourceResolution = this.sourceTileGrid_.getResolution(this.sourceZ_);
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_);
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;
goog.asserts.assert(!this.sourcesListenerKeys_,
'this.sourcesListenerKeys_ should be null');
this.sourcesListenerKeys_ = [];
this.sourceTiles_.forEach(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) {
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);
this.sourceTiles_.forEach(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(this.sourcesListenerKeys_,
'this.sourcesListenerKeys_ should not be null');
this.sourcesListenerKeys_.forEach(goog.events.unlistenByKey);
this.sourcesListenerKeys_ = null;
};

View File

@@ -0,0 +1,341 @@
goog.provide('ol.reproj.Triangulation');
goog.require('goog.asserts');
goog.require('goog.math');
goog.require('ol.extent');
goog.require('ol.proj');
/**
* Single triangle; consists of 3 source points and 3 target points.
*
* @typedef {{source: Array.<ol.Coordinate>,
* target: Array.<ol.Coordinate>}}
*/
ol.reproj.Triangle;
/**
* @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
*/
ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent,
maxSourceExtent, errorThreshold) {
/**
* @type {ol.proj.Projection}
* @private
*/
this.sourceProj_ = sourceProj;
/**
* @type {ol.proj.Projection}
* @private
*/
this.targetProj_ = targetProj;
/** @type {!Object.<string, ol.Coordinate>} */
var transformInvCache = {};
var transformInv = ol.proj.getTransform(this.targetProj_, this.sourceProj_);
/**
* @param {ol.Coordinate} c
* @return {ol.Coordinate}
* @private
*/
this.transformInv_ = function(c) {
var key = c[0] + '/' + c[1];
if (!transformInvCache[key]) {
transformInvCache[key] = transformInv(c);
}
return transformInvCache[key];
};
/**
* @type {ol.Extent}
* @private
*/
this.maxSourceExtent_ = maxSourceExtent;
/**
* @type {number}
* @private
*/
this.errorThresholdSquared_ = errorThreshold * errorThreshold;
/**
* @type {Array.<ol.reproj.Triangle>}
* @private
*/
this.triangles_ = [];
/**
* Indicates that the triangulation crosses edge of the source projection.
* @type {boolean}
* @private
*/
this.wrapsXInSource_ = false;
/**
* @type {boolean}
* @private
*/
this.canWrapXInSource_ = this.sourceProj_.canWrapX() &&
!!maxSourceExtent &&
!!this.sourceProj_.getExtent() &&
(ol.extent.getWidth(maxSourceExtent) ==
ol.extent.getWidth(this.sourceProj_.getExtent()));
/**
* @type {?number}
* @private
*/
this.sourceWorldWidth_ = this.sourceProj_.getExtent() ?
ol.extent.getWidth(this.sourceProj_.getExtent()) : null;
/**
* @type {?number}
* @private
*/
this.targetWorldWidth_ = this.targetProj_.getExtent() ?
ol.extent.getWidth(this.targetProj_.getExtent()) : null;
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_(
destinationTopLeft, destinationTopRight,
destinationBottomRight, destinationBottomLeft,
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 = {};
};
/**
* Adds triangle to the triangulation.
* @param {ol.Coordinate} a
* @param {ol.Coordinate} b
* @param {ol.Coordinate} c
* @param {ol.Coordinate} aSrc
* @param {ol.Coordinate} bSrc
* @param {ol.Coordinate} cSrc
* @private
*/
ol.reproj.Triangulation.prototype.addTriangle_ = function(a, b, c,
aSrc, bSrc, cSrc) {
this.triangles_.push({
source: [aSrc, bSrc, cSrc],
target: [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
* @param {ol.Coordinate} d
* @param {ol.Coordinate} aSrc
* @param {ol.Coordinate} bSrc
* @param {ol.Coordinate} cSrc
* @param {ol.Coordinate} dSrc
* @param {number} maxSubdivision Maximal allowed subdivision of the quad.
* @private
*/
ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d,
aSrc, bSrc, cSrc, dSrc, maxSubdivision) {
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() &&
sourceCoverageX > 0.5 && sourceCoverageX < 1;
var needsSubdivision = false;
if (maxSubdivision > 0) {
if (this.targetProj_.isGlobal() && this.targetWorldWidth_) {
var targetQuadExtent = ol.extent.boundingExtent([a, b, c, d]);
var targetCoverageX =
ol.extent.getWidth(targetQuadExtent) / this.targetWorldWidth_;
needsSubdivision |=
targetCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH;
}
if (!wrapsX && this.sourceProj_.isGlobal() && sourceCoverageX) {
needsSubdivision |=
sourceCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH;
}
}
if (!needsSubdivision && this.maxSourceExtent_) {
if (!ol.extent.intersects(sourceQuadExtent, this.maxSourceExtent_)) {
// whole quad outside source projection extent -> ignore
return;
}
}
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 (maxSubdivision > 0) {
needsSubdivision = true;
} else {
return;
}
}
}
if (maxSubdivision > 0) {
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(this.sourceWorldWidth_);
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) {
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.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];
var abSrc = this.transformInv_(ab);
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, maxSubdivision - 1);
this.addQuad_(
ab, b, c, cd, abSrc, bSrc, cSrc, cdSrc, maxSubdivision - 1);
}
return;
}
}
if (wrapsX) {
if (!this.canWrapXInSource_) {
return;
}
this.wrapsXInSource_ = true;
}
this.addTriangle_(a, c, d, aSrc, cSrc, dSrc);
this.addTriangle_(a, b, c, aSrc, bSrc, cSrc);
};
/**
* Calculates extent of the 'source' coordinates from all the triangles.
*
* @return {ol.Extent} Calculated extent.
*/
ol.reproj.Triangulation.prototype.calculateSourceExtent = function() {
var extent = ol.extent.createEmpty();
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]);
});
return extent;
};
/**
* @return {Array.<ol.reproj.Triangle>} Array of the calculated triangles.
*/
ol.reproj.Triangulation.prototype.getTriangles = function() {
return this.triangles_;
};

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,11 @@ 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');
@@ -56,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);
@@ -90,7 +106,53 @@ 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 ||
!sourceProjection ||
!projection ||
ol.proj.equivalent(sourceProjection, projection)) {
if (sourceProjection) {
projection = sourceProjection;
}
return this.getImageInternal(extent, resolution, pixelRatio, projection);
} else {
if (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 this.reprojectedImage_;
}
};
/**
* @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;
/**

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

@@ -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),

View File

@@ -3,12 +3,16 @@ 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');
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');
@@ -69,6 +73,29 @@ ol.source.TileImage = function(options) {
this.tileClass = options.tileClass !== undefined ?
options.tileClass : ol.ImageTile;
/**
* @protected
* @type {Object.<string, ol.TileCache>}
*/
this.tileCacheForProjection = {};
/**
* @protected
* @type {Object.<string, ol.tilegrid.TileGrid>}
*/
this.tileGridForProjection = {};
/**
* @private
* @type {number|undefined}
*/
this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold;
/**
* @private
* @type {boolean}
*/
this.renderReprojectionEdges_ = false;
};
goog.inherits(ol.source.TileImage, ol.source.Tile);
@@ -82,11 +109,129 @@ 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;
} else {
return goog.object.some(this.tileCacheForProjection, function(tileCache) {
return tileCache.canExpireCache();
});
}
};
/**
* @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) {
tileCache.expireCache(tileCache == usedTileCache ? 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 (this.tileGrid &&
(!thisProj || ol.proj.equivalent(thisProj, projection))) {
return this.tileGrid;
} else {
var projKey = goog.getUid(projection).toString();
if (!(projKey in this.tileGridForProjection)) {
this.tileGridForProjection[projKey] =
ol.tilegrid.getForProjection(projection);
}
return this.tileGridForProjection[projKey];
}
};
/**
* @inheritDoc
*/
ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) {
if (!ol.ENABLE_RASTER_REPROJECTION) {
return goog.base(this, 'getTileCacheForProjection', projection);
}
var thisProj = this.getProjection();
if (!thisProj || ol.proj.equivalent(thisProj, 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];
}
};
/**
* @inheritDoc
*/
ol.source.TileImage.prototype.getTile =
function(z, x, y, pixelRatio, projection) {
if (!ol.ENABLE_RASTER_REPROJECTION ||
!this.getProjection() ||
!projection ||
ol.proj.equivalent(this.getProjection(), projection)) {
return this.getTileInternal(z, x, y, pixelRatio, projection);
} else {
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 = this.getTileGridForProjection(projection);
var tile = new ol.reproj.Tile(
sourceProjection, sourceTileGrid,
projection, targetTileGrid,
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_);
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));
@@ -156,6 +301,50 @@ ol.source.TileImage.prototype.handleTileChange_ = function(event) {
};
/**
* Sets whether to render reprojection edges or not (usually for debugging).
* @param {boolean} render Render the edges.
* @api
*/
ol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) {
if (!ol.ENABLE_RASTER_REPROJECTION ||
this.renderReprojectionEdges_ == render) {
return;
}
this.renderReprojectionEdges_ = render;
goog.object.forEach(this.tileCacheForProjection, function(tileCache) {
tileCache.clear();
});
this.changed();
};
/**
* 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 =
function(projection, tilegrid) {
if (ol.ENABLE_RASTER_REPROJECTION) {
var proj = ol.proj.get(projection);
if (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.
@@ -163,6 +352,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();
};
@@ -178,6 +368,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();
};
@@ -186,9 +377,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 (tileCache && tileCache.containsKey(tileCoordKey)) {
tileCache.get(tileCoordKey);
}
};

View File

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

View File

@@ -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');
@@ -97,14 +98,19 @@ ol.source.Tile.prototype.canExpireCache = function() {
/**
* @param {ol.proj.Projection} projection Projection.
* @param {Object.<string, ol.TileRange>} 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 (tileCache) {
tileCache.expireCache(usedTiles);
}
};
/**
* @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
@@ -112,15 +118,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 (!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);
@@ -174,7 +186,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;
@@ -203,6 +215,29 @@ 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) {
var thisProj = this.getProjection();
if (thisProj && !ol.proj.equivalent(thisProj, projection)) {
return null;
} else {
return this.tileCache;
}
};
/**
* @return {number} Tile pixel ratio.
*/
ol.source.Tile.prototype.getTilePixelRatio = function() {
return this.tilePixelRatio_;
};
/**
* @param {number} z Z.
* @param {number} pixelRatio Pixel ratio.
@@ -244,6 +279,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;

View File

@@ -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),

View File

@@ -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,

View File

@@ -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,

View File

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