Merge pull request #10723 from mike-000/patch-6
Improve the extent transforms used by Graticule and handle extents crossing the dateline
This commit is contained in:
@@ -17,8 +17,8 @@ register(proj4);
|
|||||||
// and a world extent. These are required for the Graticule.
|
// and a world extent. These are required for the Graticule.
|
||||||
const sphereMollweideProjection = new Projection({
|
const sphereMollweideProjection = new Projection({
|
||||||
code: 'ESRI:53009',
|
code: 'ESRI:53009',
|
||||||
extent: [-9009954.605703328, -9009954.605703328,
|
extent: [-18019909.21177587, -9009954.605703328,
|
||||||
9009954.605703328, 9009954.605703328],
|
18019909.21177587, 9009954.605703328],
|
||||||
worldExtent: [-179, -89.99, 179, 89.99]
|
worldExtent: [-179, -89.99, 179, 89.99]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,6 +37,6 @@ const map = new Map({
|
|||||||
view: new View({
|
view: new View({
|
||||||
center: [0, 0],
|
center: [0, 0],
|
||||||
projection: sphereMollweideProjection,
|
projection: sphereMollweideProjection,
|
||||||
zoom: 0
|
zoom: 1
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
+29
-9
@@ -778,18 +778,38 @@ export function intersectsSegment(extent, start, end) {
|
|||||||
* @param {import("./proj.js").TransformFunction} transformFn Transform function.
|
* @param {import("./proj.js").TransformFunction} transformFn Transform function.
|
||||||
* Called with `[minX, minY, maxX, maxY]` extent coordinates.
|
* Called with `[minX, minY, maxX, maxY]` extent coordinates.
|
||||||
* @param {Extent=} opt_extent Destination extent.
|
* @param {Extent=} opt_extent Destination extent.
|
||||||
|
* @param {number=} opt_stops Number of stops per side used for the transform.
|
||||||
|
* By default only the corners are used.
|
||||||
* @return {Extent} Extent.
|
* @return {Extent} Extent.
|
||||||
* @api
|
* @api
|
||||||
*/
|
*/
|
||||||
export function applyTransform(extent, transformFn, opt_extent) {
|
export function applyTransform(extent, transformFn, opt_extent, opt_stops) {
|
||||||
const coordinates = [
|
let coordinates = [];
|
||||||
extent[0], extent[1],
|
if (opt_stops > 1) {
|
||||||
extent[0], extent[3],
|
const width = extent[2] - extent[0];
|
||||||
extent[2], extent[1],
|
const height = extent[3] - extent[1];
|
||||||
extent[2], extent[3]
|
for (let i = 0; i < opt_stops; ++i) {
|
||||||
];
|
coordinates.push(
|
||||||
|
extent[0] + width * i / opt_stops, extent[1],
|
||||||
|
extent[2], extent[1] + height * i / opt_stops,
|
||||||
|
extent[2] - width * i / opt_stops, extent[3],
|
||||||
|
extent[0], extent[3] - height * i / opt_stops
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
coordinates = [
|
||||||
|
extent[0], extent[1],
|
||||||
|
extent[2], extent[1],
|
||||||
|
extent[2], extent[3],
|
||||||
|
extent[0], extent[3]
|
||||||
|
];
|
||||||
|
}
|
||||||
transformFn(coordinates, coordinates, 2);
|
transformFn(coordinates, coordinates, 2);
|
||||||
const xs = [coordinates[0], coordinates[2], coordinates[4], coordinates[6]];
|
const xs = [];
|
||||||
const ys = [coordinates[1], coordinates[3], coordinates[5], coordinates[7]];
|
const ys = [];
|
||||||
|
for (let i = 0, l = coordinates.length; i < l; i += 2) {
|
||||||
|
xs.push(coordinates[i]);
|
||||||
|
ys.push(coordinates[i + 1]);
|
||||||
|
}
|
||||||
return _boundingExtentXYs(xs, ys, opt_extent);
|
return _boundingExtentXYs(xs, ys, opt_extent);
|
||||||
}
|
}
|
||||||
|
|||||||
+152
-30
@@ -12,10 +12,19 @@ import VectorSource from '../source/Vector.js';
|
|||||||
import {
|
import {
|
||||||
equivalent as equivalentProjection,
|
equivalent as equivalentProjection,
|
||||||
get as getProjection,
|
get as getProjection,
|
||||||
getTransform,
|
getTransform
|
||||||
transformExtent
|
|
||||||
} from '../proj.js';
|
} from '../proj.js';
|
||||||
import {getCenter, getHeight, getWidth, intersects, equals, getIntersection, isEmpty} from '../extent.js';
|
import {
|
||||||
|
applyTransform,
|
||||||
|
containsCoordinate,
|
||||||
|
equals,
|
||||||
|
getCenter,
|
||||||
|
getHeight,
|
||||||
|
getIntersection,
|
||||||
|
getWidth,
|
||||||
|
intersects,
|
||||||
|
isEmpty
|
||||||
|
} from '../extent.js';
|
||||||
import {clamp} from '../math.js';
|
import {clamp} from '../math.js';
|
||||||
import Style from '../style/Style.js';
|
import Style from '../style/Style.js';
|
||||||
import Feature from '../Feature.js';
|
import Feature from '../Feature.js';
|
||||||
@@ -144,7 +153,8 @@ const INTERVALS = [
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @classdesc
|
* @classdesc
|
||||||
* Layer that renders a grid for a coordinate system.
|
* Layer that renders a grid for a coordinate system (currently only EPSG:4326 is supported).
|
||||||
|
* Note that the view projection must define both extent and worldExtent.
|
||||||
*
|
*
|
||||||
* @fires import("../render/Event.js").RenderEvent
|
* @fires import("../render/Event.js").RenderEvent
|
||||||
* @api
|
* @api
|
||||||
@@ -208,25 +218,25 @@ class Graticule extends VectorLayer {
|
|||||||
* @type {number}
|
* @type {number}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.maxLatP_ = Infinity;
|
this.maxX_ = Infinity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {number}
|
* @type {number}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.maxLonP_ = Infinity;
|
this.maxY_ = Infinity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {number}
|
* @type {number}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.minLatP_ = -Infinity;
|
this.minX_ = -Infinity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {number}
|
* @type {number}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.minLonP_ = -Infinity;
|
this.minY_ = -Infinity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {number}
|
* @type {number}
|
||||||
@@ -276,6 +286,30 @@ class Graticule extends VectorLayer {
|
|||||||
*/
|
*/
|
||||||
this.projectionCenterLonLat_ = null;
|
this.projectionCenterLonLat_ = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("../coordinate.js").Coordinate}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.bottomLeft_ = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("../coordinate.js").Coordinate}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.bottomRight_ = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("../coordinate.js").Coordinate}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.topLeft_ = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("../coordinate.js").Coordinate}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.topRight_ = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Array<GraticuleLabelDataType>}
|
* @type {Array<GraticuleLabelDataType>}
|
||||||
* @private
|
* @private
|
||||||
@@ -664,24 +698,67 @@ class Graticule extends VectorLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const centerLonLat = this.toLonLatTransform_(center);
|
// Constrain the center to fit into the extent available to the graticule
|
||||||
let centerLon = centerLonLat[0];
|
|
||||||
let centerLat = centerLonLat[1];
|
const validCenterP = [
|
||||||
|
clamp(center[0], this.minX_, this.maxX_),
|
||||||
|
clamp(center[1], this.minY_, this.maxY_)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Transform the center to lon lat
|
||||||
|
// Some projections may have a void area at the poles
|
||||||
|
// so replace any NaN latitudes with the min or max value closest to a pole
|
||||||
|
|
||||||
|
const centerLonLat = this.toLonLatTransform_(validCenterP);
|
||||||
|
if (isNaN(centerLonLat[1])) {
|
||||||
|
centerLonLat[1] = Math.abs(this.maxLat_) >= Math.abs(this.minLat_) ?
|
||||||
|
this.maxLat_ : this.minLat_;
|
||||||
|
}
|
||||||
|
let centerLon = clamp(centerLonLat[0], this.minLon_, this.maxLon_);
|
||||||
|
let centerLat = clamp(centerLonLat[1], this.minLat_, this.maxLat_);
|
||||||
const maxLines = this.maxLines_;
|
const maxLines = this.maxLines_;
|
||||||
let cnt, idx, lat, lon;
|
let cnt, idx, lat, lon;
|
||||||
|
|
||||||
let validExtent = [
|
// Limit the extent to fit into the extent available to the graticule
|
||||||
Math.max(extent[0], this.minLonP_),
|
|
||||||
Math.max(extent[1], this.minLatP_),
|
const validExtentP = [
|
||||||
Math.min(extent[2], this.maxLonP_),
|
clamp(extent[0], this.minX_, this.maxX_),
|
||||||
Math.min(extent[3], this.maxLatP_)
|
clamp(extent[1], this.minY_, this.maxY_),
|
||||||
|
clamp(extent[2], this.minX_, this.maxX_),
|
||||||
|
clamp(extent[3], this.minY_, this.maxY_)
|
||||||
];
|
];
|
||||||
|
|
||||||
validExtent = transformExtent(validExtent, this.projection_, 'EPSG:4326');
|
// Transform the extent to get the lon lat ranges for the edges of the extent
|
||||||
const maxLat = validExtent[3];
|
|
||||||
const maxLon = validExtent[2];
|
const validExtent = applyTransform(validExtentP, this.toLonLatTransform_, undefined, 8);
|
||||||
const minLat = validExtent[1];
|
|
||||||
const minLon = validExtent[0];
|
// Check if extremities of the world extent lie inside the extent
|
||||||
|
// (for example the pole in a polar projection)
|
||||||
|
// and extend the extent as appropriate
|
||||||
|
|
||||||
|
if (containsCoordinate(validExtentP, this.bottomLeft_)) {
|
||||||
|
validExtent[0] = this.minLon_;
|
||||||
|
validExtent[1] = this.minLat_;
|
||||||
|
}
|
||||||
|
if (containsCoordinate(validExtentP, this.bottomRight_)) {
|
||||||
|
validExtent[2] = this.maxLon_;
|
||||||
|
validExtent[1] = this.minLat_;
|
||||||
|
}
|
||||||
|
if (containsCoordinate(validExtentP, this.topLeft_)) {
|
||||||
|
validExtent[0] = this.minLon_;
|
||||||
|
validExtent[3] = this.maxLat_;
|
||||||
|
}
|
||||||
|
if (containsCoordinate(validExtentP, this.topRight_)) {
|
||||||
|
validExtent[2] = this.maxLon_;
|
||||||
|
validExtent[3] = this.maxLat_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The transformed center may also extend the lon lat ranges used for rendering
|
||||||
|
|
||||||
|
const maxLat = clamp(validExtent[3], centerLat, this.maxLat_);
|
||||||
|
const maxLon = clamp(validExtent[2], centerLon, this.maxLon_);
|
||||||
|
const minLat = clamp(validExtent[1], this.minLat_, centerLat);
|
||||||
|
const minLon = clamp(validExtent[0], this.minLon_, centerLon);
|
||||||
|
|
||||||
// Create meridians
|
// Create meridians
|
||||||
|
|
||||||
@@ -752,11 +829,13 @@ class Graticule extends VectorLayer {
|
|||||||
/** @type {Array<number>} **/
|
/** @type {Array<number>} **/
|
||||||
const p2 = [];
|
const p2 = [];
|
||||||
for (let i = 0, ii = this.intervals_.length; i < ii; ++i) {
|
for (let i = 0, ii = this.intervals_.length; i < ii; ++i) {
|
||||||
const delta = this.intervals_[i] / 2;
|
const delta = clamp(this.intervals_[i] / 2, 0, 90);
|
||||||
|
// Don't attempt to transform latitudes beyond the poles!
|
||||||
|
const clampedLat = clamp(centerLat, -90 + delta, 90 - delta);
|
||||||
p1[0] = centerLon - delta;
|
p1[0] = centerLon - delta;
|
||||||
p1[1] = centerLat - delta;
|
p1[1] = clampedLat - delta;
|
||||||
p2[0] = centerLon + delta;
|
p2[0] = centerLon + delta;
|
||||||
p2[1] = centerLat + delta;
|
p2[1] = clampedLat + delta;
|
||||||
this.fromLonLatTransform_(p1, p1);
|
this.fromLonLatTransform_(p1, p1);
|
||||||
this.fromLonLatTransform_(p2, p2);
|
this.fromLonLatTransform_(p2, p2);
|
||||||
const dist = Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2);
|
const dist = Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2);
|
||||||
@@ -896,23 +975,66 @@ class Graticule extends VectorLayer {
|
|||||||
const epsg4326Projection = getProjection('EPSG:4326');
|
const epsg4326Projection = getProjection('EPSG:4326');
|
||||||
|
|
||||||
const worldExtent = projection.getWorldExtent();
|
const worldExtent = projection.getWorldExtent();
|
||||||
const worldExtentP = transformExtent(worldExtent, epsg4326Projection, projection);
|
|
||||||
|
|
||||||
this.maxLat_ = worldExtent[3];
|
this.maxLat_ = worldExtent[3];
|
||||||
this.maxLon_ = worldExtent[2];
|
this.maxLon_ = worldExtent[2];
|
||||||
this.minLat_ = worldExtent[1];
|
this.minLat_ = worldExtent[1];
|
||||||
this.minLon_ = worldExtent[0];
|
this.minLon_ = worldExtent[0];
|
||||||
|
|
||||||
this.maxLatP_ = worldExtentP[3];
|
// If the world extent crosses the dateline define a custom transform to
|
||||||
this.maxLonP_ = worldExtentP[2];
|
// return longitudes which wrap the dateline
|
||||||
this.minLatP_ = worldExtentP[1];
|
|
||||||
this.minLonP_ = worldExtentP[0];
|
const toLonLatTransform = getTransform(projection, epsg4326Projection);
|
||||||
|
if (this.minLon_ < this.maxLon_) {
|
||||||
|
this.toLonLatTransform_ = toLonLatTransform;
|
||||||
|
} else {
|
||||||
|
const split = this.minLon_ + this.maxLon_ / 2;
|
||||||
|
this.maxLon_ += 360;
|
||||||
|
this.toLonLatTransform_ = function(coordinates, opt_output, opt_dimension) {
|
||||||
|
const dimension = opt_dimension || 2;
|
||||||
|
const lonLatCoordinates = toLonLatTransform(coordinates, opt_output, dimension);
|
||||||
|
for (let i = 0, l = lonLatCoordinates.length; i < l; i += dimension) {
|
||||||
|
if (lonLatCoordinates[i] < split) {
|
||||||
|
lonLatCoordinates[i] += 360;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lonLatCoordinates;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform the extent to get the limits of the view projection extent
|
||||||
|
// which should be available to the graticule
|
||||||
|
|
||||||
this.fromLonLatTransform_ = getTransform(epsg4326Projection, projection);
|
this.fromLonLatTransform_ = getTransform(epsg4326Projection, projection);
|
||||||
|
const worldExtentP = applyTransform(
|
||||||
|
[this.minLon_, this.minLat_, this.maxLon_, this.maxLat_],
|
||||||
|
this.fromLonLatTransform_,
|
||||||
|
undefined,
|
||||||
|
8
|
||||||
|
);
|
||||||
|
|
||||||
this.toLonLatTransform_ = getTransform(projection, epsg4326Projection);
|
this.minX_ = worldExtentP[0];
|
||||||
|
this.maxX_ = worldExtentP[2];
|
||||||
|
this.minY_ = worldExtentP[1];
|
||||||
|
this.maxY_ = worldExtentP[3];
|
||||||
|
|
||||||
|
// Determine the view projection coordinates of the extremities of the world extent
|
||||||
|
// as these may lie inside a view extent (for example the pole in a polar projection)
|
||||||
|
|
||||||
|
this.bottomLeft_ = this.fromLonLatTransform_([this.minLon_, this.minLat_]);
|
||||||
|
this.bottomRight_ = this.fromLonLatTransform_([this.maxLon_, this.minLat_]);
|
||||||
|
this.topLeft_ = this.fromLonLatTransform_([this.minLon_, this.maxLat_]);
|
||||||
|
this.topRight_ = this.fromLonLatTransform_([this.maxLon_, this.maxLat_]);
|
||||||
|
|
||||||
|
// Transform the projection center to lon lat
|
||||||
|
// Some projections may have a void area at the poles
|
||||||
|
// so replace any NaN latitudes with the min or max value closest to a pole
|
||||||
|
|
||||||
this.projectionCenterLonLat_ = this.toLonLatTransform_(getCenter(projection.getExtent()));
|
this.projectionCenterLonLat_ = this.toLonLatTransform_(getCenter(projection.getExtent()));
|
||||||
|
if (isNaN(this.projectionCenterLonLat_[1])) {
|
||||||
|
this.projectionCenterLonLat_[1] = Math.abs(this.maxLat_) >= Math.abs(this.minLat_) ?
|
||||||
|
this.maxLat_ : this.minLat_;
|
||||||
|
}
|
||||||
|
|
||||||
this.projection_ = projection;
|
this.projection_ = projection;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -476,12 +476,14 @@ export function transform(coordinate, source, destination) {
|
|||||||
* @param {import("./extent.js").Extent} extent The extent to transform.
|
* @param {import("./extent.js").Extent} extent The extent to transform.
|
||||||
* @param {ProjectionLike} source Source projection-like.
|
* @param {ProjectionLike} source Source projection-like.
|
||||||
* @param {ProjectionLike} destination Destination projection-like.
|
* @param {ProjectionLike} destination Destination projection-like.
|
||||||
|
* @param {number=} opt_stops Number of stops per side used for the transform.
|
||||||
|
* By default only the corners are used.
|
||||||
* @return {import("./extent.js").Extent} The transformed extent.
|
* @return {import("./extent.js").Extent} The transformed extent.
|
||||||
* @api
|
* @api
|
||||||
*/
|
*/
|
||||||
export function transformExtent(extent, source, destination) {
|
export function transformExtent(extent, source, destination, opt_stops) {
|
||||||
const transformFunc = getTransform(source, destination);
|
const transformFunc = getTransform(source, destination);
|
||||||
return applyTransform(extent, transformFunc);
|
return applyTransform(extent, transformFunc, undefined, opt_stops);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as _ol_extent_ from '../../../src/ol/extent.js';
|
import * as _ol_extent_ from '../../../src/ol/extent.js';
|
||||||
import {getTransform} from '../../../src/ol/proj.js';
|
import {getTransform} from '../../../src/ol/proj.js';
|
||||||
|
import {register} from '../../../src/ol/proj/proj4.js';
|
||||||
|
|
||||||
|
|
||||||
describe('ol.extent', function() {
|
describe('ol.extent', function() {
|
||||||
@@ -783,6 +784,38 @@ describe('ol.extent', function() {
|
|||||||
expect(destinationExtent[3]).to.be(30);
|
expect(destinationExtent[3]).to.be(30);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can use the stops option', function() {
|
||||||
|
proj4.defs('EPSG:32632', '+proj=utm +zone=32 +datum=WGS84 +units=m +no_defs');
|
||||||
|
register(proj4);
|
||||||
|
const transformFn = getTransform('EPSG:4326', 'EPSG:32632');
|
||||||
|
const sourceExtentN = [6, 0, 12, 84];
|
||||||
|
const destinationExtentN = _ol_extent_.applyTransform(
|
||||||
|
sourceExtentN, transformFn);
|
||||||
|
expect(destinationExtentN).not.to.be(undefined);
|
||||||
|
expect(destinationExtentN).not.to.be(null);
|
||||||
|
expect(destinationExtentN[0]).to.roughlyEqual(166021.44308053964, 1e-8);
|
||||||
|
expect(destinationExtentN[2]).to.roughlyEqual(833978.5569194605, 1e-8);
|
||||||
|
expect(destinationExtentN[1]).to.roughlyEqual(0, 1e-8);
|
||||||
|
expect(destinationExtentN[3]).to.roughlyEqual(9329005.182447437, 1e-8);
|
||||||
|
const sourceExtentNS = [6, -84, 12, 84];
|
||||||
|
const destinationExtentNS = _ol_extent_.applyTransform(
|
||||||
|
sourceExtentNS, transformFn);
|
||||||
|
expect(destinationExtentNS).not.to.be(undefined);
|
||||||
|
expect(destinationExtentNS).not.to.be(null);
|
||||||
|
expect(destinationExtentNS[0]).to.roughlyEqual(465005.34493886377, 1e-8);
|
||||||
|
expect(destinationExtentNS[2]).to.roughlyEqual(534994.6550611362, 1e-8);
|
||||||
|
expect(destinationExtentNS[1]).to.roughlyEqual(-destinationExtentN[3], 1e-8);
|
||||||
|
expect(destinationExtentNS[3]).to.roughlyEqual(destinationExtentN[3], 1e-8);
|
||||||
|
const destinationExtentNS2 = _ol_extent_.applyTransform(
|
||||||
|
sourceExtentNS, transformFn, undefined, 2);
|
||||||
|
expect(destinationExtentNS2).not.to.be(undefined);
|
||||||
|
expect(destinationExtentNS2).not.to.be(null);
|
||||||
|
expect(destinationExtentNS2[0]).to.roughlyEqual(destinationExtentN[0], 1e-8);
|
||||||
|
expect(destinationExtentNS2[2]).to.roughlyEqual(destinationExtentN[2], 1e-8);
|
||||||
|
expect(destinationExtentNS2[1]).to.roughlyEqual(-destinationExtentN[3], 1e-8);
|
||||||
|
expect(destinationExtentNS2[3]).to.roughlyEqual(destinationExtentN[3], 1e-8);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user