UNPKG

mapbox-gl

Version:
1,190 lines (1,071 loc) 72.2 kB
// @flow import { bindAll, extend, warnOnce, clamp, wrap, ease as defaultEasing, pick, degToRad } from '../util/util.js'; import {number as interpolate} from '../style-spec/util/interpolate.js'; import browser from '../util/browser.js'; import LngLat from '../geo/lng_lat.js'; import LngLatBounds from '../geo/lng_lat_bounds.js'; import Point from '@mapbox/point-geometry'; import {Event, Evented} from '../util/evented.js'; import assert from 'assert'; import {Debug} from '../util/debug.js'; import MercatorCoordinate, {mercatorZfromAltitude, mercatorXfromLng, mercatorYfromLat} from '../geo/mercator_coordinate.js'; import {vec3} from 'gl-matrix'; import type {FreeCameraOptions} from './free_camera.js'; import type Transform from '../geo/transform.js'; import type {LngLatLike} from '../geo/lng_lat.js'; import type {LngLatBoundsLike} from '../geo/lng_lat_bounds.js'; import type {TaskID} from '../util/task_queue.js'; import type {Callback} from '../types/callback.js'; import type {PointLike} from '@mapbox/point-geometry'; import {Aabb, Frustum} from '../util/primitives.js'; import type {PaddingOptions} from '../geo/edge_insets.js'; import type {Vec3} from 'gl-matrix'; /** * A helper type: converts all Object type values to non-maybe types. */ type Required<T> = $ObjMap<T, <V>(v: V) => $NonMaybeType<V>>; /** * Options common to {@link Map#jumpTo}, {@link Map#easeTo}, and {@link Map#flyTo}, controlling the desired location, * zoom, bearing, and pitch of the camera. All properties are optional, and when a property is omitted, the current * camera value for that property will remain unchanged. * * @typedef {Object} CameraOptions * @property {LngLatLike} center The desired center. * @property {number} zoom The desired zoom level. * @property {number} bearing The desired bearing in degrees. The bearing is the compass direction that * is "up". For example, `bearing: 90` orients the map so that east is up. * @property {number} pitch The desired pitch in degrees. The pitch is the angle towards the horizon * measured in degrees with a range between 0 and 60 degrees. For example, pitch: 0 provides the appearance * of looking straight down at the map, while pitch: 60 tilts the user's perspective towards the horizon. * Increasing the pitch value is often used to display 3D objects. * @property {LngLatLike} around If `zoom` is specified, `around` determines the point around which the zoom is centered. * @property {PaddingOptions} padding Dimensions in pixels applied on each side of the viewport for shifting the vanishing point. * @example * // set the map's initial perspective with CameraOptions * const map = new mapboxgl.Map({ * container: 'map', * style: 'mapbox://styles/mapbox/streets-v11', * center: [-73.5804, 45.53483], * pitch: 60, * bearing: -60, * zoom: 10 * }); * @see [Example: Set pitch and bearing](https://docs.mapbox.com/mapbox-gl-js/example/set-perspective/) * @see [Example: Jump to a series of locations](https://docs.mapbox.com/mapbox-gl-js/example/jump-to/) * @see [Example: Fly to a location](https://docs.mapbox.com/mapbox-gl-js/example/flyto/) * @see [Example: Display buildings in 3D](https://docs.mapbox.com/mapbox-gl-js/example/3d-buildings/) */ export type CameraOptions = { center?: LngLatLike, zoom?: number, bearing?: number, pitch?: number, around?: LngLatLike, padding?: PaddingOptions, offset?: PointLike }; export type FullCameraOptions = { maxZoom: number, offset: PointLike, padding: Required<PaddingOptions> } & CameraOptions /** * Options common to map movement methods that involve animation, such as {@link Map#panBy} and * {@link Map#easeTo}, controlling the duration and easing function of the animation. All properties * are optional. * * @typedef {Object} AnimationOptions * @property {number} duration The animation's duration, measured in milliseconds. * @property {Function} easing A function taking a time in the range 0..1 and returning a number where 0 is * the initial state and 1 is the final state. * @property {PointLike} offset The target center's offset relative to real map container center at the end of animation. * @property {boolean} animate If `false`, no animation will occur. * @property {boolean} essential If `true`, then the animation is considered essential and will not be affected by * [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). * @see [Example: Slowly fly to a location](https://docs.mapbox.com/mapbox-gl-js/example/flyto-options/) * @see [Example: Customize camera animations](https://docs.mapbox.com/mapbox-gl-js/example/camera-animation/) * @see [Example: Navigate the map with game-like controls](https://docs.mapbox.com/mapbox-gl-js/example/game-controls/) */ export type AnimationOptions = { duration?: number, easing?: (_: number) => number, offset?: PointLike, animate?: boolean, essential?: boolean }; export type EasingOptions = CameraOptions & AnimationOptions; export type ElevationBoxRaycast = { minLngLat: LngLat, maxLngLat: LngLat, minAltitude: number, maxAltitude: number }; const freeCameraNotSupportedWarning = 'map.setFreeCameraOptions(...) and map.getFreeCameraOptions() are not yet supported for non-mercator projections.'; /** * Options for setting padding on calls to methods such as {@link Map#fitBounds}, {@link Map#fitScreenCoordinates}, and {@link Map#setPadding}. Adjust these options to set the amount of padding in pixels added to the edges of the canvas. Set a uniform padding on all edges or individual values for each edge. All properties of this object must be * non-negative integers. * * @typedef {Object} PaddingOptions * @property {number} top Padding in pixels from the top of the map canvas. * @property {number} bottom Padding in pixels from the bottom of the map canvas. * @property {number} left Padding in pixels from the left of the map canvas. * @property {number} right Padding in pixels from the right of the map canvas. * * @example * const bbox = [[-79, 43], [-73, 45]]; * map.fitBounds(bbox, { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); * * @example * const bbox = [[-79, 43], [-73, 45]]; * map.fitBounds(bbox, { * padding: 20 * }); * @see [Example: Fit to the bounds of a LineString](https://docs.mapbox.com/mapbox-gl-js/example/zoomto-linestring/) * @see [Example: Fit a map to a bounding box](https://docs.mapbox.com/mapbox-gl-js/example/fitbounds/) */ class Camera extends Evented { transform: Transform; _moving: boolean; _zooming: boolean; _rotating: boolean; _pitching: boolean; _padding: boolean; _bearingSnap: number; _easeStart: number; _easeOptions: {duration: number, easing: (_: number) => number}; _easeId: string | void; _onEaseFrame: ?(_: number) => Transform | void; _onEaseEnd: ?(easeId?: string) => void; _easeFrameId: ?TaskID; +_requestRenderFrame: (() => void) => TaskID; +_cancelRenderFrame: (_: TaskID) => void; +_preloadTiles: (transform: Transform | Array<Transform>, callback?: Callback<any>) => any; constructor(transform: Transform, options: {bearingSnap: number}) { super(); this._moving = false; this._zooming = false; this.transform = transform; this._bearingSnap = options.bearingSnap; bindAll(['_renderFrameCallback'], this); //addAssertions(this); } /** @section {Camera} * @method * @instance * @memberof Map */ /** * Returns the map's geographical centerpoint. * * @memberof Map# * @returns {LngLat} The map's geographical centerpoint. * @example * // Return a LngLat object such as {lng: 0, lat: 0}. * const center = map.getCenter(); * // Access longitude and latitude values directly. * const {lng, lat} = map.getCenter(); * @see [Tutorial: Use Mapbox GL JS in a React app](https://docs.mapbox.com/help/tutorials/use-mapbox-gl-js-with-react/#store-the-new-coordinates) */ getCenter(): LngLat { return new LngLat(this.transform.center.lng, this.transform.center.lat); } /** * Sets the map's geographical centerpoint. Equivalent to `jumpTo({center: center})`. * * @memberof Map# * @param {LngLatLike} center The centerpoint to set. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * map.setCenter([-74, 38]); */ setCenter(center: LngLatLike, eventData?: Object): this { return this.jumpTo({center}, eventData); } /** * Pans the map by the specified offset. * * @memberof Map# * @param {PointLike} offset The `x` and `y` coordinates by which to pan the map. * @param {AnimationOptions | null} options An options object describing the destination and animation of the transition. We do not recommend using `options.offset` since this value will override the value of the `offset` parameter. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} `this` Returns itself to allow for method chaining. * @example * map.panBy([-74, 38]); * @example * // panBy with an animation of 5 seconds. * map.panBy([-74, 38], {duration: 5000}); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ panBy(offset: PointLike, options?: AnimationOptions, eventData?: Object): this { offset = Point.convert(offset).mult(-1); return this.panTo(this.transform.center, extend({offset}, options), eventData); } /** * Pans the map to the specified location with an animated transition. * * @memberof Map# * @param {LngLatLike} lnglat The location to pan the map to. * @param {AnimationOptions | null} options Options describing the destination and animation of the transition. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * map.panTo([-74, 38]); * @example * // Specify that the panTo animation should last 5000 milliseconds. * map.panTo([-74, 38], {duration: 5000}); * @see [Example: Update a feature in realtime](https://docs.mapbox.com/mapbox-gl-js/example/live-update-feature/) */ panTo(lnglat: LngLatLike, options?: AnimationOptions, eventData?: Object): this { return this.easeTo(extend({ center: lnglat }, options), eventData); } /** * Returns the map's current zoom level. * * @memberof Map# * @returns {number} The map's current zoom level. * @example * map.getZoom(); */ getZoom(): number { return this.transform.zoom; } /** * Sets the map's zoom level. Equivalent to `jumpTo({zoom: zoom})`. * * @memberof Map# * @param {number} zoom The zoom level to set (0-20). * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:moveend * @fires Map.event:zoomend * @returns {Map} Returns itself to allow for method chaining. * @example * // Zoom to the zoom level 5 without an animated transition * map.setZoom(5); */ setZoom(zoom: number, eventData?: Object): this { this.jumpTo({zoom}, eventData); return this; } /** * Zooms the map to the specified zoom level, with an animated transition. * * @memberof Map# * @param {number} zoom The zoom level to transition to. * @param {AnimationOptions | null} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:moveend * @fires Map.event:zoomend * @returns {Map} Returns itself to allow for method chaining. * @example * // Zoom to the zoom level 5 without an animated transition * map.zoomTo(5); * // Zoom to the zoom level 8 with an animated transition * map.zoomTo(8, { * duration: 2000, * offset: [100, 50] * }); */ zoomTo(zoom: number, options: ? AnimationOptions, eventData?: Object): this { return this.easeTo(extend({ zoom }, options), eventData); } /** * Increases the map's zoom level by 1. * * @memberof Map# * @param {AnimationOptions | null} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:moveend * @fires Map.event:zoomend * @returns {Map} Returns itself to allow for method chaining. * @example * // zoom the map in one level with a custom animation duration * map.zoomIn({duration: 1000}); */ zoomIn(options?: AnimationOptions, eventData?: Object): this { this.zoomTo(this.getZoom() + 1, options, eventData); return this; } /** * Decreases the map's zoom level by 1. * * @memberof Map# * @param {AnimationOptions | null} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:moveend * @fires Map.event:zoomend * @returns {Map} Returns itself to allow for method chaining. * @example * // zoom the map out one level with a custom animation offset * map.zoomOut({offset: [80, 60]}); */ zoomOut(options?: AnimationOptions, eventData?: Object): this { this.zoomTo(this.getZoom() - 1, options, eventData); return this; } /** * Returns the map's current bearing. The bearing is the compass direction that is "up"; for example, a bearing * of 90° orients the map so that east is up. * * @memberof Map# * @returns {number} The map's current bearing. * @example * const bearing = map.getBearing(); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ getBearing(): number { return this.transform.bearing; } /** * Sets the map's bearing (rotation). The bearing is the compass direction that is "up"; for example, a bearing * of 90° orients the map so that east is up. * * Equivalent to `jumpTo({bearing: bearing})`. * * @memberof Map# * @param {number} bearing The desired bearing. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // Rotate the map to 90 degrees. * map.setBearing(90); */ setBearing(bearing: number, eventData?: Object): this { this.jumpTo({bearing}, eventData); return this; } /** * Returns the current padding applied around the map viewport. * * @memberof Map# * @returns {PaddingOptions} The current padding around the map viewport. * @example * const padding = map.getPadding(); */ getPadding(): PaddingOptions { return this.transform.padding; } /** * Sets the padding in pixels around the viewport. * * Equivalent to `jumpTo({padding: padding})`. * * @memberof Map# * @param {PaddingOptions} padding The desired padding. Format: {left: number, right: number, top: number, bottom: number}. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // Sets a left padding of 300px, and a top padding of 50px * map.setPadding({left: 300, top: 50}); */ setPadding(padding: PaddingOptions, eventData?: Object): this { this.jumpTo({padding}, eventData); return this; } /** * Rotates the map to the specified bearing, with an animated transition. The bearing is the compass direction * that is \"up\"; for example, a bearing of 90° orients the map so that east is up. * * @memberof Map# * @param {number} bearing The desired bearing. * @param {AnimationOptions | null} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * map.rotateTo(30); * @example * // rotateTo with an animation of 2 seconds. * map.rotateTo(30, {duration: 2000}); */ rotateTo(bearing: number, options?: AnimationOptions, eventData?: Object): this { return this.easeTo(extend({ bearing }, options), eventData); } /** * Rotates the map so that north is up (0° bearing), with an animated transition. * * @memberof Map# * @param {AnimationOptions | null} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // resetNorth with an animation of 2 seconds. * map.resetNorth({duration: 2000}); */ resetNorth(options?: AnimationOptions, eventData?: Object): this { this.rotateTo(0, extend({duration: 1000}, options), eventData); return this; } /** * Rotates and pitches the map so that north is up (0° bearing) and pitch is 0°, with an animated transition. * * @memberof Map# * @param {AnimationOptions | null} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // resetNorthPitch with an animation of 2 seconds. * map.resetNorthPitch({duration: 2000}); */ resetNorthPitch(options?: AnimationOptions, eventData?: Object): this { this.easeTo(extend({ bearing: 0, pitch: 0, duration: 1000 }, options), eventData); return this; } /** * Snaps the map so that north is up (0° bearing), if the current bearing is * close enough to it (within the `bearingSnap` threshold). * * @memberof Map# * @param {AnimationOptions | null} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // snapToNorth with an animation of 2 seconds. * map.snapToNorth({duration: 2000}); */ snapToNorth(options?: AnimationOptions, eventData?: Object): this { if (Math.abs(this.getBearing()) < this._bearingSnap) { return this.resetNorth(options, eventData); } return this; } /** * Returns the map's current [pitch](https://docs.mapbox.com/help/glossary/camera/) (tilt). * * @memberof Map# * @returns {number} The map's current pitch, measured in degrees away from the plane of the screen. * @example * const pitch = map.getPitch(); */ getPitch(): number { return this.transform.pitch; } /** * Sets the map's [pitch](https://docs.mapbox.com/help/glossary/camera/) (tilt). Equivalent to `jumpTo({pitch: pitch})`. * * @memberof Map# * @param {number} pitch The pitch to set, measured in degrees away from the plane of the screen (0-60). * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:pitchstart * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // setPitch with an animation of 2 seconds. * map.setPitch(80, {duration: 2000}); */ setPitch(pitch: number, eventData?: Object): this { this.jumpTo({pitch}, eventData); return this; } /** * Returns a {@link CameraOptions} object for the highest zoom level * up to and including `Map#getMaxZoom()` that fits the bounds * in the viewport at the specified bearing. * * @memberof Map# * @param {LngLatBoundsLike} 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. LngLatBounds represent a box that is always axis-aligned with bearing 0. * @param {CameraOptions | null} options Options object. * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds. * @param {number} [options.bearing=0] Desired map bearing at end of animation, in degrees. * @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 * `center`, `zoom`, and `bearing`. If map is unable to fit, method will warn and return undefined. * @example * const bbox = [[-79, 43], [-73, 45]]; * const newCameraTransform = map.cameraForBounds(bbox, { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); */ cameraForBounds(bounds: LngLatBoundsLike, options?: CameraOptions): ?EasingOptions { bounds = LngLatBounds.convert(bounds); const bearing = (options && options.bearing) || 0; return this._cameraForBoxAndBearing(bounds.getNorthWest(), bounds.getSouthEast(), bearing, options); } _extendCameraOptions(options?: CameraOptions): FullCameraOptions { const defaultPadding = { top: 0, bottom: 0, right: 0, left: 0 }; options = extend({ padding: defaultPadding, offset: [0, 0], maxZoom: this.transform.maxZoom }, options); if (typeof options.padding === 'number') { const p = options.padding; options.padding = { top: p, bottom: p, right: p, left: p }; } options.padding = extend(defaultPadding, options.padding); return 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 {number} bearing Desired map bearing at end of animation, in degrees * @param {CameraOptions | null} 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 * `center`, `zoom`, and `bearing`. 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): ?EasingOptions { const eOptions = this._extendCameraOptions(options); const tr = this.transform; const edgePadding = tr.padding; // We want to calculate the corners of the box defined by p0 and p1 in a coordinate system // rotated to match the destination bearing. All four corners of the box must be taken // into account because of camera rotation. const p0world = tr.project(LngLat.convert(p0)); const p1world = tr.project(LngLat.convert(p1)); const p2world = new Point(p0world.x, p1world.y); const p3world = new Point(p1world.x, p0world.y); const angleRadians = -degToRad(bearing); const p0rotated = p0world.rotate(angleRadians); const p1rotated = p1world.rotate(angleRadians); const p2rotated = p2world.rotate(angleRadians); const p3rotated = p3world.rotate(angleRadians); const upperRight = new Point( Math.max(p0rotated.x, p1rotated.x, p2rotated.x, p3rotated.x), Math.max(p0rotated.y, p1rotated.y, p2rotated.y, p3rotated.y) ); const lowerLeft = new Point( Math.min(p0rotated.x, p1rotated.x, p2rotated.x, p3rotated.x), Math.min(p0rotated.y, p1rotated.y, p2rotated.y, p3rotated.y) ); // Calculate zoom: consider the original bbox and padding. const size = upperRight.sub(lowerLeft); const scaleX = (tr.width - ((edgePadding.left || 0) + (edgePadding.right || 0) + eOptions.padding.left + eOptions.padding.right)) / size.x; const scaleY = (tr.height - ((edgePadding.top || 0) + (edgePadding.bottom || 0) + eOptions.padding.top + eOptions.padding.bottom)) / size.y; if (scaleY < 0 || scaleX < 0) { warnOnce( 'Map cannot fit within canvas with the given bounds, padding, and/or offset.' ); return; } const zoom = Math.min(tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), eOptions.maxZoom); // Calculate center: apply the zoom, the configured offset, as well as offset that exists as a result of padding. const offset = (typeof eOptions.offset.x === 'number' && typeof eOptions.offset.y === 'number') ? new Point(eOptions.offset.x, eOptions.offset.y) : Point.convert(eOptions.offset); const paddingOffsetX = (eOptions.padding.left - eOptions.padding.right) / 2; const paddingOffsetY = (eOptions.padding.top - eOptions.padding.bottom) / 2; const paddingOffset = new Point(paddingOffsetX, paddingOffsetY); const rotatedPaddingOffset = paddingOffset.rotate(bearing * Math.PI / 180); const offsetAtInitialZoom = offset.add(rotatedPaddingOffset); const offsetAtFinalZoom = offsetAtInitialZoom.mult(tr.scale / tr.zoomScale(zoom)); const center = tr.unproject(p0world.add(p1world).div(2).sub(offsetAtFinalZoom)); return { center, zoom, bearing }; } /** * Finds the best camera fit for two given viewport point coordinates. * The method will iteratively ray march towards the target and stops * when any of the given input points collides with the view frustum. * @memberof Map# * @param {LngLatLike} p0 First point * @param {LngLatLike} p1 Second point * @param {number} minAltitude Optional min altitude in meters * @param {number} maxAltitude Optional max altitude in meters * @param {CameraOptions | null} options * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds. * @returns {CameraOptions | void} If map is able to fit to provided bounds, returns `CameraOptions` with * `center`, `zoom`, `bearing` and `pitch`. If map is unable to fit, method will warn and return undefined. * @private */ _cameraForBox(p0: LngLatLike, p1: LngLatLike, minAltitude?: number, maxAltitude?: number, options?: CameraOptions): ?EasingOptions { const eOptions = this._extendCameraOptions(options); minAltitude = minAltitude || 0; maxAltitude = maxAltitude || 0; p0 = LngLat.convert(p0); p1 = LngLat.convert(p1); const tr = this.transform.clone(); tr.padding = eOptions.padding; const camera = this.getFreeCameraOptions(); const focus = new LngLat((p0.lng + p1.lng) * 0.5, (p0.lat + p1.lat) * 0.5); const focusAltitude = (minAltitude + maxAltitude) * 0.5; if (tr._camera.position[2] < mercatorZfromAltitude(focusAltitude, focus.lat)) { warnOnce('Map cannot fit within canvas with the given bounds, padding, and/or offset.'); return; } camera.lookAtPoint(focus); tr.setFreeCameraOptions(camera); const coord0 = MercatorCoordinate.fromLngLat(p0); const coord1 = MercatorCoordinate.fromLngLat(p1); const toVec3 = (p: MercatorCoordinate): Vec3 => [p.x, p.y, p.z]; const centerIntersectionPoint = tr.pointRayIntersection(tr.centerPoint, focusAltitude); const centerIntersectionCoord = toVec3(tr.rayIntersectionCoordinate(centerIntersectionPoint)); const centerMercatorRay = tr.screenPointToMercatorRay(tr.centerPoint); const zInMeters = tr.projection.name !== 'globe'; const maxMarchingSteps = 10; let steps = 0; let halfDistanceToGround; do { const z = Math.floor(tr.zoom); const z2 = 1 << z; const minX = Math.min(z2 * coord0.x, z2 * coord1.x); const minY = Math.min(z2 * coord0.y, z2 * coord1.y); const maxX = Math.max(z2 * coord0.x, z2 * coord1.x); const maxY = Math.max(z2 * coord0.y, z2 * coord1.y); const aabb = new Aabb([minX, minY, minAltitude], [maxX, maxY, maxAltitude]); const frustum = Frustum.fromInvProjectionMatrix(tr.invProjMatrix, tr.worldSize, z, zInMeters); // Stop marching when frustum intersection // reports any aabb point not fully inside if (aabb.intersects(frustum) !== 2) { // Went too far, step one iteration back if (halfDistanceToGround) { tr._camera.position = vec3.scaleAndAdd([], tr._camera.position, centerMercatorRay.dir, -halfDistanceToGround); tr._updateStateFromCamera(); } break; } const cameraPositionToGround = vec3.sub([], tr._camera.position, centerIntersectionCoord); halfDistanceToGround = 0.5 * vec3.length(cameraPositionToGround); // March the camera position forward by half the distance to the ground tr._camera.position = vec3.scaleAndAdd([], tr._camera.position, centerMercatorRay.dir, halfDistanceToGround); try { tr._updateStateFromCamera(); } catch (e) { warnOnce('Map cannot fit within canvas with the given bounds, padding, and/or offset.'); return; } } while (++steps < maxMarchingSteps); return { center: tr.center, zoom: tr.zoom, bearing: tr.bearing, pitch: tr.pitch }; } /** * Pans and zooms the map to contain its visible area within the specified geographical bounds. * This function will also reset the map's bearing to 0 if bearing is nonzero. * If a padding is set on the map, the bounds are fit to the inset. * * @memberof Map# * @param {LngLatBoundsLike} bounds Center these bounds in the viewport and use the highest * zoom level up to and including `Map#getMaxZoom()` that fits them in the viewport. * @param {Object} [options] Options supports all properties from {@link AnimationOptions} and {@link CameraOptions} in addition to the fields below. * @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 {Object} [eventData] Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * const bbox = [[-79, 43], [-73, 45]]; * map.fitBounds(bbox, { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); * @see [Example: Fit a map to a bounding box](https://www.mapbox.com/mapbox-gl-js/example/fitbounds/) */ fitBounds(bounds: LngLatBoundsLike, options?: EasingOptions, eventData?: Object): this { return this._fitInternal( this.cameraForBounds(bounds, options), options, eventData); } _raycastElevationBox(point0: Point, point1: Point): ?ElevationBoxRaycast { const elevation = this.transform.elevation; if (!elevation) return; const point2 = new Point(point0.x, point1.y); const point3 = new Point(point1.x, point0.y); const r0 = elevation.pointCoordinate(point0); if (!r0) return; const r1 = elevation.pointCoordinate(point1); if (!r1) return; const r2 = elevation.pointCoordinate(point2); if (!r2) return; const r3 = elevation.pointCoordinate(point3); if (!r3) return; const m0 = new MercatorCoordinate(r0[0], r0[1]).toLngLat(); const m1 = new MercatorCoordinate(r1[0], r1[1]).toLngLat(); const m2 = new MercatorCoordinate(r2[0], r2[1]).toLngLat(); const m3 = new MercatorCoordinate(r3[0], r3[1]).toLngLat(); const minLng = Math.min(m0.lng, Math.min(m1.lng, Math.min(m2.lng, m3.lng))); const minLat = Math.min(m0.lat, Math.min(m1.lat, Math.min(m2.lat, m3.lat))); const maxLng = Math.max(m0.lng, Math.max(m1.lng, Math.max(m2.lng, m3.lng))); const maxLat = Math.max(m0.lat, Math.max(m1.lat, Math.max(m2.lat, m3.lat))); const minAltitude = Math.min(r0[3], Math.min(r1[3], Math.min(r2[3], r3[3]))); const maxAltitude = Math.max(r0[3], Math.max(r1[3], Math.max(r2[3], r3[3]))); const minLngLat = new LngLat(minLng, minLat); const maxLngLat = new LngLat(maxLng, maxLat); return {minLngLat, maxLngLat, minAltitude, maxAltitude}; } /** * 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 {PointLike} p0 First point on screen, in pixel coordinates. * @param {PointLike} p1 Second point on screen, in pixel coordinates. * @param {number} bearing Desired map bearing at end of animation, in degrees. This value is ignored if the map has non-zero pitch. * @param {CameraOptions | null} options Options object. * @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 {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * const p0 = [220, 400]; * const p1 = [500, 900]; * map.fitScreenCoordinates(p0, p1, map.getBearing(), { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); * @see Used by {@link BoxZoomHandler} */ fitScreenCoordinates(p0: PointLike, p1: PointLike, bearing: number, options?: EasingOptions, eventData?: Object): this { let lngLat0, lngLat1, minAltitude, maxAltitude; const point0 = Point.convert(p0); const point1 = Point.convert(p1); const raycast = this._raycastElevationBox(point0, point1); if (!raycast) { if (this.transform.anyCornerOffEdge(point0, point1)) { return this; } lngLat0 = this.transform.pointLocation(point0); lngLat1 = this.transform.pointLocation(point1); } else { lngLat0 = raycast.minLngLat; lngLat1 = raycast.maxLngLat; minAltitude = raycast.minAltitude; maxAltitude = raycast.maxAltitude; } if (this.transform.pitch === 0) { return this._fitInternal( this._cameraForBoxAndBearing( this.transform.pointLocation(Point.convert(p0)), this.transform.pointLocation(Point.convert(p1)), bearing, options), options, eventData); } return this._fitInternal( this._cameraForBox( lngLat0, lngLat1, minAltitude, maxAltitude, options), options, eventData); } _fitInternal(calculatedOptions?: ?EasingOptions, options?: EasingOptions, eventData?: Object): this { // cameraForBounds warns + returns undefined if unable to fit: if (!calculatedOptions) return this; options = extend(calculatedOptions, options); // Explicitly remove the padding field because, calculatedOptions already accounts for padding by setting zoom and center accordingly. delete options.padding; 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 * details not specified in `options`. * * @memberof Map# * @param {CameraOptions} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:pitchstart * @fires Map.event:rotate * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:pitch * @fires Map.event:moveend * @fires Map.event:zoomend * @fires Map.event:pitchend * @returns {Map} Returns itself to allow for method chaining. * @example * // jump to coordinates at current zoom * map.jumpTo({center: [0, 0]}); * // jump with zoom, pitch, and bearing options * map.jumpTo({ * center: [0, 0], * zoom: 8, * pitch: 45, * bearing: 90 * }); * @see [Example: Jump to a series of locations](https://docs.mapbox.com/mapbox-gl-js/example/jump-to/) * @see [Example: Update a feature in realtime](https://docs.mapbox.com/mapbox-gl-js/example/live-update-feature/) */ jumpTo(options: CameraOptions & {preloadOnly?: boolean}, eventData?: Object): this { this.stop(); const tr = options.preloadOnly ? this.transform.clone() : this.transform; let zoomChanged = false, bearingChanged = false, pitchChanged = false; if ('zoom' in options && tr.zoom !== +options.zoom) { zoomChanged = true; tr.zoom = +options.zoom; } if (options.center !== undefined) { tr.center = LngLat.convert(options.center); } if ('bearing' in options && tr.bearing !== +options.bearing) { bearingChanged = true; tr.bearing = +options.bearing; } if ('pitch' in options && tr.pitch !== +options.pitch) { pitchChanged = true; tr.pitch = +options.pitch; } if (options.padding != null && !tr.isPaddingEqual(options.padding)) { tr.padding = options.padding; } if (options.preloadOnly) { this._preloadTiles(tr); return this; } this.fire(new Event('movestart', eventData)) .fire(new Event('move', eventData)); if (zoomChanged) { this.fire(new Event('zoomstart', eventData)) .fire(new Event('zoom', eventData)) .fire(new Event('zoomend', eventData)); } if (bearingChanged) { this.fire(new Event('rotatestart', eventData)) .fire(new Event('rotate', eventData)) .fire(new Event('rotateend', eventData)); } if (pitchChanged) { this.fire(new Event('pitchstart', eventData)) .fire(new Event('pitch', eventData)) .fire(new Event('pitchend', eventData)); } return this.fire(new Event('moveend', eventData)); } /** * Returns position and orientation of the camera entity. * * This method is not supported for projections other than mercator. * * @memberof Map# * @returns {FreeCameraOptions} The camera state. * @example * const camera = map.getFreeCameraOptions(); * * const position = [138.72649, 35.33974]; * const altitude = 3000; * * camera.position = mapboxgl.MercatorCoordinate.fromLngLat(position, altitude); * camera.lookAtPoint([138.73036, 35.36197]); * * map.setFreeCameraOptions(camera); */ getFreeCameraOptions(): FreeCameraOptions { if (!this.transform.projection.supportsFreeCamera) { warnOnce(freeCameraNotSupportedWarning); } return this.transform.getFreeCameraOptions(); } /** * `FreeCameraOptions` provides more direct access to the underlying camera entity. * For backwards compatibility the state set using this API must be representable with * `CameraOptions` as well. Parameters are clamped into a valid range or discarded as invalid * if the conversion to the pitch and bearing presentation is ambiguous. For example orientation * can be invalid if it leads to the camera being upside down, the quaternion has zero length, * or the pitch is over the maximum pitch limit. * * This method is not supported for projections other than mercator. * * @memberof Map# * @param {FreeCameraOptions} options `FreeCameraOptions` object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:pitchstart * @fires Map.event:rotate * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:pitch * @fires Map.event:moveend * @fires Map.event:zoomend * @fires Map.event:pitchend * @returns {Map} Returns itself to allow for method chaining. * @example * const camera = map.getFreeCameraOptions(); * * const position = [138.72649, 35.33974]; * const altitude = 3000; * * camera.position = mapboxgl.MercatorCoordinate.fromLngLat(position, altitude); * camera.lookAtPoint([138.73036, 35.36197]); * * map.setFreeCameraOptions(camera); */ setFreeCameraOptions(options: FreeCameraOptions, eventData?: Object): this { const tr = this.transform; if (!tr.projection.supportsFreeCamera) { warnOnce(freeCameraNotSupportedWarning); return this; } this.stop(); const prevZoom = tr.zoom; const prevPitch = tr.pitch; const prevBearing = tr.bearing; tr.setFreeCameraOptions(options); const zoomChanged = prevZoom !== tr.zoom; const pitchChanged = prevPitch !== tr.pitch; const bearingChanged = prevBearing !== tr.bearing; this.fire(new Event('movestart', eventData)) .fire(new Event('move', eventData)); if (zoomChanged) { this.fire(new Event('zoomstart', eventData)) .fire(new Event('zoom', eventData)) .fire(new Event('zoomend', eventData)); } if (bearingChanged) { this.fire(new Event('rotatestart', eventData)) .fire(new Event('rotate', eventData)) .fire(new Event('rotateend', eventData)); } if (pitchChanged) { this.fire(new Event('pitchstart', eventData)) .fire(new Event('pitch', eventData)) .fire(new Event('pitchend', eventData)); } this.fire(new Event('moveend', eventData)); return this; } /** * Changes any combination of `center`, `zoom`, `bearing`, `pitch`, and `padding` with an animated transition * between old and new values. The map will retain its current values for any * details not specified in `options`. * * Note: The transition will happen instantly if the user has enabled * the `reduced motion` accessibility feature enabled in their operating system, * unless `options` includes `essential: true`. * * @memberof Map# * @param {EasingOptions} options Options describing the destination and animation of the transition. * Accepts {@link CameraOptions} and {@link AnimationOptions}. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:pitchstart * @fires Map.event:rotate * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:pitch * @fires Map.event:moveend * @fires Map.event:zoomend * @fires Map.event:pitchend * @returns {Map} `this` Returns itself to allow for method chaining. * @example * // Ease with default options to null island for 5 seconds. * map.easeTo({center: [0, 0], zoom: 9, duration: 5000}); * @example * // Using easeTo options. * map.easeTo({ * center: [0, 0], * zoom: 9, * speed: 0.2, * curve: 1, * duration: 5000, * easing(t) { * return t; * } * }); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ easeTo(options: EasingOptions & {easeId?: string, preloadOnly?: boolean}, eventData?: Object): this { this._stop(false, options.easeId); options = extend({ offset: [0, 0], duration: 500, easing: defaultEasing }, options); if (options.animate === false || (!options.essential && browser.prefersReducedMotion)) options.duration = 0; const tr = this.transform, startZoom = this.getZoom(), startBearing = this.getBearing(), startPitch = this.getPitch(), start