Merge pull request #11219 from ahocevar/safe-proj4

Make proj4 transforms behave like built-in transforms
This commit is contained in:
Andreas Hocevar
2020-06-27 10:25:05 +02:00
committed by GitHub
4 changed files with 125 additions and 15 deletions

View File

@@ -406,21 +406,35 @@ export function toStringXY(coordinate, opt_fractionDigits) {
* exclusive.
*
* @param {Coordinate} coordinate Coordinate.
* @param {import("./proj/Projection.js").default} projection Projection
* @param {import("./proj/Projection.js").default} projection Projection.
* @return {Coordinate} The coordinate within the real world extent.
*/
export function wrapX(coordinate, projection) {
const projectionExtent = projection.getExtent();
if (
projection.canWrapX() &&
(coordinate[0] < projectionExtent[0] ||
coordinate[0] >= projectionExtent[2])
) {
const worldWidth = getWidth(projectionExtent);
const worldsAway = Math.floor(
(coordinate[0] - projectionExtent[0]) / worldWidth
);
const worldWidth = getWidth(projection.getExtent());
const worldsAway = getWorldsAway(coordinate, projection, worldWidth);
if (worldsAway) {
coordinate[0] -= worldsAway * worldWidth;
}
return coordinate;
}
/**
* @param {Coordinate} coordinate Coordinate.
* @param {import("./proj/Projection.js").default} projection Projection.
* @param {number=} opt_sourceExtentWidth Width of the source extent.
* @return {number} Offset in world widths.
*/
export function getWorldsAway(coordinate, projection, opt_sourceExtentWidth) {
const projectionExtent = projection.getExtent();
let worldsAway = 0;
if (
projection.canWrapX() &&
(coordinate[0] < projectionExtent[0] || coordinate[0] > projectionExtent[2])
) {
const sourceExtentWidth =
opt_sourceExtentWidth || getWidth(projectionExtent);
worldsAway = Math.floor(
(coordinate[0] - projectionExtent[0]) / sourceExtentWidth
);
}
return worldsAway;
}

View File

@@ -71,9 +71,17 @@ import {
clear as clearTransformFuncs,
get as getTransformFunc,
} from './proj/transforms.js';
import {applyTransform} from './extent.js';
import {
applyTransform,
getBottomLeft,
getBottomRight,
getTopLeft,
getTopRight,
getWidth,
} from './extent.js';
import {clamp, modulo} from './math.js';
import {getDistance} from './sphere.js';
import {modulo} from './math.js';
import {getWorldsAway} from './coordinate.js';
/**
* A projection as {@link module:ol/proj/Projection}, SRS identifier
@@ -625,6 +633,64 @@ export function fromUserExtent(extent, destProjection) {
return transformExtent(extent, userProjection, destProjection);
}
/**
* Creates a safe coordinate transform function from a coordinate transform function.
* "Safe" means that it can handle wrapping of x-coordinates for global projections,
* and that coordinates exceeding the projection validity extent's range will be
* clamped to the validity range.
* @param {Projection} sourceProj Source projection.
* @param {Projection} destProj Destination projection.
* @param {function(import("./coordinate.js").Coordinate): import("./coordinate.js").Coordinate} forward Transform function (source to destiation).
* @param {function(import("./coordinate.js").Coordinate): import("./coordinate.js").Coordinate} inverse Transform function (destiation to source).
* @return {function(import("./coordinate.js").Coordinate): import("./coordinate.js").Coordinate} Safe transform function (source to destiation).
*/
export function createSafeCoordinateTransform(
sourceProj,
destProj,
forward,
inverse
) {
return function (coord) {
let x, y, worldsAway;
if (sourceProj.canWrapX()) {
const sourceExtent = sourceProj.getExtent();
const sourceExtentWidth = getWidth(sourceExtent);
worldsAway = getWorldsAway(coord, sourceProj, sourceExtentWidth);
if (worldsAway) {
// Move x to the real world
x = coord[0] - worldsAway * sourceExtentWidth;
}
}
let transformed = forward(x === undefined ? coord : [x, coord[1]]);
const destExtent = destProj.getExtent();
if (!isFinite(transformed[0]) || !isFinite(transformed[1])) {
// Try to recover from out-of-bounds transform
if (destExtent) {
const corner1 = inverse(getBottomLeft(destExtent));
const corner2 = inverse(getBottomRight(destExtent));
const corner3 = inverse(getTopLeft(destExtent));
const corner4 = inverse(getTopRight(destExtent));
const minX = Math.min(corner1[0], corner2[0], corner3[0], corner4[0]);
const maxX = Math.max(corner1[0], corner2[0], corner3[0], corner4[0]);
const minY = Math.min(corner1[1], corner2[1], corner3[1], corner4[1]);
const maxY = Math.max(corner1[1], corner2[1], corner3[1], corner4[1]);
if (isFinite(minX) && isFinite(maxX)) {
x = clamp(x == undefined ? coord[1] : x, minX, maxX);
}
if (isFinite(minY) && isFinite(maxY)) {
y = clamp(coord[1], minY, maxY);
}
transformed = forward([x, y]);
}
}
if (worldsAway && destProj.canWrapX()) {
// Move transformed coordinate back to the offset world
transformed[0] += worldsAway * getWidth(destExtent);
}
return transformed;
};
}
/**
* Add transforms to and from EPSG:4326 and EPSG:3857. This function is called
* by when this module is executed and should only need to be called again after

View File

@@ -6,6 +6,7 @@ import {
addCoordinateTransforms,
addEquivalentProjections,
addProjection,
createSafeCoordinateTransform,
get,
} from '../proj.js';
import {assign} from '../obj.js';
@@ -60,8 +61,18 @@ export function register(proj4) {
addCoordinateTransforms(
proj1,
proj2,
transform.forward,
transform.inverse
createSafeCoordinateTransform(
proj1,
proj2,
transform.forward,
transform.inverse
),
createSafeCoordinateTransform(
proj2,
proj1,
transform.inverse,
transform.forward
)
);
}
}

View File

@@ -560,6 +560,25 @@ describe('ol.proj', function () {
});
expect(getProjection('EPSG:4326')).to.equal(epsg4326);
});
it('uses safe transform functions', function () {
register(proj4);
const wgs84 = getProjection('WGS84');
const epsg4326 = getProjection('EPSG:4326');
wgs84.setExtent(epsg4326.getExtent());
wgs84.setGlobal(true);
const google = getProjection('GOOGLE');
const epsg3857 = getProjection('EPSG:3857');
google.setExtent(epsg3857.getExtent());
google.setGlobal(true);
const coord = [-190, 90];
const transformed = transform(coord, wgs84, google);
expect(transformed).to.eql(transform(coord, epsg4326, epsg3857));
const got = transform(transformed, google, wgs84);
const expected = transform(transformed, epsg3857, epsg4326);
expect(got[0]).to.roughlyEqual(expected[0], 1e-9);
expect(got[1]).to.roughlyEqual(expected[1], 1e-9);
});
});
describe('ol.proj.getTransformFromProjections()', function () {