diff --git a/examples/sphere-mollweide.js b/examples/sphere-mollweide.js index 7540cd095b..83fc6adcc4 100644 --- a/examples/sphere-mollweide.js +++ b/examples/sphere-mollweide.js @@ -17,8 +17,8 @@ register(proj4); // and a world extent. These are required for the Graticule. const sphereMollweideProjection = new Projection({ code: 'ESRI:53009', - extent: [-9009954.605703328, -9009954.605703328, - 9009954.605703328, 9009954.605703328], + extent: [-18019909.21177587, -9009954.605703328, + 18019909.21177587, 9009954.605703328], worldExtent: [-179, -89.99, 179, 89.99] }); @@ -37,6 +37,6 @@ const map = new Map({ view: new View({ center: [0, 0], projection: sphereMollweideProjection, - zoom: 0 + zoom: 1 }) }); diff --git a/src/ol/extent.js b/src/ol/extent.js index cc74b803fb..94eb9cb249 100644 --- a/src/ol/extent.js +++ b/src/ol/extent.js @@ -778,18 +778,38 @@ export function intersectsSegment(extent, start, end) { * @param {import("./proj.js").TransformFunction} transformFn Transform function. * Called with `[minX, minY, maxX, maxY]` extent coordinates. * @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. * @api */ -export function applyTransform(extent, transformFn, opt_extent) { - const coordinates = [ - extent[0], extent[1], - extent[0], extent[3], - extent[2], extent[1], - extent[2], extent[3] - ]; +export function applyTransform(extent, transformFn, opt_extent, opt_stops) { + let coordinates = []; + if (opt_stops > 1) { + const width = extent[2] - extent[0]; + const height = extent[3] - extent[1]; + 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); - const xs = [coordinates[0], coordinates[2], coordinates[4], coordinates[6]]; - const ys = [coordinates[1], coordinates[3], coordinates[5], coordinates[7]]; + const xs = []; + 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); } diff --git a/src/ol/layer/Graticule.js b/src/ol/layer/Graticule.js index f1480dddc9..0fbc0cea50 100644 --- a/src/ol/layer/Graticule.js +++ b/src/ol/layer/Graticule.js @@ -12,10 +12,19 @@ import VectorSource from '../source/Vector.js'; import { equivalent as equivalentProjection, get as getProjection, - getTransform, - transformExtent + getTransform } 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 Style from '../style/Style.js'; import Feature from '../Feature.js'; @@ -144,7 +153,8 @@ const INTERVALS = [ /** * @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 * @api @@ -208,25 +218,25 @@ class Graticule extends VectorLayer { * @type {number} * @private */ - this.maxLatP_ = Infinity; + this.maxX_ = Infinity; /** * @type {number} * @private */ - this.maxLonP_ = Infinity; + this.maxY_ = Infinity; /** * @type {number} * @private */ - this.minLatP_ = -Infinity; + this.minX_ = -Infinity; /** * @type {number} * @private */ - this.minLonP_ = -Infinity; + this.minY_ = -Infinity; /** * @type {number} @@ -276,6 +286,30 @@ class Graticule extends VectorLayer { */ 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} * @private @@ -664,24 +698,67 @@ class Graticule extends VectorLayer { return; } - const centerLonLat = this.toLonLatTransform_(center); - let centerLon = centerLonLat[0]; - let centerLat = centerLonLat[1]; + // Constrain the center to fit into the extent available to the graticule + + 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_; let cnt, idx, lat, lon; - let validExtent = [ - Math.max(extent[0], this.minLonP_), - Math.max(extent[1], this.minLatP_), - Math.min(extent[2], this.maxLonP_), - Math.min(extent[3], this.maxLatP_) + // Limit the extent to fit into the extent available to the graticule + + const validExtentP = [ + clamp(extent[0], this.minX_, this.maxX_), + 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'); - const maxLat = validExtent[3]; - const maxLon = validExtent[2]; - const minLat = validExtent[1]; - const minLon = validExtent[0]; + // Transform the extent to get the lon lat ranges for the edges of the extent + + const validExtent = applyTransform(validExtentP, this.toLonLatTransform_, undefined, 8); + + // 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 @@ -752,11 +829,13 @@ class Graticule extends VectorLayer { /** @type {Array} **/ const p2 = []; 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[1] = centerLat - delta; + p1[1] = clampedLat - delta; p2[0] = centerLon + delta; - p2[1] = centerLat + delta; + p2[1] = clampedLat + delta; this.fromLonLatTransform_(p1, p1); this.fromLonLatTransform_(p2, p2); 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 worldExtent = projection.getWorldExtent(); - const worldExtentP = transformExtent(worldExtent, epsg4326Projection, projection); this.maxLat_ = worldExtent[3]; this.maxLon_ = worldExtent[2]; this.minLat_ = worldExtent[1]; this.minLon_ = worldExtent[0]; - this.maxLatP_ = worldExtentP[3]; - this.maxLonP_ = worldExtentP[2]; - this.minLatP_ = worldExtentP[1]; - this.minLonP_ = worldExtentP[0]; + // If the world extent crosses the dateline define a custom transform to + // return longitudes which wrap the dateline + + 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); + 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())); + if (isNaN(this.projectionCenterLonLat_[1])) { + this.projectionCenterLonLat_[1] = Math.abs(this.maxLat_) >= Math.abs(this.minLat_) ? + this.maxLat_ : this.minLat_; + } this.projection_ = projection; } diff --git a/src/ol/proj.js b/src/ol/proj.js index f64cc05a85..d2be0feb3f 100644 --- a/src/ol/proj.js +++ b/src/ol/proj.js @@ -476,12 +476,14 @@ export function transform(coordinate, source, destination) { * @param {import("./extent.js").Extent} extent The extent to transform. * @param {ProjectionLike} source Source 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. * @api */ -export function transformExtent(extent, source, destination) { +export function transformExtent(extent, source, destination, opt_stops) { const transformFunc = getTransform(source, destination); - return applyTransform(extent, transformFunc); + return applyTransform(extent, transformFunc, undefined, opt_stops); } diff --git a/test/spec/ol/extent.test.js b/test/spec/ol/extent.test.js index eb59120a2e..3e12639d19 100644 --- a/test/spec/ol/extent.test.js +++ b/test/spec/ol/extent.test.js @@ -1,5 +1,6 @@ import * as _ol_extent_ from '../../../src/ol/extent.js'; import {getTransform} from '../../../src/ol/proj.js'; +import {register} from '../../../src/ol/proj/proj4.js'; describe('ol.extent', function() { @@ -783,6 +784,38 @@ describe('ol.extent', function() { 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); + }); + }); });