Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CameraForBoxAndBearing function #6894

Merged
merged 6 commits into from
Aug 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## master
### ✨ Features and improvements
* Add `Map#fitScreenCoordinates`: fits viewport to two points, similar to `Map#fitBounds` but uses screen coordinates and supports non-zero map bearings. ([#6894](https://github.com/mapbox/mapbox-gl-js/pull/6894))


## 0.47.0

## ✨ Features and improvements
Expand Down
103 changes: 91 additions & 12 deletions src/ui/camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,9 @@ class Camera extends Evented {

/**
* @memberof Map#
* @param bounds Calculate the center for these bounds in the viewport and use
* @param {LatLngBoundsLike} bounds Calculate the center for these bounds in the viewport and use
* the highest zoom level up to and including `Map#getMaxZoom()` that fits
* in the viewport.
* in the viewport. LatLngBounds represent a box that is always axis-aligned with bearing 0.
* @param options
* @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
* @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
Expand All @@ -374,6 +374,35 @@ class Camera extends Evented {
* });
*/
cameraForBounds(bounds: LngLatBoundsLike, options?: CameraOptions): void | CameraOptions & AnimationOptions {
bounds = LngLatBounds.convert(bounds);
return this._cameraForBoxAndBearing(bounds.getNorthWest(), bounds.getSouthEast(), 0, options);
}

/**
* Calculate the center of these two points in the viewport and use
* the highest zoom level up to and including `Map#getMaxZoom()` that fits
* the points in the viewport at the specified bearing.
* @memberof Map#
* @param {LngLatLike} p0 First point
* @param {LngLatLike} p1 Second point
* @param bearing Desired map bearing at end of animation, in degrees
* @param options
* @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
* @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
* @param {number} [options.maxZoom] The maximum zoom level to allow when the camera would transition to the specified bounds.
* @returns {CameraOptions | void} If map is able to fit to provided bounds, returns `CameraOptions` with
* at least `center`, `zoom`, `bearing`, `offset`, `padding`, and `maxZoom`, as well as any other
* `options` provided in arguments. If map is unable to fit, method will warn and return undefined.
* @private
* @example
* var p0 = [-79, 43];
* var p1 = [-73, 45];
* var bearing = 90;
* var newCameraTransform = map._cameraForBoxAndBearing(p0, p1, bearing, {
* padding: {top: 10, bottom:25, left: 15, right: 5}
* });
*/
_cameraForBoxAndBearing(p0: LngLatLike, p1: LngLatLike, bearing: number, options?: CameraOptions): void | CameraOptions & AnimationOptions {
options = extend({
padding: {
top: 0,
Expand Down Expand Up @@ -405,8 +434,6 @@ class Camera extends Evented {
return;
}

bounds = LngLatBounds.convert(bounds);

// we separate the passed padding option into two parts, the part that does not affect the map's center
// (lateral and vertical padding), and the part that does (paddingOffset). We add the padding offset
// to the options `offset` object where it can alter the map's center in the subsequent calls to
Expand All @@ -416,11 +443,19 @@ class Camera extends Evented {
verticalPadding = Math.min(options.padding.top, options.padding.bottom);
options.offset = [options.offset[0] + paddingOffset[0], options.offset[1] + paddingOffset[1]];

const tr = this.transform;
// we want to calculate the upper right and lower left of the box defined by p0 and p1
// in a coordinate system rotate to match the destination bearing.
const p0world = tr.project(LngLat.convert(p0));
const p1world = tr.project(LngLat.convert(p1));
const p0rotated = p0world.rotate(-bearing * Math.PI / 180);
const p1rotated = p1world.rotate(-bearing * Math.PI / 180);

const upperRight = new Point(Math.max(p0rotated.x, p1rotated.x), Math.max(p0rotated.y, p1rotated.y));
const lowerLeft = new Point(Math.min(p0rotated.x, p1rotated.x), Math.min(p0rotated.y, p1rotated.y));

const offset = Point.convert(options.offset),
tr = this.transform,
nw = tr.project(bounds.getNorthWest()),
se = tr.project(bounds.getSouthEast()),
size = se.sub(nw),
size = upperRight.sub(lowerLeft),
scaleX = (tr.width - lateralPadding * 2 - Math.abs(offset.x) * 2) / size.x,
scaleY = (tr.height - verticalPadding * 2 - Math.abs(offset.y) * 2) / size.y;

Expand All @@ -430,10 +465,9 @@ class Camera extends Evented {
);
return;
}

options.center = tr.unproject(nw.add(se).div(2));
options.center = tr.unproject(p0world.add(p1world).div(2));
options.zoom = Math.min(tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), options.maxZoom);
options.bearing = 0;
options.bearing = bearing;

return options;
}
Expand Down Expand Up @@ -465,8 +499,53 @@ class Camera extends Evented {
* @see [Fit a map to a bounding box](https://www.mapbox.com/mapbox-gl-js/example/fitbounds/)
*/
fitBounds(bounds: LngLatBoundsLike, options?: AnimationOptions & CameraOptions, eventData?: Object) {
const calculatedOptions = this.cameraForBounds(bounds, options);
return this._fitInternal(
this.cameraForBounds(bounds, options),
options,
eventData);
}

/**
* Pans, rotates and zooms the map to to fit the box made by points p0 and p1
* once the map is rotated to the specified bearing. To zoom without rotating,
* pass in the current map bearing.
*
* @memberof Map#
* @param p0 First point on screen, in pixel coordinates
* @param p1 Second point on screen, in pixel coordinates
* @param bearing Desired map bearing at end of animation, in degrees
* @param options
* @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
* @param {boolean} [options.linear=false] If `true`, the map transitions using
* {@link Map#easeTo}. If `false`, the map transitions using {@link Map#flyTo}. See
* those functions and {@link AnimationOptions} for information about options available.
* @param {Function} [options.easing] An easing function for the animated transition. See {@link AnimationOptions}.
* @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
* @param {number} [options.maxZoom] The maximum zoom level to allow when the map view transitions to the specified bounds.
* @param eventData Additional properties to be added to event objects of events triggered by this method.
* @fires movestart
* @fires moveend
* @returns {Map} `this`
* @example
* var p0 = [220, 400];
* var p1 = [500, 900];
* map.fitScreenCoordintes(p0, p1, map.getBearing(), {
* padding: {top: 10, bottom:25, left: 15, right: 5}
* });
* @see [Used by BoxZoomHandler](https://www.mapbox.com/mapbox-gl-js/api/#boxzoomhandler)
*/
fitScreenCoordinates(p0: PointLike, p1: PointLike, bearing: number, options?: AnimationOptions & CameraOptions, eventData?: Object) {
return this._fitInternal(
this._cameraForBoxAndBearing(
this.transform.pointLocation(Point.convert(p0)),
this.transform.pointLocation(Point.convert(p1)),
bearing,
options),
options,
eventData);
}

_fitInternal(calculatedOptions?: CameraOptions & AnimationOptions, options?: AnimationOptions & CameraOptions, eventData?: Object) {
// cameraForBounds warns + returns undefined if unable to fit:
if (!calculatedOptions) return this;

Expand Down
10 changes: 3 additions & 7 deletions src/ui/handler/box_zoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import DOM from '../../util/dom';

import LngLatBounds from '../../geo/lng_lat_bounds';
import { bindAll } from '../../util/util';
import window from '../../util/window';
import { Event } from '../../util/evented';
Expand Down Expand Up @@ -126,10 +125,7 @@ class BoxZoomHandler {
if (e.button !== 0) return;

const p0 = this._startPos,
p1 = DOM.mousePos(this._el, e),
bounds = new LngLatBounds()
.extend(this._map.unproject(p0))
.extend(this._map.unproject(p1));
p1 = DOM.mousePos(this._el, e);

this._finish();

Expand All @@ -139,8 +135,8 @@ class BoxZoomHandler {
this._fireEvent('boxzoomcancel', e);
} else {
this._map
.fitBounds(bounds, {linear: true})
.fire(new Event('boxzoomend', { originalEvent: e, boxZoomBounds: bounds }));
.fitScreenCoordinates(p0, p1, this._map.getBearing(), {linear: true})
.fire(new Event('boxzoomend', { originalEvent: e}));
}
}

Expand Down
45 changes: 45 additions & 0 deletions test/unit/ui/camera.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1737,5 +1737,50 @@ test('camera', (t) => {
t.end();
});

t.test('#fitScreenCoordinates', (t) => {
t.test('bearing 225', (t) => {
const camera = createCamera();
const p0 = [128, 128];
const p1 = [256, 256];
const bearing = 225;

camera.fitScreenCoordinates(p0, p1, bearing, {duration:0});
t.deepEqual(fixedLngLat(camera.getCenter(), 4), { lng: -45, lat: 40.9799 }, 'centers, rotates 225 degrees, and zooms based on screen coordinates');
t.equal(fixedNum(camera.getZoom(), 3), 1.5);
t.equal(camera.getBearing(), -135);
t.end();
});

t.test('bearing 0', (t) => {
const camera = createCamera();

const p0 = [128, 128];
const p1 = [256, 256];
const bearing = 0;

camera.fitScreenCoordinates(p0, p1, bearing, {duration:0});
t.deepEqual(fixedLngLat(camera.getCenter(), 4), { lng: -45, lat: 40.9799 }, 'centers and zooms in based on screen coordinates');
t.equal(fixedNum(camera.getZoom(), 3), 2);
t.equal(camera.getBearing(), 0);
t.end();
});

t.test('inverted points', (t) => {
const camera = createCamera();
const p1 = [128, 128];
const p0 = [256, 256];
const bearing = 0;

camera.fitScreenCoordinates(p0, p1, bearing, {duration:0});
t.deepEqual(fixedLngLat(camera.getCenter(), 4), { lng: -45, lat: 40.9799 }, 'centers and zooms based on screen coordinates in opposite order');
t.equal(fixedNum(camera.getZoom(), 3), 2);
t.equal(camera.getBearing(), 0);
t.end();
});

t.end();
});


t.end();
});