Merge pull request #10463 from pjsg/fix_triangulation
Fix issue with reprojection and double drawing pixels.
This commit is contained in:
158
src/ol/reproj.js
158
src/ol/reproj.js
@@ -2,11 +2,73 @@
|
||||
* @module ol/reproj
|
||||
*/
|
||||
import {createCanvasContext2D} from './dom.js';
|
||||
import {containsCoordinate, createEmpty, extend, getHeight, getTopLeft, getWidth} from './extent.js';
|
||||
import {containsCoordinate, createEmpty, extend, forEachCorner, getCenter, getHeight, getTopLeft, getWidth} from './extent.js';
|
||||
import {solveLinearSystem} from './math.js';
|
||||
import {getPointResolution, transform} from './proj.js';
|
||||
import {assign} from './obj.js';
|
||||
|
||||
let brokenDiagonalRendering_;
|
||||
|
||||
/**
|
||||
* This draws a small triangle into a canvas by setting the triangle as the clip region
|
||||
* and then drawing a (too large) rectangle
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx The context in which to draw the triangle
|
||||
* @param {number} u1 The x-coordinate of the second point. The first point is 0,0.
|
||||
* @param {number} v1 The y-coordinate of the second point.
|
||||
* @param {number} u2 The x-coordinate of the third point.
|
||||
* @param {number} v2 The y-coordinate of the third point.
|
||||
*/
|
||||
function drawTestTriangle(ctx, u1, v1, u2, v2) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(u1, v1);
|
||||
ctx.lineTo(u2, v2);
|
||||
ctx.closePath();
|
||||
ctx.save();
|
||||
ctx.clip();
|
||||
ctx.fillRect(0, 0, Math.max(u1, u2) + 1, Math.max(v1, v2));
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the data from getImageData, see if the right values appear at the provided offset.
|
||||
* Returns true if either the color or transparency is off
|
||||
*
|
||||
* @param {Uint8ClampedArray} data The data returned from getImageData
|
||||
* @param {number} offset The pixel offset from the start of data.
|
||||
* @return {boolean} true if the diagonal rendering is broken
|
||||
*/
|
||||
function verifyBrokenDiagonalRendering(data, offset) {
|
||||
// the values ought to be close to the rgba(210, 0, 0, 0.75)
|
||||
return Math.abs(data[offset * 4] - 210) > 2 || Math.abs(data[offset * 4 + 3] - 0.75 * 255) > 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current browser configuration can render triangular clip regions correctly.
|
||||
* This value is cached so the function is only expensive the first time called.
|
||||
* Firefox on Windows (as of now) does not if HWA is enabled. See https://bugzilla.mozilla.org/show_bug.cgi?id=1606976
|
||||
* IE also doesn't. Chrome works, and everything seems to work on OSX and Android. This function caches the
|
||||
* result. I suppose that it is conceivably possible that a browser might flip modes while the app is
|
||||
* running, but lets hope not.
|
||||
*
|
||||
* @return {boolean} true if the Diagonal Rendering is broken.
|
||||
*/
|
||||
function isBrokenDiagonalRendering() {
|
||||
if (brokenDiagonalRendering_ === undefined) {
|
||||
const ctx = document.createElement('canvas').getContext('2d');
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
ctx.fillStyle = 'rgba(210, 0, 0, 0.75)';
|
||||
drawTestTriangle(ctx, 4, 5, 4, 0);
|
||||
drawTestTriangle(ctx, 4, 5, 0, 5);
|
||||
const data = ctx.getImageData(0, 0, 3, 3).data;
|
||||
brokenDiagonalRendering_ = verifyBrokenDiagonalRendering(data, 0) ||
|
||||
verifyBrokenDiagonalRendering(data, 4) ||
|
||||
verifyBrokenDiagonalRendering(data, 8);
|
||||
}
|
||||
|
||||
return brokenDiagonalRendering_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates ideal resolution to use from the source in order to achieve
|
||||
@@ -55,20 +117,31 @@ export function calculateSourceResolution(sourceProj, targetProj,
|
||||
|
||||
|
||||
/**
|
||||
* Enlarge the clipping triangle point by 1 pixel to ensure the edges overlap
|
||||
* in order to mask gaps caused by antialiasing.
|
||||
* 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 {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 {import("./coordinate.js").Coordinate} New point 1 px farther from the centroid.
|
||||
* @param {import("./proj/Projection.js").default} sourceProj Source projection.
|
||||
* @param {import("./proj/Projection.js").default} targetProj Target projection.
|
||||
* @param {import("./extent.js").Extent} targetExtent Target extent
|
||||
* @param {number} targetResolution Target resolution.
|
||||
* @return {number} The best resolution to use. Can be +-Infinity, NaN or 0.
|
||||
*/
|
||||
function enlargeClipPoint(centroidX, centroidY, x, y) {
|
||||
const dX = x - centroidX;
|
||||
const dY = y - centroidY;
|
||||
const distance = Math.sqrt(dX * dX + dY * dY);
|
||||
return [Math.round(x + dX / distance), Math.round(y + dY / distance)];
|
||||
export function calculateSourceExtentResolution(sourceProj, targetProj,
|
||||
targetExtent, targetResolution) {
|
||||
|
||||
const targetCenter = getCenter(targetExtent);
|
||||
let sourceResolution = calculateSourceResolution(sourceProj, targetProj, targetCenter, targetResolution);
|
||||
|
||||
if (!isFinite(sourceResolution) || sourceResolution <= 0) {
|
||||
forEachCorner(targetExtent, function(corner) {
|
||||
sourceResolution = calculateSourceResolution(sourceProj, targetProj, corner, targetResolution);
|
||||
return isFinite(sourceResolution) && sourceResolution > 0;
|
||||
});
|
||||
}
|
||||
|
||||
return sourceResolution;
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +179,12 @@ export function render(width, height, pixelRatio,
|
||||
|
||||
context.scale(pixelRatio, pixelRatio);
|
||||
|
||||
function pixelRound(value) {
|
||||
return Math.round(value * pixelRatio) / pixelRatio;
|
||||
}
|
||||
|
||||
context.globalCompositeOperation = 'lighter';
|
||||
|
||||
const sourceDataExtent = createEmpty();
|
||||
sources.forEach(function(src, i, arr) {
|
||||
extend(sourceDataExtent, src.extent);
|
||||
@@ -126,12 +205,15 @@ export function render(width, height, pixelRatio,
|
||||
const srcWidth = getWidth(src.extent);
|
||||
const srcHeight = getHeight(src.extent);
|
||||
|
||||
stitchContext.drawImage(
|
||||
src.image,
|
||||
gutter, gutter,
|
||||
src.image.width - 2 * gutter, src.image.height - 2 * gutter,
|
||||
xPos * stitchScale, yPos * stitchScale,
|
||||
srcWidth * stitchScale, srcHeight * stitchScale);
|
||||
// This test should never fail -- but it does. Need to find a fix the upstream condition
|
||||
if (src.image.width > 0 && src.image.height > 0) {
|
||||
stitchContext.drawImage(
|
||||
src.image,
|
||||
gutter, gutter,
|
||||
src.image.width - 2 * gutter, src.image.height - 2 * gutter,
|
||||
xPos * stitchScale, yPos * stitchScale,
|
||||
srcWidth * stitchScale, srcHeight * stitchScale);
|
||||
}
|
||||
});
|
||||
|
||||
const targetTopLeft = getTopLeft(targetExtent);
|
||||
@@ -194,15 +276,37 @@ export function render(width, height, pixelRatio,
|
||||
|
||||
context.save();
|
||||
context.beginPath();
|
||||
const centroidX = (u0 + u1 + u2) / 3;
|
||||
const centroidY = (v0 + v1 + v2) / 3;
|
||||
const p0 = enlargeClipPoint(centroidX, centroidY, u0, v0);
|
||||
const p1 = enlargeClipPoint(centroidX, centroidY, u1, v1);
|
||||
const p2 = enlargeClipPoint(centroidX, centroidY, u2, v2);
|
||||
|
||||
context.moveTo(p1[0], p1[1]);
|
||||
context.lineTo(p0[0], p0[1]);
|
||||
context.lineTo(p2[0], p2[1]);
|
||||
if (isBrokenDiagonalRendering()) {
|
||||
// Make sure that everything is on pixel boundaries
|
||||
const u0r = pixelRound(u0);
|
||||
const v0r = pixelRound(v0);
|
||||
const u1r = pixelRound(u1);
|
||||
const v1r = pixelRound(v1);
|
||||
const u2r = pixelRound(u2);
|
||||
const v2r = pixelRound(v2);
|
||||
// Make sure that all lines are horizontal or vertical
|
||||
context.moveTo(u1r, v1r);
|
||||
// This is the diagonal line. Do it in 4 steps
|
||||
const steps = 4;
|
||||
const ud = u0r - u1r;
|
||||
const vd = v0r - v1r;
|
||||
for (let step = 0; step < steps; step++) {
|
||||
// Go horizontally
|
||||
context.lineTo(u1r + pixelRound((step + 1) * ud / steps), v1r + pixelRound(step * vd / (steps - 1)));
|
||||
// Go vertically
|
||||
if (step != (steps - 1)) {
|
||||
context.lineTo(u1r + pixelRound((step + 1) * ud / steps), v1r + pixelRound((step + 1) * vd / (steps - 1)));
|
||||
}
|
||||
}
|
||||
// We are almost at u0r, v0r
|
||||
context.lineTo(u2r, v2r);
|
||||
} else {
|
||||
context.moveTo(u1, v1);
|
||||
context.lineTo(u0, v0);
|
||||
context.lineTo(u2, v2);
|
||||
}
|
||||
|
||||
context.clip();
|
||||
|
||||
context.transform(
|
||||
|
||||
Reference in New Issue
Block a user