Skip to content

Commit

Permalink
Merge pull request #4 from windycom/kubapelc/tilebounds-antimeridian-fix
Browse files Browse the repository at this point in the history
TileBounds supports ranges that cross the antimeridian
  • Loading branch information
kubapelc authored Oct 29, 2024
2 parents 260ec5e + b15b06e commit 3b2b002
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 9 deletions.
56 changes: 56 additions & 0 deletions src/source/tile_bounds.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {TileBounds} from './tile_bounds';
import {CanonicalTileID} from './tile_id';

describe('TileBounds', () => {
test('default', () => {
const bounds = new TileBounds([-180, -90, 180, 90]);
expect(bounds.contains(new CanonicalTileID(2, 0, 0))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 1, 1))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 2, 2))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 3, 3))).toBe(true);
});

describe('latitude', () => {
test('is clamped', () => {
const bounds = new TileBounds([-180, -900, 180, 900]);
expect(bounds.contains(new CanonicalTileID(2, 0, 0))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 1, 1))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 2, 2))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 3, 3))).toBe(true);
});

test('limits extent', () => {
const bounds = new TileBounds([-180, -45, 180, 45]);
expect(bounds.contains(new CanonicalTileID(2, 0, 0))).toBe(false);
expect(bounds.contains(new CanonicalTileID(2, 1, 1))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 2, 2))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 3, 3))).toBe(false);
});
});

describe('longitude with wrapping', () => {
test('half range', () => {
const bounds = new TileBounds([0, -90, 180, 90]);
expect(bounds.contains(new CanonicalTileID(2, 0, 0))).toBe(false);
expect(bounds.contains(new CanonicalTileID(2, 1, 1))).toBe(false);
expect(bounds.contains(new CanonicalTileID(2, 2, 2))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 3, 3))).toBe(true);
});

test('wrapped positive', () => {
const bounds = new TileBounds([0, -90, 270, 90]);
expect(bounds.contains(new CanonicalTileID(2, 0, 0))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 1, 1))).toBe(false);
expect(bounds.contains(new CanonicalTileID(2, 2, 2))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 3, 3))).toBe(true);
});

test('wrapped negative', () => {
const bounds = new TileBounds([-270, -90, 0, 90]);
expect(bounds.contains(new CanonicalTileID(2, 0, 0))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 1, 1))).toBe(true);
expect(bounds.contains(new CanonicalTileID(2, 2, 2))).toBe(false);
expect(bounds.contains(new CanonicalTileID(2, 3, 3))).toBe(true);
});
});
});
38 changes: 29 additions & 9 deletions src/source/tile_bounds.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import {LngLatBounds, LngLatBoundsLike} from '../geo/lng_lat_bounds';
import {mercatorXfromLng, mercatorYfromLat} from '../geo/mercator_coordinate';
import {mod} from '../util/util';

import type {CanonicalTileID} from './tile_id';

function fract(x: number): number {
return x - Math.floor(x);
}

export class TileBounds {
/**
* Coordinate bounds. Longitude is *not* clamped to [-180, 180]!
*/
bounds: LngLatBounds;
minzoom: number;
maxzoom: number;
Expand All @@ -17,18 +25,30 @@ export class TileBounds {
validateBounds(bounds: [number, number, number, number]): LngLatBoundsLike {
// make sure the bounds property contains valid longitude and latitudes
if (!Array.isArray(bounds) || bounds.length !== 4) return [-180, -90, 180, 90];
return [Math.max(-180, bounds[0]), Math.max(-90, bounds[1]), Math.min(180, bounds[2]), Math.min(90, bounds[3])];
return [bounds[0], Math.max(-90, bounds[1]), bounds[2], Math.min(90, bounds[3])];
}

contains(tileID: CanonicalTileID) {
const worldSize = Math.pow(2, tileID.z);
const level = {
minX: Math.floor(mercatorXfromLng(this.bounds.getWest()) * worldSize),
minY: Math.floor(mercatorYfromLat(this.bounds.getNorth()) * worldSize),
maxX: Math.ceil(mercatorXfromLng(this.bounds.getEast()) * worldSize),
maxY: Math.ceil(mercatorYfromLat(this.bounds.getSouth()) * worldSize)
};
const hit = tileID.x >= level.minX && tileID.x < level.maxX && tileID.y >= level.minY && tileID.y < level.maxY;
return hit;

// Latitude test
const minY = Math.floor(mercatorYfromLat(this.bounds.getNorth()) * worldSize);
const maxY = Math.ceil(mercatorYfromLat(this.bounds.getSouth()) * worldSize);

if (tileID.y < minY || tileID.y >= maxY) {
return false;
}

// Longitude test with wrapping around the globe
let minX = fract(mercatorXfromLng(this.bounds.getWest()));
let maxX = fract(mercatorXfromLng(this.bounds.getEast()));
if (minX >= maxX) {
maxX += 1;
}
minX = Math.floor(minX * worldSize);
maxX = Math.ceil(maxX * worldSize);

const wrappedTileX = mod(tileID.x, worldSize);
return (wrappedTileX >= minX && wrappedTileX < maxX) || (wrappedTileX + worldSize >= minX && wrappedTileX + worldSize < maxX);
}
}

0 comments on commit 3b2b002

Please sign in to comment.