315 lines
11 KiB
JavaScript
315 lines
11 KiB
JavaScript
goog.provide('ol.reproj');
|
|
|
|
goog.require('goog.array');
|
|
goog.require('goog.labs.userAgent.browser');
|
|
goog.require('goog.labs.userAgent.platform');
|
|
goog.require('goog.math');
|
|
goog.require('ol.dom');
|
|
goog.require('ol.extent');
|
|
goog.require('ol.math');
|
|
goog.require('ol.proj');
|
|
|
|
|
|
/**
|
|
* Calculates ideal resolution to use from the source in order to achieve
|
|
* pixel mapping as close as possible to 1:1 during reprojection.
|
|
* The resolution is calculated regardless on what resolutions
|
|
* are actually available in the dataset (TileGrid, Image, ...).
|
|
*
|
|
* @param {ol.proj.Projection} sourceProj
|
|
* @param {ol.proj.Projection} targetProj
|
|
* @param {ol.Coordinate} targetCenter
|
|
* @param {number} targetResolution
|
|
* @return {number} The best resolution to use. Can be +-Infinity, NaN or 0.
|
|
*/
|
|
ol.reproj.calculateSourceResolution = function(sourceProj, targetProj,
|
|
targetCenter, targetResolution) {
|
|
|
|
var sourceCenter = ol.proj.transform(targetCenter, targetProj, sourceProj);
|
|
|
|
var targetMPU = targetProj.getMetersPerUnit();
|
|
var sourceMPU = sourceProj.getMetersPerUnit();
|
|
|
|
// calculate the ideal resolution of the source data
|
|
var sourceResolution =
|
|
targetProj.getPointResolution(targetResolution, targetCenter);
|
|
if (goog.isDef(targetMPU)) sourceResolution *= targetMPU;
|
|
if (goog.isDef(sourceMPU)) sourceResolution /= sourceMPU;
|
|
|
|
// based on the projection properties, the point resolution at the specified
|
|
// 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;
|
|
};
|
|
|
|
|
|
/**
|
|
* Type of solution used to solve the wrapX issue.
|
|
* @enum {number}
|
|
* @private
|
|
*/
|
|
ol.reproj.WrapXRendering_ = {
|
|
NONE: 0,
|
|
STITCH_SHIFT: 1,
|
|
STITCH_EXTENDED: 2
|
|
};
|
|
|
|
|
|
/**
|
|
* We need to employ more sophisticated solution
|
|
* if the web browser antialiases clipping edges on canvas.
|
|
*
|
|
* Currently only Chrome does not antialias the edges, but this is probably
|
|
* going to be "fixed" in the future: http://crbug.com/424291
|
|
*
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
ol.reproj.browserAntialiasesClip_ = !goog.labs.userAgent.browser.isChrome() ||
|
|
goog.labs.userAgent.platform.isIos();
|
|
|
|
|
|
/**
|
|
* Renders the source into the canvas based on the triangulation.
|
|
* @param {CanvasRenderingContext2D} context
|
|
* @param {number} sourceResolution
|
|
* @param {ol.Extent} sourceExtent
|
|
* @param {number} targetResolution
|
|
* @param {ol.Extent} targetExtent
|
|
* @param {ol.reproj.Triangulation} triangulation
|
|
* @param {Array.<{extent: ol.Extent,
|
|
* image: (HTMLCanvasElement|Image)}>} sources
|
|
* @param {number} sourcePixelRatio
|
|
* @param {boolean=} opt_renderEdges
|
|
*/
|
|
ol.reproj.renderTriangles = function(context,
|
|
sourceResolution, sourceExtent, targetResolution, targetExtent,
|
|
triangulation, sources, sourcePixelRatio, opt_renderEdges) {
|
|
|
|
var wrapXShiftDistance = !goog.isNull(sourceExtent) ?
|
|
ol.extent.getWidth(sourceExtent) : 0;
|
|
|
|
var wrapXType = ol.reproj.WrapXRendering_.NONE;
|
|
|
|
if (triangulation.getWrapsXInSource() && wrapXShiftDistance > 0) {
|
|
// If possible, stitch the sources shifted to solve the wrapX issue here.
|
|
// This is not possible if crossing both "dateline" and "prime meridian".
|
|
var triangulationSrcExtent = triangulation.calculateSourceExtent();
|
|
var triangulationSrcWidth = goog.math.modulo(
|
|
ol.extent.getWidth(triangulationSrcExtent), wrapXShiftDistance);
|
|
|
|
if (triangulationSrcWidth < wrapXShiftDistance / 2) {
|
|
wrapXType = ol.reproj.WrapXRendering_.STITCH_SHIFT;
|
|
} else {
|
|
wrapXType = ol.reproj.WrapXRendering_.STITCH_EXTENDED;
|
|
}
|
|
}
|
|
|
|
var srcDataExtent = ol.extent.createEmpty();
|
|
goog.array.forEach(sources, function(src, i, arr) {
|
|
if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) {
|
|
var srcW = src.extent[2] - src.extent[0];
|
|
var srcX = goog.math.modulo(src.extent[0], wrapXShiftDistance);
|
|
ol.extent.extend(srcDataExtent, [srcX, src.extent[1],
|
|
srcX + srcW, src.extent[3]]);
|
|
} else {
|
|
ol.extent.extend(srcDataExtent, src.extent);
|
|
}
|
|
});
|
|
if (!goog.isNull(sourceExtent)) {
|
|
if (wrapXType == ol.reproj.WrapXRendering_.NONE) {
|
|
srcDataExtent[0] = goog.math.clamp(
|
|
srcDataExtent[0], sourceExtent[0], sourceExtent[2]);
|
|
srcDataExtent[2] = goog.math.clamp(
|
|
srcDataExtent[2], sourceExtent[0], sourceExtent[2]);
|
|
}
|
|
srcDataExtent[1] = goog.math.clamp(
|
|
srcDataExtent[1], sourceExtent[1], sourceExtent[3]);
|
|
srcDataExtent[3] = goog.math.clamp(
|
|
srcDataExtent[3], sourceExtent[1], sourceExtent[3]);
|
|
}
|
|
|
|
var srcDataWidth = ol.extent.getWidth(srcDataExtent);
|
|
var srcDataHeight = ol.extent.getHeight(srcDataExtent);
|
|
var canvasWidthInUnits;
|
|
if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) {
|
|
canvasWidthInUnits = 2 * wrapXShiftDistance;
|
|
} else {
|
|
canvasWidthInUnits = srcDataWidth;
|
|
}
|
|
|
|
var stitchContext = ol.dom.createCanvasContext2D(
|
|
Math.round(sourcePixelRatio * canvasWidthInUnits / sourceResolution),
|
|
Math.round(sourcePixelRatio * srcDataHeight / sourceResolution));
|
|
|
|
stitchContext.scale(sourcePixelRatio / sourceResolution,
|
|
sourcePixelRatio / sourceResolution);
|
|
stitchContext.translate(-srcDataExtent[0], srcDataExtent[3]);
|
|
|
|
goog.array.forEach(sources, 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);
|
|
|
|
if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) {
|
|
xPos = goog.math.modulo(xPos, wrapXShiftDistance);
|
|
}
|
|
stitchContext.drawImage(src.image, xPos, yPos, srcWidth, srcHeight);
|
|
|
|
if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) {
|
|
stitchContext.drawImage(src.image, wrapXShiftDistance + xPos, yPos,
|
|
srcWidth, srcHeight);
|
|
}
|
|
});
|
|
|
|
var targetTL = ol.extent.getTopLeft(targetExtent);
|
|
|
|
goog.array.forEach(triangulation.getTriangles(), function(tri, i, arr) {
|
|
context.save();
|
|
|
|
/* 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 src = tri.source, tgt = tri.target;
|
|
var x0 = src[0][0], y0 = src[0][1],
|
|
x1 = src[1][0], y1 = src[1][1],
|
|
x2 = src[2][0], y2 = src[2][1];
|
|
var u0 = (tgt[0][0] - targetTL[0]) / targetResolution,
|
|
v0 = -(tgt[0][1] - targetTL[1]) / targetResolution;
|
|
var u1 = (tgt[1][0] - targetTL[0]) / targetResolution,
|
|
v1 = -(tgt[1][1] - targetTL[1]) / targetResolution;
|
|
var u2 = (tgt[2][0] - targetTL[0]) / targetResolution,
|
|
v2 = -(tgt[2][1] - targetTL[1]) / targetResolution;
|
|
|
|
var performWrapXShift = false;
|
|
if (wrapXType == ol.reproj.WrapXRendering_.STITCH_SHIFT) {
|
|
performWrapXShift = true;
|
|
} else if (wrapXType == ol.reproj.WrapXRendering_.STITCH_EXTENDED) {
|
|
var minX = Math.min(x0, x1, x2);
|
|
var maxX = Math.max(x0, x1, x2);
|
|
|
|
performWrapXShift = (maxX - minX) > wrapXShiftDistance / 2 ||
|
|
minX <= sourceExtent[0];
|
|
}
|
|
|
|
if (performWrapXShift) {
|
|
x0 = goog.math.modulo(x0, wrapXShiftDistance);
|
|
x1 = goog.math.modulo(x1, wrapXShiftDistance);
|
|
x2 = goog.math.modulo(x2, wrapXShiftDistance);
|
|
}
|
|
|
|
// Shift all the source points to improve numerical stability
|
|
// of all the subsequent calculations. The [x0, y0] is used here.
|
|
// This is also used to simplify the linear system.
|
|
var srcNumericalShiftX = x0, srcNumericalShiftY = y0;
|
|
x0 = 0;
|
|
y0 = 0;
|
|
x1 -= srcNumericalShiftX;
|
|
y1 -= srcNumericalShiftY;
|
|
x2 -= srcNumericalShiftX;
|
|
y2 -= srcNumericalShiftY;
|
|
|
|
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 coefs = ol.math.solveLinearSystem(augmentedMatrix);
|
|
if (goog.isNull(coefs)) {
|
|
return;
|
|
}
|
|
|
|
context.beginPath();
|
|
if (ol.reproj.browserAntialiasesClip_) {
|
|
// Enlarge the clipping triangle by 1 pixel to ensure the edges overlap
|
|
// in order to mask gaps caused by antialiasing.
|
|
var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3;
|
|
var calcClipPoint = function(u, v) {
|
|
var dX = u - centroidX, dY = v - centroidY;
|
|
var distance = Math.sqrt(dX * dX + dY * dY);
|
|
return [Math.round(u + dX / distance), Math.round(v + dY / distance)];
|
|
};
|
|
|
|
var p0 = calcClipPoint(u0, v0);
|
|
var p1 = calcClipPoint(u1, v1);
|
|
var p2 = calcClipPoint(u2, v2);
|
|
|
|
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(coefs[0], coefs[2], coefs[1], coefs[3], u0, v0);
|
|
|
|
context.translate(srcDataExtent[0] - srcNumericalShiftX,
|
|
srcDataExtent[3] - srcNumericalShiftY);
|
|
|
|
context.scale(sourceResolution / sourcePixelRatio,
|
|
-sourceResolution / sourcePixelRatio);
|
|
|
|
context.drawImage(stitchContext.canvas, 0, 0);
|
|
context.restore();
|
|
});
|
|
|
|
if (opt_renderEdges) {
|
|
context.save();
|
|
|
|
context.strokeStyle = 'black';
|
|
context.lineWidth = 1;
|
|
|
|
goog.array.forEach(triangulation.getTriangles(), function(tri, i, arr) {
|
|
|
|
var tgt = tri.target;
|
|
var u0 = (tgt[0][0] - targetTL[0]) / targetResolution,
|
|
v0 = -(tgt[0][1] - targetTL[1]) / targetResolution;
|
|
var u1 = (tgt[1][0] - targetTL[0]) / targetResolution,
|
|
v1 = -(tgt[1][1] - targetTL[1]) / targetResolution;
|
|
var u2 = (tgt[2][0] - targetTL[0]) / targetResolution,
|
|
v2 = -(tgt[2][1] - targetTL[1]) / targetResolution;
|
|
|
|
context.beginPath();
|
|
context.moveTo(u0, v0);
|
|
context.lineTo(u1, v1);
|
|
context.lineTo(u2, v2);
|
|
context.closePath();
|
|
context.stroke();
|
|
});
|
|
|
|
context.restore();
|
|
}
|
|
};
|