diff --git a/src/ol/coordinate.js b/src/ol/coordinate.js index 177c8bce55..fb375162b9 100644 --- a/src/ol/coordinate.js +++ b/src/ol/coordinate.js @@ -3,6 +3,7 @@ */ import {modulo} from './math.js'; import {padNumber} from './string.js'; +import {getWidth} from './extent.js'; /** @@ -402,3 +403,22 @@ export function toStringHDMS(coordinate, opt_fractionDigits) { export function toStringXY(coordinate, opt_fractionDigits) { return format(coordinate, '{x}, {y}', opt_fractionDigits); } + + +/** + * Modifies the provided coordinate in-place to be within the real world + * extent. + * + * @param {Coordinate} coordinate Coordinate. + * @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); + coordinate[0] -= (worldsAway * worldWidth); + } + return coordinate; +} diff --git a/src/ol/extent.js b/src/ol/extent.js index 94eb9cb249..ef969a196a 100644 --- a/src/ol/extent.js +++ b/src/ol/extent.js @@ -813,3 +813,25 @@ export function applyTransform(extent, transformFn, opt_extent, opt_stops) { } return _boundingExtentXYs(xs, ys, opt_extent); } + + +/** + * Modifies the provided extent in-place to be within the real world + * extent. + * + * @param {Extent} extent Extent. + * @param {import("./proj/Projection.js").default} projection Projection + * @return {Extent} The extent within the real world extent. + */ +export function wrapX(extent, projection) { + const projectionExtent = projection.getExtent(); + const center = getCenter(extent); + if (projection.canWrapX() && (center[0] < projectionExtent[0] || center[0] > projectionExtent[2])) { + const worldWidth = getWidth(projectionExtent); + const worldsAway = Math.floor((center[0] - projectionExtent[0]) / worldWidth); + const offset = (worldsAway * worldWidth); + extent[0] -= offset; + extent[2] -= offset; + } + return extent; +} diff --git a/src/ol/layer/Graticule.js b/src/ol/layer/Graticule.js index 7a991858dc..4e4023fd68 100644 --- a/src/ol/layer/Graticule.js +++ b/src/ol/layer/Graticule.js @@ -24,7 +24,9 @@ import { getIntersection, getWidth, intersects, - isEmpty + isEmpty, + buffer as bufferExtent, + wrapX } from '../extent.js'; import {clamp} from '../math.js'; import Style from '../style/Style.js'; @@ -481,19 +483,10 @@ class Graticule extends VectorLayer { strategyFunction(extent, resolution) { // extents may be passed in different worlds, to avoid endless loop we use only one const realExtent = extent.slice(); - if (this.projection_) { - const center = getCenter(extent); - const projectionExtent = this.projection_.getExtent(); - const worldWidth = getWidth(projectionExtent); - if (this.getSource().getWrapX() && this.projection_.canWrapX() && !containsExtent(projectionExtent, extent)) { - const worldsAway = Math.floor((center[0] - projectionExtent[0]) / worldWidth); - realExtent[0] -= (worldsAway * worldWidth); - realExtent[2] -= (worldsAway * worldWidth); - } - realExtent[0] = Math.round(realExtent[0] * 1e6) / 1e6; - realExtent[2] = Math.round(realExtent[2] * 1e6) / 1e6; + if (this.projection_ && this.getSource().getWrapX()) { + wrapX(realExtent, this.projection_); } - if (this.loadedExtent_ && !equals(this.loadedExtent_, realExtent)) { + if (this.loadedExtent_ && !containsExtent(bufferExtent(this.loadedExtent_, resolution / 2), realExtent)) { // we should not keep track of loaded extents this.getSource().removeLoadedExtent(this.loadedExtent_); } diff --git a/src/ol/renderer/Map.js b/src/ol/renderer/Map.js index 591e96534f..86ee999a81 100644 --- a/src/ol/renderer/Map.js +++ b/src/ol/renderer/Map.js @@ -9,6 +9,7 @@ import {inView} from '../layer/Layer.js'; import {shared as iconImageCache} from '../style/IconImageCache.js'; import {compose as composeTransform, makeInverse} from '../transform.js'; import {renderDeclutterItems} from '../render.js'; +import {wrapX} from '../coordinate.js'; /** * @abstract @@ -102,19 +103,12 @@ class MapRenderer extends Disposable { const projection = viewState.projection; - let translatedCoordinate = coordinate; + const translatedCoordinate = wrapX(coordinate.slice(), projection); const offsets = [[0, 0]]; - if (projection.canWrapX()) { + if (projection.canWrapX() && checkWrapped) { const projectionExtent = projection.getExtent(); const worldWidth = getWidth(projectionExtent); - const x = coordinate[0]; - if (x < projectionExtent[0] || x > projectionExtent[2]) { - const worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth); - translatedCoordinate = [x + worldWidth * worldsAway, coordinate[1]]; - } - if (checkWrapped) { - offsets.push([-worldWidth, 0], [worldWidth, 0]); - } + offsets.push([-worldWidth, 0], [worldWidth, 0]); } const layerStates = frameState.layerStatesArray; diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 472333c8c9..0e2bebf908 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -3,7 +3,8 @@ */ import {getUid} from '../../util.js'; import ViewHint from '../../ViewHint.js'; -import {buffer, createEmpty, containsExtent, getWidth, intersects as intersectsExtent} from '../../extent.js'; +import {buffer, createEmpty, containsExtent, getWidth, intersects as intersectsExtent, wrapX as wrapExtentX} from '../../extent.js'; +import {wrapX as wrapCoordinateX} from '../../coordinate.js'; import {fromUserExtent, toUserExtent, getUserProjection, getTransformFromProjections} from '../../proj.js'; import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import ExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; @@ -364,7 +365,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const loadExtents = [extent.slice()]; const projectionExtent = viewState.projection.getExtent(); - if (vectorSource.getWrapX() && viewState.projection.canWrapX() && + if (vectorSource.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, frameState.extent)) { // For the replay group, we need an extent that intersects the real world // (-180° to +180°). To support geometries in a coordinate range from -540° @@ -375,11 +376,9 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const gutter = Math.max(getWidth(extent) / 2, worldWidth); extent[0] = projectionExtent[0] - gutter; extent[2] = projectionExtent[2] + gutter; - const worldsAway = Math.floor((center[0] - projectionExtent[0]) / worldWidth); - center[0] -= (worldsAway * worldWidth); - const loadExtent = loadExtents[0]; - loadExtent[0] -= (worldsAway * worldWidth); - loadExtent[2] -= (worldsAway * worldWidth); + wrapCoordinateX(center, projection); + const loadExtent = wrapExtentX(loadExtents[0], projection); + wrapExtentX(loadExtent, projection); // If the extent crosses the date line, we load data for both edges of the worlds if (loadExtent[0] < projectionExtent[0] && loadExtent[2] < projectionExtent[2]) { loadExtents.push([loadExtent[0] + worldWidth, loadExtent[1], loadExtent[2] + worldWidth, loadExtent[3]]); diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 9c0974fa91..acdd1198d6 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -25,6 +25,7 @@ import { import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; import {clear} from '../../obj.js'; import {createHitDetectionImageData, hitDetect} from '../../render/canvas/hitdetect.js'; +import {wrapX} from '../../coordinate.js'; /** @@ -353,9 +354,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (tile.getState() === TileState.LOADED && tile.hifi) { const extent = tileGrid.getTileCoordExtent(tile.tileCoord); if (source.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) { - const worldWidth = getWidth(projectionExtent); - const worldsAway = Math.floor((coordinate[0] - projectionExtent[0]) / worldWidth); - coordinate[0] -= (worldsAway * worldWidth); + wrapX(coordinate, projection); } break; } diff --git a/test/spec/ol/coordinate.test.js b/test/spec/ol/coordinate.test.js index 23d18509fc..90b6858718 100644 --- a/test/spec/ol/coordinate.test.js +++ b/test/spec/ol/coordinate.test.js @@ -1,5 +1,6 @@ -import {add as addCoordinate, scale as scaleCoordinate, rotate as rotateCoordinate, equals as coordinatesEqual, format as formatCoordinate, closestOnCircle, closestOnSegment, createStringXY, squaredDistanceToSegment, toStringXY, toStringHDMS} from '../../../src/ol/coordinate.js'; +import {add as addCoordinate, scale as scaleCoordinate, rotate as rotateCoordinate, equals as coordinatesEqual, format as formatCoordinate, closestOnCircle, closestOnSegment, createStringXY, squaredDistanceToSegment, toStringXY, toStringHDMS, wrapX} from '../../../src/ol/coordinate.js'; import Circle from '../../../src/ol/geom/Circle.js'; +import {get} from '../../../src/ol/proj.js'; describe('ol.coordinate', function() { @@ -235,4 +236,29 @@ describe('ol.coordinate', function() { }); }); + describe('wrapX()', function() { + const projection = get('EPSG:4326'); + + it('leaves real world coordinate untouched', function() { + expect(wrapX([16, 48], projection)).to.eql([16, 48]); + }); + + it('moves left world coordinate to real world', function() { + expect(wrapX([-344, 48], projection)).to.eql([16, 48]); + }); + + it('moves right world coordinate to real world', function() { + expect(wrapX([376, 48], projection)).to.eql([16, 48]); + }); + + it('moves far off left coordinate to real world', function() { + expect(wrapX([-1064, 48], projection)).to.eql([16, 48]); + }); + + it('moves far off right coordinate to real world', function() { + expect(wrapX([1096, 48], projection)).to.eql([16, 48]); + }); + + }); + }); diff --git a/test/spec/ol/extent.test.js b/test/spec/ol/extent.test.js index 3e12639d19..2bad7e640a 100644 --- a/test/spec/ol/extent.test.js +++ b/test/spec/ol/extent.test.js @@ -1,5 +1,5 @@ import * as _ol_extent_ from '../../../src/ol/extent.js'; -import {getTransform} from '../../../src/ol/proj.js'; +import {getTransform, get} from '../../../src/ol/proj.js'; import {register} from '../../../src/ol/proj/proj4.js'; @@ -818,4 +818,37 @@ describe('ol.extent', function() { }); + describe('wrapX()', function() { + const projection = get('EPSG:4326'); + + it('leaves real world extent untouched', function() { + expect(_ol_extent_.wrapX([16, 48, 18, 49], projection)).to.eql([16, 48, 18, 49]); + }); + + it('moves left world extent to real world', function() { + expect(_ol_extent_.wrapX([-344, 48, -342, 49], projection)).to.eql([16, 48, 18, 49]); + }); + + it('moves right world extent to real world', function() { + expect(_ol_extent_.wrapX([376, 48, 378, 49], projection)).to.eql([16, 48, 18, 49]); + }); + + it('moves far off left extent to real world', function() { + expect(_ol_extent_.wrapX([-1064, 48, -1062, 49], projection)).to.eql([16, 48, 18, 49]); + }); + + it('moves far off right extent to real world', function() { + expect(_ol_extent_.wrapX([1096, 48, 1098, 49], projection)).to.eql([16, 48, 18, 49]); + }); + + it('leaves -180 crossing extent with real world center untouched', function() { + expect(_ol_extent_.wrapX([-184, 48, 16, 49], projection)).to.eql([-184, 48, 16, 49]); + }); + + it('moves +180 crossing extent with off-world center to the real world', function() { + expect(_ol_extent_.wrapX([300, 48, 376, 49], projection)).to.eql([-60, 48, 16, 49]); + }); + + }); + });