Files
openlayers/src/ol/reproj/reproj.js

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