Skip to content

Commit

Permalink
Add some more bounds checking and tests in project/unproject
Browse files Browse the repository at this point in the history
Don't allow latitude to exceed ±85.05112877980659°
Web Mercator shouldn't fetch tiles beyond this limit
and the projected y coordinate will approach Infinity
  • Loading branch information
bhousel committed Feb 21, 2024
1 parent a552588 commit d199102
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 20 deletions.
16 changes: 10 additions & 6 deletions packages/math/src/Viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const MAXZOOM = 24;
const MINK = geoZoomToScale(MINZOOM, TILESIZE);
const MAXK = geoZoomToScale(MAXZOOM, TILESIZE);

const MAXPHI = 2 * Math.atan(Math.exp(Math.PI)) - HALF_PI; // 85.0511287798 in radians
const MINPHI = -MAXPHI;

function clamp(num: number, min: number, max: number): number {
return Math.max(min, Math.min(num, max));
Expand Down Expand Up @@ -80,9 +82,10 @@ export class Viewport {
project(loc: Vec2): Vec2 {
const { x, y, k, r } = this._transform;
const lambda: number = loc[0] * DEG2RAD;
const phi: number = clamp(loc[1], -85.0511287798, 85.0511287798) * DEG2RAD;
const mercator: Vec2 = [lambda, Math.log(Math.tan((HALF_PI + phi) / 2))];
const point: Vec2 = [mercator[0] * k + x, y - mercator[1] * k];
const phi: number = clamp(loc[1] * DEG2RAD, MINPHI, MAXPHI);
const mercatorX: number = lambda
const mercatorY: number = Math.log(Math.tan((HALF_PI + phi) / 2));
const point: Vec2 = [mercatorX * k + x, y - mercatorY * k];
if (r) {
return vecRotate(point, r, this._dimensions.center());
} else {
Expand All @@ -106,9 +109,10 @@ export class Viewport {
if (r) {
point = vecRotate(point, -r, this._dimensions.center());
}
const mercator: Vec2 = [(point[0] - x) / k, (y - point[1]) / k];
const lambda: number = mercator[0];
const phi: number = 2 * Math.atan(Math.exp(mercator[1])) - HALF_PI;
const mercatorX: number = (point[0] - x) / k;
const mercatorY: number = clamp((y - point[1]) / k, -Math.PI, Math.PI);
const lambda: number = mercatorX;
const phi: number = 2 * Math.atan(Math.exp(mercatorY)) - HALF_PI;
return [lambda * RAD2DEG, phi * RAD2DEG];
}

Expand Down
108 changes: 94 additions & 14 deletions packages/math/test/Viewport.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,39 +113,55 @@ describe('math/viewport', () => {

describe('z1', () => {
it('Projects [0°, 0°] -> [0, 0] (at z1)', () => {
const view = new Viewport();
const view = new Viewport({ k: 256 / Math.PI });
const point = view.project([0, 0]);
assert.ok(point instanceof Array);
assert.closeTo(point[0], 0);
assert.closeTo(point[1], 0);
});

it('Projects [180°, -85.0511287798°] -> [256, 256] (at z1)', () => {
const view = new Viewport();
const view = new Viewport({ k: 256 / Math.PI });
const point = view.project([180, -85.0511287798]);
assert.ok(point instanceof Array);
assert.closeTo(point[0], 256);
assert.closeTo(point[1], 256);
});

it('Projects out of bounds [270°, -95°] -> [384, 256] (at z1)', () => {
const view = new Viewport({ k: 256 / Math.PI });
const point = view.project([270, -95]);
assert.ok(point instanceof Array);
assert.closeTo(point[0], 384);
assert.closeTo(point[1], 256);
});

it('Projects [-180°, 85.0511287798°] -> [-256, -256] (at z1)', () => {
const view = new Viewport();
const view = new Viewport({ k: 256 / Math.PI });
const point = view.project([-180, 85.0511287798]);
assert.ok(point instanceof Array);
assert.closeTo(point[0], -256);
assert.closeTo(point[1], -256);
});

it('Projects out of bounds [-270°, 95°] -> [-384, -256] (at z1)', () => {
const view = new Viewport({ k: 256 / Math.PI });
const point = view.project([-270, 95]);
assert.ok(point instanceof Array);
assert.closeTo(point[0], -384);
assert.closeTo(point[1], -256);
});

it('Applies translation when projecting (at z1)', () => {
const view = new Viewport({ x: 20, y: 30 });
const view = new Viewport({ x: 20, y: 30, k: 256 / Math.PI });
const point = view.project([-180, 85.0511287798]);
assert.ok(point instanceof Array);
assert.closeTo(point[0], -236);
assert.closeTo(point[1], -226);
});

it('Applies rotation when projecting (at z1)', () => {
const view = new Viewport({ r: Math.PI / 2 }); // quarter turn clockwise
const view = new Viewport({ k: 256 / Math.PI, r: Math.PI / 2 }); // quarter turn clockwise
const point = view.project([180, 0]);
assert.ok(point instanceof Array);
assert.closeTo(point[0], 0);
Expand All @@ -170,6 +186,14 @@ describe('math/viewport', () => {
assert.closeTo(point[1], 512);
});

it('Projects out of bounds [270°, -95°] -> [768, 512] (at z2)', () => {
const view = new Viewport({ k: 512 / Math.PI });
const point = view.project([270, -95]);
assert.ok(point instanceof Array);
assert.closeTo(point[0], 768);
assert.closeTo(point[1], 512);
});

it('Projects [-180°, 85.0511287798°] -> [-512, -512] (at z2)', () => {
const view = new Viewport({ k: 512 / Math.PI });
const point = view.project([-180, 85.0511287798]);
Expand All @@ -178,6 +202,14 @@ describe('math/viewport', () => {
assert.closeTo(point[1], -512);
});

it('Projects out of bounds [-270°, 95°] -> [-768, -512] (at z2)', () => {
const view = new Viewport({ k: 512 / Math.PI });
const point = view.project([-270, 95]);
assert.ok(point instanceof Array);
assert.closeTo(point[0], -768);
assert.closeTo(point[1], -512);
});

it('Applies translation when projecting (at z2)', () => {
const view = new Viewport({ x: 20, y: 30, k: 512 / Math.PI });
const point = view.project([-180, 85.0511287798]);
Expand Down Expand Up @@ -214,6 +246,14 @@ describe('math/viewport', () => {
assert.closeTo(loc[1], -85.0511287798);
});

it('Unprojects out of bounds [192, Infinity] -> [270°, -85.0511287798°] (at z0)', () => {
const view = new Viewport({ k: 128 / Math.PI });
const loc = view.unproject([192, Infinity]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], 270);
assert.closeTo(loc[1], -85.0511287798);
});

it('Unprojects [-128, -128] -> [-180°, 85.0511287798°] (at z0)', () => {
const view = new Viewport({ k: 128 / Math.PI });
const loc = view.unproject([-128, -128]);
Expand All @@ -222,15 +262,23 @@ describe('math/viewport', () => {
assert.closeTo(loc[1], 85.0511287798);
});

it('Applies translation when unprojecting at (z0)', () => {
it('Unprojects out of bounds [-192, -Infinity] -> [-270°, 85.0511287798°] (at z0)', () => {
const view = new Viewport({ k: 128 / Math.PI });
const loc = view.unproject([-192, -Infinity]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], -270);
assert.closeTo(loc[1], 85.0511287798);
});

it('Applies translation when unprojecting (at z0)', () => {
const view = new Viewport({ x: 20, y: 30, k: 128 / Math.PI });
const loc = view.unproject([-108, -98]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], -180);
assert.closeTo(loc[1], 85.0511287798);
});

it('Applies rotation when unprojecting at (z0)', () => {
it('Applies rotation when unprojecting (at z0)', () => {
const view = new Viewport({ k: 128 / Math.PI, r: Math.PI / 2 }); // quarter turn clockwise
const loc = view.unproject([0, 128]);
assert.ok(loc instanceof Array);
Expand All @@ -241,39 +289,55 @@ describe('math/viewport', () => {

describe('z1', () => {
it('Unprojects [0, 0] -> [0°, 0°] (at z1)', () => {
const view = new Viewport();
const view = new Viewport({ k: 256 / Math.PI });
const loc = view.unproject([0, 0]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], 0);
assert.closeTo(loc[1], 0);
});

it('Unprojects [256, 256] -> [180°, -85.0511287798°] (at z1)', () => {
const view = new Viewport();
const view = new Viewport({ k: 256 / Math.PI });
const loc = view.unproject([256, 256]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], 180);
assert.closeTo(loc[1], -85.0511287798);
});

it('Unprojects out of bounds [384, Infinity] -> [270°, -85.0511287798°] (at z1)', () => {
const view = new Viewport({ k: 256 / Math.PI });
const loc = view.unproject([384, Infinity]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], 270);
assert.closeTo(loc[1], -85.0511287798);
});

it('Unprojects [-256, -256] -> [-180°, 85.0511287798°] (at z1)', () => {
const view = new Viewport();
const view = new Viewport({ k: 256 / Math.PI });
const loc = view.unproject([-256, -256]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], -180);
assert.closeTo(loc[1], 85.0511287798);
});

it('Unprojects out of bounds [-384, -Infinity] -> [-270°, 85.0511287798°] (at z1)', () => {
const view = new Viewport({ k: 256 / Math.PI });
const loc = view.unproject([-384, -Infinity]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], -270);
assert.closeTo(loc[1], 85.0511287798);
});

it('Applies translation when unprojecting (at z1)', () => {
const view = new Viewport({ x: 20, y: 30 });
const view = new Viewport({ x: 20, y: 30, k: 256 / Math.PI });
const loc = view.unproject([-236, -226]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], -180);
assert.closeTo(loc[1], 85.0511287798);
});

it('Applies rotation when unprojecting at (z1)', () => {
const view = new Viewport({ r: Math.PI / 2 }); // quarter turn clockwise
it('Applies rotation when unprojecting (at z1)', () => {
const view = new Viewport({ k: 256 / Math.PI, r: Math.PI / 2 }); // quarter turn clockwise
const loc = view.unproject([0, 256]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], 180);
Expand All @@ -298,6 +362,14 @@ describe('math/viewport', () => {
assert.closeTo(loc[1], -85.0511287798);
});

it('Unprojects out of bounds [768, Infinity] -> [270°, -85.0511287798°] (at z2)', () => {
const view = new Viewport({ k: 512 / Math.PI });
const loc = view.unproject([768, Infinity]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], 270);
assert.closeTo(loc[1], -85.0511287798);
});

it('Unprojects [-512, -512] -> [-180°, 85.0511287798°] (at z2)', () => {
const view = new Viewport({ k: 512 / Math.PI });
const loc = view.unproject([-512, -512]);
Expand All @@ -306,6 +378,14 @@ describe('math/viewport', () => {
assert.closeTo(loc[1], 85.0511287798);
});

it('Unprojects out of bounds [-768, -Infinity] -> [-270°, 85.0511287798°] (at z2)', () => {
const view = new Viewport({ k: 512 / Math.PI });
const loc = view.unproject([-768, -Infinity]);
assert.ok(loc instanceof Array);
assert.closeTo(loc[0], -270);
assert.closeTo(loc[1], 85.0511287798);
});

it('Applies translation when unprojecting (at z2)', () => {
const view = new Viewport({ x: 20, y: 30, k: 512 / Math.PI });
const loc = view.unproject([-492, -482]);
Expand All @@ -314,7 +394,7 @@ describe('math/viewport', () => {
assert.closeTo(loc[1], 85.0511287798);
});

it('Applies rotation when unprojecting at (z2)', () => {
it('Applies rotation when unprojecting (at z2)', () => {
const view = new Viewport({ k: 512 / Math.PI, r: Math.PI / 2 }); // quarter turn clockwise
const loc = view.unproject([0, 512]);
assert.ok(loc instanceof Array);
Expand Down

0 comments on commit d199102

Please sign in to comment.