From 51da380683a5c329b58d38bbbcf5d16d2a450fb6 Mon Sep 17 00:00:00 2001 From: taraadiseshan Date: Fri, 29 Jun 2018 15:06:18 -0700 Subject: [PATCH 1/6] Add CameraForBoxAndBearing function --- src/ui/camera.js | 46 +++++++++++++++++++++++++++++--------- src/ui/handler/box_zoom.js | 10 +++------ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/ui/camera.js b/src/ui/camera.js index 47e4fa6a77f..77ef895b93e 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -11,8 +11,8 @@ import { } from '../util/util'; import { number as interpolate } from '../style-spec/util/interpolate'; import browser from '../util/browser'; -import LngLat from '../geo/lng_lat'; import LngLatBounds from '../geo/lng_lat_bounds'; +import LngLat from '../geo/lng_lat'; import Point from '@mapbox/point-geometry'; import { Event, Evented } from '../util/evented'; @@ -374,6 +374,11 @@ 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); + } + + cameraForBoxAndBearing(p0: LngLat, p1: LngLat, bearing: number, options?: CameraOptions): void | CameraOptions & AnimationOptions { options = extend({ padding: { top: 0, @@ -405,8 +410,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 @@ -416,11 +419,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(p0); + const p1world = tr.project(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; @@ -430,10 +441,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; } @@ -477,6 +487,22 @@ class Camera extends Evented { this.flyTo(options, eventData); } + /** + * + */ + fitScreenCoordinates(p0: PointLike, p1: PointLike, bearing: number, options?: AnimationOptions & CameraOptions, eventData?: Object) { + const calculatedOptions = this.cameraForBoxAndBearing(this.transform.pointLocation(Point.convert(p0)), this.transform.pointLocation(Point.convert(p1)), bearing, options); + + // cameraForBounds warns + returns undefined if unable to fit: + if (!calculatedOptions) return this; + + options = extend(calculatedOptions, options); + + return options.linear ? + this.easeTo(options, eventData) : + this.flyTo(options, eventData); + } + /** * Changes any combination of center, zoom, bearing, and pitch, without * an animated transition. The map will retain its current values for any diff --git a/src/ui/handler/box_zoom.js b/src/ui/handler/box_zoom.js index 5f3724fb84f..3dadc54a9be 100644 --- a/src/ui/handler/box_zoom.js +++ b/src/ui/handler/box_zoom.js @@ -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'; @@ -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(); @@ -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})); } } From d4d48b90a422ddbf1326b16faaeba0b8f1680c9a Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Mon, 2 Jul 2018 16:43:37 -0700 Subject: [PATCH 2/6] - Add documentation for new public fit/camera methods. - Factor common code into _fitInternal method - Accept LatLngLike in cameraForBoxAndBearing --- src/ui/camera.js | 86 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/src/ui/camera.js b/src/ui/camera.js index 77ef895b93e..aeeb762e6d7 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -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. @@ -378,7 +378,30 @@ class Camera extends Evented { return this.cameraForBoxAndBearing(bounds.getNorthWest(), bounds.getSouthEast(), 0, options); } - cameraForBoxAndBearing(p0: LngLat, p1: LngLat, bearing: number, options?: CameraOptions): void | CameraOptions & AnimationOptions { + /** + * 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. + * @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, @@ -422,8 +445,8 @@ class Camera extends Evented { 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(p0); - const p1world = tr.project(p1); + 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); @@ -475,24 +498,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); - - // cameraForBounds warns + returns undefined if unable to fit: - if (!calculatedOptions) return this; - - options = extend(calculatedOptions, options); - - return options.linear ? - this.easeTo(options, eventData) : - this.flyTo(options, eventData); + 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) { - const calculatedOptions = this.cameraForBoxAndBearing(this.transform.pointLocation(Point.convert(p0)), this.transform.pointLocation(Point.convert(p1)), bearing, options); + 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; From cbc9fcaeb20298dbf12c99bb451d3242781fb345 Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Mon, 2 Jul 2018 17:12:46 -0700 Subject: [PATCH 3/6] Basic unit tests for fitScreenCoordinates. --- test/unit/ui/camera.test.js | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/unit/ui/camera.test.js b/test/unit/ui/camera.test.js index 85ac423580b..ed644259056 100644 --- a/test/unit/ui/camera.test.js +++ b/test/unit/ui/camera.test.js @@ -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(); }); From b31050f3b7a43340becbb55b979e064ddfb086b1 Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Fri, 17 Aug 2018 17:21:51 -0700 Subject: [PATCH 4/6] Add changelog entry for `fitScreenCoordinates`. Also: restore previous import order in camera.js. --- CHANGELOG.md | 5 +++++ src/ui/camera.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index add2c2cc2ab..69b44315528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/ui/camera.js b/src/ui/camera.js index aeeb762e6d7..a7c0d62509d 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -11,8 +11,8 @@ import { } from '../util/util'; import { number as interpolate } from '../style-spec/util/interpolate'; import browser from '../util/browser'; -import LngLatBounds from '../geo/lng_lat_bounds'; import LngLat from '../geo/lng_lat'; +import LngLatBounds from '../geo/lng_lat_bounds'; import Point from '@mapbox/point-geometry'; import { Event, Evented } from '../util/evented'; From adce3d14cb4d32fb70b661429ca085cc11ef729b Mon Sep 17 00:00:00 2001 From: taraadiseshan Date: Fri, 17 Aug 2018 16:45:33 -0700 Subject: [PATCH 5/6] Make cameraforboxandbearing private --- src/ui/camera.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/camera.js b/src/ui/camera.js index a7c0d62509d..b2d4c0efdb4 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -393,6 +393,7 @@ class Camera extends Evented { * @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]; @@ -401,7 +402,7 @@ class Camera extends Evented { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); */ - cameraForBoxAndBearing(p0: LngLatLike, p1: LngLatLike, bearing: number, options?: CameraOptions): void | CameraOptions & AnimationOptions { + _cameraForBoxAndBearing(p0: LngLatLike, p1: LngLatLike, bearing: number, options?: CameraOptions): void | CameraOptions & AnimationOptions { options = extend({ padding: { top: 0, From dd773101a4e03bf63042588fbb0bb596e9627aef Mon Sep 17 00:00:00 2001 From: taraadiseshan Date: Fri, 17 Aug 2018 16:54:48 -0700 Subject: [PATCH 6/6] Fix references --- src/ui/camera.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/camera.js b/src/ui/camera.js index b2d4c0efdb4..428677e2041 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -375,7 +375,7 @@ 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); + return this._cameraForBoxAndBearing(bounds.getNorthWest(), bounds.getSouthEast(), 0, options); } /** @@ -398,7 +398,7 @@ class Camera extends Evented { * var p0 = [-79, 43]; * var p1 = [-73, 45]; * var bearing = 90; - * var newCameraTransform = map.cameraForBoxAndBearing(p0, p1, bearing, { + * var newCameraTransform = map._cameraForBoxAndBearing(p0, p1, bearing, { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); */ @@ -536,7 +536,7 @@ class Camera extends Evented { */ fitScreenCoordinates(p0: PointLike, p1: PointLike, bearing: number, options?: AnimationOptions & CameraOptions, eventData?: Object) { return this._fitInternal( - this.cameraForBoxAndBearing( + this._cameraForBoxAndBearing( this.transform.pointLocation(Point.convert(p0)), this.transform.pointLocation(Point.convert(p1)), bearing,