diff --git a/src/ol/extent.js b/src/ol/extent.js index a9c20bc445..67d11ad039 100644 --- a/src/ol/extent.js +++ b/src/ol/extent.js @@ -858,3 +858,47 @@ export function wrapX(extent, projection) { } return extent; } + +/** + * Fits the extent to the real world + * + * If the extent does not cross the anti meridian, this will return the extent in an array + * If the extent crosses the anti meridian, the extent will be sliced, so each part fits within the + * real world + * + * + * @param {Extent} extent Extent. + * @param {import("./proj/Projection.js").default} projection Projection + * @return {Array} The extent within the real world extent. + */ +export function wrapAndSliceX(extent, projection) { + if (projection.canWrapX()) { + const projectionExtent = projection.getExtent(); + + if (!isFinite(extent[0]) || !isFinite(extent[2])) { + return [[projectionExtent[0], extent[1], projectionExtent[2], extent[3]]]; + } + + wrapX(extent, projection); + const worldWidth = getWidth(projectionExtent); + + if (getWidth(extent) > worldWidth) { + // the extent wraps around on itself + return [[projectionExtent[0], extent[1], projectionExtent[2], extent[3]]]; + } else if (extent[0] < projectionExtent[0]) { + // the extent crosses the anti meridian, so it needs to be sliced + return [ + [extent[0] + worldWidth, extent[1], projectionExtent[2], extent[3]], + [projectionExtent[0], extent[1], extent[2], extent[3]], + ]; + } else if (extent[2] > projectionExtent[2]) { + // the extent crosses the anti meridian, so it needs to be sliced + return [ + [extent[0], extent[1], projectionExtent[2], extent[3]], + [projectionExtent[0], extent[1], extent[2] - worldWidth, extent[3]], + ]; + } + } + + return [extent]; +} diff --git a/src/ol/source/Vector.js b/src/ol/source/Vector.js index 1c4f795849..76572b4ba3 100644 --- a/src/ol/source/Vector.js +++ b/src/ol/source/Vector.js @@ -14,7 +14,7 @@ import VectorEventType from './VectorEventType.js'; import {TRUE, VOID} from '../functions.js'; import {all as allStrategy} from '../loadingstrategy.js'; import {assert} from '../asserts.js'; -import {containsExtent, equals} from '../extent.js'; +import {containsExtent, equals, wrapAndSliceX} from '../extent.js'; import {extend} from '../array.js'; import {getUid} from '../util.js'; import {getValues, isEmpty} from '../obj.js'; @@ -736,12 +736,25 @@ class VectorSource extends Source { * features. * * @param {import("../extent.js").Extent} extent Extent. + * @param {import("../proj/Projection.js").default} [opt_projection] Include features + * where `extent` exceeds the x-axis bounds of `projection` and wraps around the world. * @return {Array>} Features. * @api */ - getFeaturesInExtent(extent) { + getFeaturesInExtent(extent, opt_projection) { if (this.featuresRtree_) { - return this.featuresRtree_.getInExtent(extent); + const multiWorld = + opt_projection && opt_projection.canWrapX() && this.getWrapX(); + + if (!multiWorld) { + return this.featuresRtree_.getInExtent(extent); + } + + const extents = wrapAndSliceX(extent, opt_projection); + + return [].concat( + ...extents.map((anExtent) => this.featuresRtree_.getInExtent(anExtent)) + ); } else if (this.featuresCollection_) { return this.featuresCollection_.getArray().slice(0); } else { diff --git a/test/browser/spec/ol/source/vector.test.js b/test/browser/spec/ol/source/vector.test.js index 94beac641f..b2f39f1c99 100644 --- a/test/browser/spec/ol/source/vector.test.js +++ b/test/browser/spec/ol/source/vector.test.js @@ -7,6 +7,7 @@ import Point from '../../../../../src/ol/geom/Point.js'; import VectorLayer from '../../../../../src/ol/layer/Vector.js'; import VectorSource from '../../../../../src/ol/source/Vector.js'; import View from '../../../../../src/ol/View.js'; +import sinon from 'sinon'; import {bbox as bboxStrategy} from '../../../../../src/ol/loadingstrategy.js'; import { fromLonLat, @@ -979,4 +980,45 @@ describe('ol.source.Vector', function () { expect(source.getFeatures().length).to.be(0); }); }); + + describe('#getFeaturesInExtent()', function () { + it('adjusts the extent if projection canWrapX', function () { + const a = new Feature(new Point([0, 0])); + const b = new Feature(new Point([179, 0])); + const c = new Feature(new Point([-179, 0])); + + const source = new VectorSource({ + features: [a, b, c], + }); + + const projection = getProjection('EPSG:4326'); + + expect( + source.getFeaturesInExtent([-180, -90, 180, 90], projection).length + ).to.be(3); + const onlyB = source.getFeaturesInExtent([1, -90, 180, 90], projection); + expect(onlyB.length).to.be(1); + expect(onlyB).to.contain(b); + const bAndC = source.getFeaturesInExtent([1, -90, 182, 90], projection); + expect(bAndC.length).to.be(2); + expect(bAndC).to.contain(b); + expect(bAndC).to.contain(c); + + const onlyC = source.getFeaturesInExtent([-180, -90, -1, 90], projection); + expect(onlyC.length).to.be(1); + expect(onlyC).to.contain(c); + + const bAndCAgain = source.getFeaturesInExtent( + [-182, -90, -1, 90], + projection + ); + expect(bAndCAgain.length).to.be(2); + expect(bAndCAgain).to.contain(b); + expect(bAndCAgain).to.contain(c); + + const onlyA = source.getFeaturesInExtent([359, -90, 361, 90], projection); + expect(onlyA.length).to.be(1); + expect(onlyA).to.contain(a); + }); + }); }); diff --git a/test/node/ol/extent.test.js b/test/node/ol/extent.test.js index 28dbc2eec9..6313c2c058 100644 --- a/test/node/ol/extent.test.js +++ b/test/node/ol/extent.test.js @@ -964,4 +964,52 @@ describe('ol/extent.js', function () { ).to.be(false); }); }); + + describe('wrapAndSliceX', function () { + const projection = get('EPSG:4326'); + + it('leaves real world extent untouched', function () { + expect(_ol_extent_.wrapAndSliceX([16, 48, 18, 49], projection)).to.eql([ + [16, 48, 18, 49], + ]); + }); + + it('slices +180 crossing extents', function () { + expect(_ol_extent_.wrapAndSliceX([164, 48, 198, 49], projection)).to.eql([ + [164, 48, 180, 49], + [-180, 48, -162, 49], + ]); + + expect(_ol_extent_.wrapAndSliceX([178, 48, 198, 49], projection)).to.eql([ + [178, 48, 180, 49], + [-180, 48, -162, 49], + ]); + }); + + it('slices -180 crossing extents', function () { + expect( + _ol_extent_.wrapAndSliceX([-198, 48, -160, 49], projection) + ).to.eql([ + [162, 48, 180, 49], + [-180, 48, -160, 49], + ]); + + expect( + _ol_extent_.wrapAndSliceX([-202, 48, -160, 49], projection) + ).to.eql([ + [158, 48, 180, 49], + [-180, 48, -160, 49], + ]); + }); + + it('fits infinite extents to the projection extent', function () { + expect( + _ol_extent_.wrapAndSliceX([-Infinity, 48, -160, 49], projection) + ).to.eql([[-180, 48, 180, 49]]); + + expect( + _ol_extent_.wrapAndSliceX([-198, 48, Infinity, 49], projection) + ).to.eql([[-180, 48, 180, 49]]); + }); + }); });