Skip to content

Commit

Permalink
Fix potential infinite loop in zoomFromMercatorZAdjusted #12450 (#1…
Browse files Browse the repository at this point in the history
…2488)

- Prevent NaN by gating invalid values from the callee
- Prevent infinite loop by using a binary search instead of the previous method more prone to floating point comparison issues
- Add regression tests and new test for this function
  • Loading branch information
karimnaaji committed Jan 3, 2023
1 parent c28833d commit 781cdab
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 16 deletions.
53 changes: 38 additions & 15 deletions src/geo/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import assert from 'assert';
import getProjectionAdjustments, {getProjectionAdjustmentInverted, getScaleAdjustment, getProjectionInterpolationT} from './projection/adjustments.js';
import {getPixelsToTileUnitsMatrix} from '../source/pixels_to_tile_units.js';
import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id.js';
import {calculateGlobeMatrix, polesInViewport, GLOBE_ZOOM_THRESHOLD_MIN, GLOBE_SCALE_MATCH_LATITUDE} from '../geo/projection/globe_util.js';
import {
calculateGlobeMatrix,
polesInViewport,
GLOBE_ZOOM_THRESHOLD_MIN,
GLOBE_ZOOM_THRESHOLD_MAX,
GLOBE_SCALE_MATCH_LATITUDE
} from '../geo/projection/globe_util.js';
import {projectClamped} from '../symbol/projection.js';

import type Projection from '../geo/projection/projection.js';
Expand Down Expand Up @@ -2059,19 +2065,36 @@ class Transform {
// latitude and the center's latitude as you zoom in, camera to center distance varies dynamically.
// As the cameraToCenterDistance is a function of zoom, we need to approximate the true zoom
// given a mercator meter value in order to eliminate the zoom/cameraToCenterDistance dependency.
zoomFromMercatorZAdjusted(z: number): number {
const getZoom = (zoom) => {
const d = this.getCameraToCenterDistance(this.projection, zoom);
return this.scaleZoom(d / (z * this.tileSize));
};
zoomFromMercatorZAdjusted(mercatorZ: number): number {
assert(this.projection.name === 'globe');
assert(mercatorZ !== 0);

let zoomLow = 0;
let zoomHigh = GLOBE_ZOOM_THRESHOLD_MAX;
let zoom = 0;
let minZoomDiff = Infinity;

const epsilon = 1e-6;

while (zoomHigh - zoomLow > epsilon && zoomHigh > zoomLow) {
const zoomMid = zoomLow + (zoomHigh - zoomLow) * 0.5;

const worldSize = this.tileSize * Math.pow(2, zoomMid);
const d = this.getCameraToCenterDistance(this.projection, zoomMid, worldSize);
const newZoom = this.scaleZoom(d / (mercatorZ * this.tileSize));

let zoom = getZoom(this.zoom);
let diff = Math.abs(zoom - getZoom(zoom));
let lastdiff;
while (lastdiff !== diff) {
zoom = getZoom(zoom);
lastdiff = diff;
diff = Math.abs(zoom - getZoom(zoom));
const diff = Math.abs(zoomMid - newZoom);

if (diff < minZoomDiff) {
minZoomDiff = diff;
zoom = zoomMid;
}

if (zoomMid < newZoom) {
zoomLow = zoomMid;
} else {
zoomHigh = zoomMid;
}
}

return zoom;
Expand Down Expand Up @@ -2183,9 +2206,9 @@ class Transform {
}
}

getCameraToCenterDistance(projection: Projection, zoom: number = this.zoom): number {
getCameraToCenterDistance(projection: Projection, zoom: number = this.zoom, worldSize: number = this.worldSize): number {
const t = getProjectionInterpolationT(projection, zoom, this.width, this.height, 1024);
const projectionScaler = projection.pixelSpaceConversion(this.center.lat, this.worldSize, t);
const projectionScaler = projection.pixelSpaceConversion(this.center.lat, worldSize, t);
return 0.5 / Math.tan(this._fov * 0.5) * this.height * projectionScaler;
}

Expand Down
2 changes: 1 addition & 1 deletion src/ui/camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ class Camera extends Evented {
const meterPerECEF = earthRadius / GLOBE_RADIUS;
const altitudeECEF = vec3.length(cameraPosition);
const altitudeMeter = altitudeECEF * meterPerECEF - earthRadius;
const mercatorZ = mercatorZfromAltitude(altitudeMeter, 0);
const mercatorZ = mercatorZfromAltitude(Math.max(altitudeMeter, Number.EPSILON), 0);

const zoom = Math.min(tr.zoomFromMercatorZAdjusted(mercatorZ), eOptions.maxZoom);

Expand Down
19 changes: 19 additions & 0 deletions test/unit/geo/transform.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,25 @@ test('transform', (t) => {
t.end();
});

t.test("zoomFromMercatorZAdjusted", (t) => {
const transform = new Transform();
transform.resize(500, 500);
t.ok(transform.setProjection({name: 'globe'}));

for (let zoom = 0; zoom < 6; ++zoom) {
transform.zoom = zoom;

t.equal(fixedNum(transform.zoomFromMercatorZAdjusted(mercatorZfromAltitude(1e3, 0)), 3), 6);
t.equal(fixedNum(transform.zoomFromMercatorZAdjusted(mercatorZfromAltitude(1e6, 0)), 3), 5.874);
t.equal(fixedNum(transform.zoomFromMercatorZAdjusted(mercatorZfromAltitude(4e6, 0)), 3), 3.87);
t.equal(fixedNum(transform.zoomFromMercatorZAdjusted(mercatorZfromAltitude(1e7, 0)), 3), 2.054);
t.equal(fixedNum(transform.zoomFromMercatorZAdjusted(mercatorZfromAltitude(2e7, 0)), 3), 1.052);
t.equal(fixedNum(transform.zoomFromMercatorZAdjusted(mercatorZfromAltitude(5e7, 0)), 3), 0);
}

t.end();
});

t.test("ZoomDeltaToMovement", (t) => {
const transform = new Transform();
transform.resize(100, 100);
Expand Down
16 changes: 16 additions & 0 deletions test/unit/ui/camera.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {fixedLngLat, fixedNum, fixedVec3} from '../../util/fixed.js';
import {equalWithPrecision} from '../../util/index.js';
import MercatorCoordinate from '../../../src/geo/mercator_coordinate.js';
import LngLat from '../../../src/geo/lng_lat.js';
import LngLatBounds from '../../../src/geo/lng_lat_bounds.js';
import {vec3, quat} from 'gl-matrix';

test('camera', (t) => {
Expand Down Expand Up @@ -2305,6 +2306,21 @@ test('camera', (t) => {
t.end();
});

t.test('#12450', (t) => {
const camera = createCamera();

camera.setCenter([-115.6288447, 35.1509267]);
camera.setZoom(5);

const bounds = new LngLatBounds();
bounds.extend([-115.6288447, 35.1509267]);
camera.fitBounds(bounds, {padding: 75, duration: 0});

t.deepEqual(fixedLngLat(camera.getCenter(), 4), {lng: -115.6288, lat: 35.1509});
t.equal(camera.getZoom(), 20);
t.end();
});

t.end();
});

Expand Down

0 comments on commit 781cdab

Please sign in to comment.