UNPKG

51.5 kBPlain TextView Raw
1import {bindAll, extend, warnOnce, clamp, wrap, ease as defaultEasing, pick} from '../util/util';
2import {number as interpolate} from '../style-spec/util/interpolate';
3import browser from '../util/browser';
4import LngLat from '../geo/lng_lat';
5import LngLatBounds from '../geo/lng_lat_bounds';
6import Point from '@mapbox/point-geometry';
7import {Event, Evented} from '../util/evented';
8import assert from 'assert';
9import {Debug} from '../util/debug';
10
11import type Transform from '../geo/transform';
12import type {LngLatLike} from '../geo/lng_lat';
13import type {LngLatBoundsLike} from '../geo/lng_lat_bounds';
14import type {TaskID} from '../util/task_queue';
15import type {PaddingOptions} from '../geo/edge_insets';
16
17/**
18 * A [Point](https://github.com/mapbox/point-geometry) or an array of two numbers representing `x` and `y` screen coordinates in pixels.
19 *
20 * @typedef {(Point | [number, number])} PointLike
21 * @example
22 * var p1 = new Point(-77, 38); // a PointLike which is a Point
23 * var p2 = [-77, 38]; // a PointLike which is an array of two numbers
24 */
25export type PointLike = Point | [number, number];
26
27export type RequireAtLeastOne<T> = { [K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>; }[keyof T];
28
29/**
30 * Options common to {@link Map#jumpTo}, {@link Map#easeTo}, and {@link Map#flyTo}, controlling the desired location,
31 * zoom, bearing, and pitch of the camera. All properties are optional, and when a property is omitted, the current
32 * camera value for that property will remain unchanged.
33 *
34 * @typedef {Object} CameraOptions
35 * @property {LngLatLike} center The desired center.
36 * @property {number} zoom The desired zoom level.
37 * @property {number} bearing The desired bearing in degrees. The bearing is the compass direction that
38 * is "up". For example, `bearing: 90` orients the map so that east is up.
39 * @property {number} pitch The desired pitch in degrees. The pitch is the angle towards the horizon
40 * measured in degrees with a range between 0 and 60 degrees. For example, pitch: 0 provides the appearance
41 * of looking straight down at the map, while pitch: 60 tilts the user's perspective towards the horizon.
42 * Increasing the pitch value is often used to display 3D objects.
43 * @property {LngLatLike} around If `zoom` is specified, `around` determines the point around which the zoom is centered.
44 * @property {PaddingOptions} padding Dimensions in pixels applied on each side of the viewport for shifting the vanishing point.
45 * @example
46 * // set the map's initial perspective with CameraOptions
47 * var map = new maplibregl.Map({
48 * container: 'map',
49 * style: 'https://demotiles.maplibre.org/style.json',
50 * center: [-73.5804, 45.53483],
51 * pitch: 60,
52 * bearing: -60,
53 * zoom: 10
54 * });
55 * @see [Set pitch and bearing](https://maplibre.org/maplibre-gl-js-docs/example/set-perspective/)
56 * @see [Jump to a series of locations](https://maplibre.org/maplibre-gl-js-docs/example/jump-to/)
57 * @see [Fly to a location](https://maplibre.org/maplibre-gl-js-docs/example/flyto/)
58 * @see [Display buildings in 3D](https://maplibre.org/maplibre-gl-js-docs/example/3d-buildings/)
59 */
60export type CameraOptions = CenterZoomBearing & {
61 pitch?: number;
62 around?: LngLatLike;
63};
64
65export type CenterZoomBearing = {
66 center?: LngLatLike;
67 zoom?: number;
68 bearing?: number;
69}
70
71export type JumpToOptions = CameraOptions & {
72 padding?: PaddingOptions;
73}
74
75export type CameraForBoundsOptions = CameraOptions & {
76 padding?: number | RequireAtLeastOne<PaddingOptions>;
77 offset?: PointLike;
78 maxZoom?: number;
79}
80
81export type FlyToOptions = AnimationOptions & CameraOptions & {
82 curve?: number;
83 minZoom?: number;
84 speed?: number;
85 screenSpeed?: number;
86 maxDuration?: number;
87 padding?: number | RequireAtLeastOne<PaddingOptions>;
88}
89
90export type EaseToOptions = AnimationOptions & CameraOptions & {
91 delayEndEvents?: number;
92 padding?: number | RequireAtLeastOne<PaddingOptions>;
93}
94
95export type FitBoundsOptions = FlyToOptions & {
96 linear?: boolean;
97 offset?: PointLike;
98 maxZoom?: number;
99 maxDuration?: number;
100 padding?: number | RequireAtLeastOne<PaddingOptions>;
101}
102
103/**
104 * Options common to map movement methods that involve animation, such as {@link Map#panBy} and
105 * {@link Map#easeTo}, controlling the duration and easing function of the animation. All properties
106 * are optional.
107 *
108 * @typedef {Object} AnimationOptions
109 * @property {number} duration The animation's duration, measured in milliseconds.
110 * @property {Function} easing A function taking a time in the range 0..1 and returning a number where 0 is
111 * the initial state and 1 is the final state.
112 * @property {PointLike} offset of the target center relative to real map container center at the end of animation.
113 * @property {boolean} animate If `false`, no animation will occur.
114 * @property {boolean} essential If `true`, then the animation is considered essential and will not be affected by
115 * [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion).
116 */
117export type AnimationOptions = {
118 duration?: number;
119 easing?: (_: number) => number;
120 offset?: PointLike;
121 animate?: boolean;
122 essential?: boolean;
123};
124
125abstract class Camera extends Evented {
126 transform: Transform;
127 _moving: boolean;
128 _zooming: boolean;
129 _rotating: boolean;
130 _pitching: boolean;
131 _padding: boolean;
132
133 _bearingSnap: number;
134 _easeStart: number;
135 _easeOptions: {
136 duration?: number;
137 easing?: (_: number) => number;
138 };
139 _easeId: string | void;
140
141 _onEaseFrame: (_: number) => void;
142 _onEaseEnd: (easeId?: string) => void;
143 _easeFrameId: TaskID;
144
145 abstract _requestRenderFrame(a: () => void): TaskID;
146 abstract _cancelRenderFrame(_: TaskID): void;
147
148 constructor(transform: Transform, options: {
149 bearingSnap: number;
150 }) {
151 super();
152 this._moving = false;
153 this._zooming = false;
154 this.transform = transform;
155 this._bearingSnap = options.bearingSnap;
156
157 bindAll(['_renderFrameCallback'], this);
158
159 //addAssertions(this);
160 }
161
162 /**
163 * Returns the map's geographical centerpoint.
164 *
165 * @memberof Map#
166 * @returns The map's geographical centerpoint.
167 * @example
168 * // return a LngLat object such as {lng: 0, lat: 0}
169 * var center = map.getCenter();
170 * // access longitude and latitude values directly
171 * var {lng, lat} = map.getCenter();
172 */
173 getCenter(): LngLat { return new LngLat(this.transform.center.lng, this.transform.center.lat); }
174
175 /**
176 * Sets the map's geographical centerpoint. Equivalent to `jumpTo({center: center})`.
177 *
178 * @memberof Map#
179 * @param center The centerpoint to set.
180 * @param eventData Additional properties to be added to event objects of events triggered by this method.
181 * @fires movestart
182 * @fires moveend
183 * @returns {Map} `this`
184 * @example
185 * map.setCenter([-74, 38]);
186 */
187 setCenter(center: LngLatLike, eventData?: any) {
188 return this.jumpTo({center}, eventData);
189 }
190
191 /**
192 * Pans the map by the specified offset.
193 *
194 * @memberof Map#
195 * @param offset `x` and `y` coordinates by which to pan the map.
196 * @param options Options object
197 * @param eventData Additional properties to be added to event objects of events triggered by this method.
198 * @fires movestart
199 * @fires moveend
200 * @returns {Map} `this`
201 * @see [Navigate the map with game-like controls](https://maplibre.org/maplibre-gl-js-docs/example/game-controls/)
202 */
203 panBy(offset: PointLike, options?: AnimationOptions, eventData?: any) {
204 offset = Point.convert(offset).mult(-1);
205 return this.panTo(this.transform.center, extend({offset}, options), eventData);
206 }
207
208 /**
209 * Pans the map to the specified location with an animated transition.
210 *
211 * @memberof Map#
212 * @param lnglat The location to pan the map to.
213 * @param options Options describing the destination and animation of the transition.
214 * @param eventData Additional properties to be added to event objects of events triggered by this method.
215 * @fires movestart
216 * @fires moveend
217 * @returns {Map} `this`
218 * @example
219 * map.panTo([-74, 38]);
220 * @example
221 * // Specify that the panTo animation should last 5000 milliseconds.
222 * map.panTo([-74, 38], {duration: 5000});
223 * @see [Update a feature in realtime](https://maplibre.org/maplibre-gl-js-docs/example/live-update-feature/)
224 */
225 panTo(lnglat: LngLatLike, options?: AnimationOptions, eventData?: any) {
226 return this.easeTo(extend({
227 center: lnglat
228 }, options), eventData);
229 }
230
231 /**
232 * Returns the map's current zoom level.
233 *
234 * @memberof Map#
235 * @returns The map's current zoom level.
236 * @example
237 * map.getZoom();
238 */
239 getZoom(): number { return this.transform.zoom; }
240
241 /**
242 * Sets the map's zoom level. Equivalent to `jumpTo({zoom: zoom})`.
243 *
244 * @memberof Map#
245 * @param zoom The zoom level to set (0-20).
246 * @param eventData Additional properties to be added to event objects of events triggered by this method.
247 * @fires movestart
248 * @fires zoomstart
249 * @fires move
250 * @fires zoom
251 * @fires moveend
252 * @fires zoomend
253 * @returns {Map} `this`
254 * @example
255 * // Zoom to the zoom level 5 without an animated transition
256 * map.setZoom(5);
257 */
258 setZoom(zoom: number, eventData?: any) {
259 this.jumpTo({zoom}, eventData);
260 return this;
261 }
262
263 /**
264 * Zooms the map to the specified zoom level, with an animated transition.
265 *
266 * @memberof Map#
267 * @param zoom The zoom level to transition to.
268 * @param options Options object
269 * @param eventData Additional properties to be added to event objects of events triggered by this method.
270 * @fires movestart
271 * @fires zoomstart
272 * @fires move
273 * @fires zoom
274 * @fires moveend
275 * @fires zoomend
276 * @returns {Map} `this`
277 * @example
278 * // Zoom to the zoom level 5 without an animated transition
279 * map.zoomTo(5);
280 * // Zoom to the zoom level 8 with an animated transition
281 * map.zoomTo(8, {
282 * duration: 2000,
283 * offset: [100, 50]
284 * });
285 */
286 zoomTo(zoom: number, options?: AnimationOptions | null, eventData?: any) {
287 return this.easeTo(extend({
288 zoom
289 }, options), eventData);
290 }
291
292 /**
293 * Increases the map's zoom level by 1.
294 *
295 * @memberof Map#
296 * @param options Options object
297 * @param eventData Additional properties to be added to event objects of events triggered by this method.
298 * @fires movestart
299 * @fires zoomstart
300 * @fires move
301 * @fires zoom
302 * @fires moveend
303 * @fires zoomend
304 * @returns {Map} `this`
305 * @example
306 * // zoom the map in one level with a custom animation duration
307 * map.zoomIn({duration: 1000});
308 */
309 zoomIn(options?: AnimationOptions, eventData?: any) {
310 this.zoomTo(this.getZoom() + 1, options, eventData);
311 return this;
312 }
313
314 /**
315 * Decreases the map's zoom level by 1.
316 *
317 * @memberof Map#
318 * @param options Options object
319 * @param eventData Additional properties to be added to event objects of events triggered by this method.
320 * @fires movestart
321 * @fires zoomstart
322 * @fires move
323 * @fires zoom
324 * @fires moveend
325 * @fires zoomend
326 * @returns {Map} `this`
327 * @example
328 * // zoom the map out one level with a custom animation offset
329 * map.zoomOut({offset: [80, 60]});
330 */
331 zoomOut(options?: AnimationOptions, eventData?: any) {
332 this.zoomTo(this.getZoom() - 1, options, eventData);
333 return this;
334 }
335
336 /**
337 * Returns the map's current bearing. The bearing is the compass direction that is "up"; for example, a bearing
338 * of 90° orients the map so that east is up.
339 *
340 * @memberof Map#
341 * @returns The map's current bearing.
342 * @see [Navigate the map with game-like controls](https://maplibre.org/maplibre-gl-js-docs/example/game-controls/)
343 */
344 getBearing(): number { return this.transform.bearing; }
345
346 /**
347 * Sets the map's bearing (rotation). The bearing is the compass direction that is "up"; for example, a bearing
348 * of 90° orients the map so that east is up.
349 *
350 * Equivalent to `jumpTo({bearing: bearing})`.
351 *
352 * @memberof Map#
353 * @param bearing The desired bearing.
354 * @param eventData Additional properties to be added to event objects of events triggered by this method.
355 * @fires movestart
356 * @fires moveend
357 * @returns {Map} `this`
358 * @example
359 * // rotate the map to 90 degrees
360 * map.setBearing(90);
361 */
362 setBearing(bearing: number, eventData?: any) {
363 this.jumpTo({bearing}, eventData);
364 return this;
365 }
366
367 /**
368 * Returns the current padding applied around the map viewport.
369 *
370 * @memberof Map#
371 * @returns The current padding around the map viewport.
372 */
373 getPadding(): PaddingOptions { return this.transform.padding; }
374
375 /**
376 * Sets the padding in pixels around the viewport.
377 *
378 * Equivalent to `jumpTo({padding: padding})`.
379 *
380 * @memberof Map#
381 * @param padding The desired padding. Format: { left: number, right: number, top: number, bottom: number }
382 * @param eventData Additional properties to be added to event objects of events triggered by this method.
383 * @fires movestart
384 * @fires moveend
385 * @returns {Map} `this`
386 * @example
387 * // Sets a left padding of 300px, and a top padding of 50px
388 * map.setPadding({ left: 300, top: 50 });
389 */
390 setPadding(padding: PaddingOptions, eventData?: any) {
391 this.jumpTo({padding}, eventData);
392 return this;
393 }
394
395 /**
396 * Rotates the map to the specified bearing, with an animated transition. The bearing is the compass direction
397 * that is \"up\"; for example, a bearing of 90° orients the map so that east is up.
398 *
399 * @memberof Map#
400 * @param bearing The desired bearing.
401 * @param options Options object
402 * @param eventData Additional properties to be added to event objects of events triggered by this method.
403 * @fires movestart
404 * @fires moveend
405 * @returns {Map} `this`
406 */
407 rotateTo(bearing: number, options?: AnimationOptions, eventData?: any) {
408 return this.easeTo(extend({
409 bearing
410 }, options), eventData);
411 }
412
413 /**
414 * Rotates the map so that north is up (0° bearing), with an animated transition.
415 *
416 * @memberof Map#
417 * @param options Options object
418 * @param eventData Additional properties to be added to event objects of events triggered by this method.
419 * @fires movestart
420 * @fires moveend
421 * @returns {Map} `this`
422 */
423 resetNorth(options?: AnimationOptions, eventData?: any) {
424 this.rotateTo(0, extend({duration: 1000}, options), eventData);
425 return this;
426 }
427
428 /**
429 * Rotates and pitches the map so that north is up (0° bearing) and pitch is 0°, with an animated transition.
430 *
431 * @memberof Map#
432 * @param options Options object
433 * @param eventData Additional properties to be added to event objects of events triggered by this method.
434 * @fires movestart
435 * @fires moveend
436 * @returns {Map} `this`
437 */
438 resetNorthPitch(options?: AnimationOptions, eventData?: any) {
439 this.easeTo(extend({
440 bearing: 0,
441 pitch: 0,
442 duration: 1000
443 }, options), eventData);
444 return this;
445 }
446
447 /**
448 * Snaps the map so that north is up (0° bearing), if the current bearing is close enough to it (i.e. within the
449 * `bearingSnap` threshold).
450 *
451 * @memberof Map#
452 * @param options Options object
453 * @param eventData Additional properties to be added to event objects of events triggered by this method.
454 * @fires movestart
455 * @fires moveend
456 * @returns {Map} `this`
457 */
458 snapToNorth(options?: AnimationOptions, eventData?: any) {
459 if (Math.abs(this.getBearing()) < this._bearingSnap) {
460 return this.resetNorth(options, eventData);
461 }
462 return this;
463 }
464
465 /**
466 * Returns the map's current pitch (tilt).
467 *
468 * @memberof Map#
469 * @returns The map's current pitch, measured in degrees away from the plane of the screen.
470 */
471 getPitch(): number { return this.transform.pitch; }
472
473 /**
474 * Sets the map's pitch (tilt). Equivalent to `jumpTo({pitch: pitch})`.
475 *
476 * @memberof Map#
477 * @param pitch The pitch to set, measured in degrees away from the plane of the screen (0-60).
478 * @param eventData Additional properties to be added to event objects of events triggered by this method.
479 * @fires pitchstart
480 * @fires movestart
481 * @fires moveend
482 * @returns {Map} `this`
483 */
484 setPitch(pitch: number, eventData?: any) {
485 this.jumpTo({pitch}, eventData);
486 return this;
487 }
488
489 /**
490 * @memberof Map#
491 * @param {LngLatBoundsLike} bounds Calculate the center for these bounds in the viewport and use
492 * the highest zoom level up to and including `Map#getMaxZoom()` that fits
493 * in the viewport. LngLatBounds represent a box that is always axis-aligned with bearing 0.
494 * @param options Options object
495 * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
496 * @param {number} [options.bearing=0] Desired map bearing at end of animation, in degrees.
497 * @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
498 * @param {number} [options.maxZoom] The maximum zoom level to allow when the camera would transition to the specified bounds.
499 * @returns {CenterZoomBearing} If map is able to fit to provided bounds, returns `center`, `zoom`, and `bearing`.
500 * If map is unable to fit, method will warn and return undefined.
501 * @example
502 * var bbox = [[-79, 43], [-73, 45]];
503 * var newCameraTransform = map.cameraForBounds(bbox, {
504 * padding: {top: 10, bottom:25, left: 15, right: 5}
505 * });
506 */
507 cameraForBounds(bounds: LngLatBoundsLike, options?: CameraForBoundsOptions): CenterZoomBearing {
508 bounds = LngLatBounds.convert(bounds);
509 const bearing = options && options.bearing || 0;
510 return this._cameraForBoxAndBearing(bounds.getNorthWest(), bounds.getSouthEast(), bearing, options);
511 }
512
513 /**
514 * Calculate the center of these two points in the viewport and use
515 * the highest zoom level up to and including `Map#getMaxZoom()` that fits
516 * the points in the viewport at the specified bearing.
517 * @memberof Map#
518 * @param {LngLatLike} p0 First point
519 * @param {LngLatLike} p1 Second point
520 * @param bearing Desired map bearing at end of animation, in degrees
521 * @param options
522 * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
523 * @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
524 * @param {number} [options.maxZoom] The maximum zoom level to allow when the camera would transition to the specified bounds.
525 * @returns {CenterZoomBearing} If map is able to fit to provided bounds, returns `center`, `zoom`, and `bearing`.
526 * If map is unable to fit, method will warn and return undefined.
527 * @private
528 * @example
529 * var p0 = [-79, 43];
530 * var p1 = [-73, 45];
531 * var bearing = 90;
532 * var newCameraTransform = map._cameraForBoxAndBearing(p0, p1, bearing, {
533 * padding: {top: 10, bottom:25, left: 15, right: 5}
534 * });
535 */
536 _cameraForBoxAndBearing(p0: LngLatLike, p1: LngLatLike, bearing: number, options?: CameraForBoundsOptions): CenterZoomBearing {
537 const defaultPadding = {
538 top: 0,
539 bottom: 0,
540 right: 0,
541 left: 0
542 };
543 options = extend({
544 padding: defaultPadding,
545 offset: [0, 0],
546 maxZoom: this.transform.maxZoom
547 }, options);
548
549 if (typeof options.padding === 'number') {
550 const p = options.padding;
551 options.padding = {
552 top: p,
553 bottom: p,
554 right: p,
555 left: p
556 };
557 }
558
559 options.padding = extend(defaultPadding, options.padding) as PaddingOptions;
560 const tr = this.transform;
561 const edgePadding = tr.padding;
562
563 // We want to calculate the upper right and lower left of the box defined by p0 and p1
564 // in a coordinate system rotate to match the destination bearing.
565 const p0world = tr.project(LngLat.convert(p0));
566 const p1world = tr.project(LngLat.convert(p1));
567 const p0rotated = p0world.rotate(-bearing * Math.PI / 180);
568 const p1rotated = p1world.rotate(-bearing * Math.PI / 180);
569
570 const upperRight = new Point(Math.max(p0rotated.x, p1rotated.x), Math.max(p0rotated.y, p1rotated.y));
571 const lowerLeft = new Point(Math.min(p0rotated.x, p1rotated.x), Math.min(p0rotated.y, p1rotated.y));
572
573 // Calculate zoom: consider the original bbox and padding.
574 const size = upperRight.sub(lowerLeft);
575 const scaleX = (tr.width - (edgePadding.left + edgePadding.right + options.padding.left + options.padding.right)) / size.x;
576 const scaleY = (tr.height - (edgePadding.top + edgePadding.bottom + options.padding.top + options.padding.bottom)) / size.y;
577
578 if (scaleY < 0 || scaleX < 0) {
579 warnOnce(
580 'Map cannot fit within canvas with the given bounds, padding, and/or offset.'
581 );
582 return undefined;
583 }
584
585 const zoom = Math.min(tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), options.maxZoom);
586
587 // Calculate center: apply the zoom, the configured offset, as well as offset that exists as a result of padding.
588 const offset = Point.convert(options.offset);
589 const paddingOffsetX = (options.padding.left - options.padding.right) / 2;
590 const paddingOffsetY = (options.padding.top - options.padding.bottom) / 2;
591 const paddingOffset = new Point(paddingOffsetX, paddingOffsetY);
592 const rotatedPaddingOffset = paddingOffset.rotate(bearing * Math.PI / 180);
593 const offsetAtInitialZoom = offset.add(rotatedPaddingOffset);
594 const offsetAtFinalZoom = offsetAtInitialZoom.mult(tr.scale / tr.zoomScale(zoom));
595
596 const center = tr.unproject(p0world.add(p1world).div(2).sub(offsetAtFinalZoom));
597
598 return {
599 center,
600 zoom,
601 bearing
602 };
603 }
604
605 /**
606 * Pans and zooms the map to contain its visible area within the specified geographical bounds.
607 * This function will also reset the map's bearing to 0 if bearing is nonzero.
608 *
609 * @memberof Map#
610 * @param bounds Center these bounds in the viewport and use the highest
611 * zoom level up to and including `Map#getMaxZoom()` that fits them in the viewport.
612 * @param {FitBoundsOptions} [options] Options supports all properties from {@link AnimationOptions} and {@link CameraOptions} in addition to the fields below.
613 * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
614 * @param {boolean} [options.linear=false] If `true`, the map transitions using
615 * {@link Map#easeTo}. If `false`, the map transitions using {@link Map#flyTo}. See
616 * those functions and {@link AnimationOptions} for information about options available.
617 * @param {Function} [options.easing] An easing function for the animated transition. See {@link AnimationOptions}.
618 * @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
619 * @param {number} [options.maxZoom] The maximum zoom level to allow when the map view transitions to the specified bounds.
620 * @param {Object} [eventData] Additional properties to be added to event objects of events triggered by this method.
621 * @fires movestart
622 * @fires moveend
623 * @returns {Map} `this`
624 * @example
625 * var bbox = [[-79, 43], [-73, 45]];
626 * map.fitBounds(bbox, {
627 * padding: {top: 10, bottom:25, left: 15, right: 5}
628 * });
629 * @see [Fit a map to a bounding box](https://maplibre.org/maplibre-gl-js-docs/example/fitbounds/)
630 */
631 fitBounds(bounds: LngLatBoundsLike, options?: FitBoundsOptions, eventData?: any) {
632 return this._fitInternal(
633 this.cameraForBounds(bounds, options),
634 options,
635 eventData);
636 }
637
638 /**
639 * Pans, rotates and zooms the map to to fit the box made by points p0 and p1
640 * once the map is rotated to the specified bearing. To zoom without rotating,
641 * pass in the current map bearing.
642 *
643 * @memberof Map#
644 * @param p0 First point on screen, in pixel coordinates
645 * @param p1 Second point on screen, in pixel coordinates
646 * @param bearing Desired map bearing at end of animation, in degrees
647 * @param options Options object
648 * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
649 * @param {boolean} [options.linear=false] If `true`, the map transitions using
650 * {@link Map#easeTo}. If `false`, the map transitions using {@link Map#flyTo}. See
651 * those functions and {@link AnimationOptions} for information about options available.
652 * @param {Function} [options.easing] An easing function for the animated transition. See {@link AnimationOptions}.
653 * @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
654 * @param {number} [options.maxZoom] The maximum zoom level to allow when the map view transitions to the specified bounds.
655 * @param eventData Additional properties to be added to event objects of events triggered by this method.
656 * @fires movestart
657 * @fires moveend
658 * @returns {Map} `this`
659 * @example
660 * var p0 = [220, 400];
661 * var p1 = [500, 900];
662 * map.fitScreenCoordinates(p0, p1, map.getBearing(), {
663 * padding: {top: 10, bottom:25, left: 15, right: 5}
664 * });
665 * @see Used by {@link BoxZoomHandler}
666 */
667 fitScreenCoordinates(p0: PointLike, p1: PointLike, bearing: number, options?: FitBoundsOptions, eventData?: any) {
668 return this._fitInternal(
669 this._cameraForBoxAndBearing(
670 this.transform.pointLocation(Point.convert(p0)),
671 this.transform.pointLocation(Point.convert(p1)),
672 bearing,
673 options),
674 options,
675 eventData);
676 }
677
678 _fitInternal(calculatedOptions?: CenterZoomBearing, options?: FitBoundsOptions, eventData?: any) {
679 // cameraForBounds warns + returns undefined if unable to fit:
680 if (!calculatedOptions) return this;
681
682 options = extend(calculatedOptions, options);
683 // Explictly remove the padding field because, calculatedOptions already accounts for padding by setting zoom and center accordingly.
684 delete options.padding;
685
686 return options.linear ?
687 this.easeTo(options, eventData) :
688 this.flyTo(options, eventData);
689 }
690
691 /**
692 * Changes any combination of center, zoom, bearing, and pitch, without
693 * an animated transition. The map will retain its current values for any
694 * details not specified in `options`.
695 *
696 * @memberof Map#
697 * @param options Options object
698 * @param eventData Additional properties to be added to event objects of events triggered by this method.
699 * @fires movestart
700 * @fires zoomstart
701 * @fires pitchstart
702 * @fires rotate
703 * @fires move
704 * @fires zoom
705 * @fires pitch
706 * @fires moveend
707 * @fires zoomend
708 * @fires pitchend
709 * @returns {Map} `this`
710 * @example
711 * // jump to coordinates at current zoom
712 * map.jumpTo({center: [0, 0]});
713 * // jump with zoom, pitch, and bearing options
714 * map.jumpTo({
715 * center: [0, 0],
716 * zoom: 8,
717 * pitch: 45,
718 * bearing: 90
719 * });
720 * @see [Jump to a series of locations](https://maplibre.org/maplibre-gl-js-docs/example/jump-to/)
721 * @see [Update a feature in realtime](https://maplibre.org/maplibre-gl-js-docs/example/live-update-feature/)
722 */
723 jumpTo(options: JumpToOptions, eventData?: any) {
724 this.stop();
725
726 const tr = this.transform;
727 let zoomChanged = false,
728 bearingChanged = false,
729 pitchChanged = false;
730
731 if ('zoom' in options && tr.zoom !== +options.zoom) {
732 zoomChanged = true;
733 tr.zoom = +options.zoom;
734 }
735
736 if (options.center !== undefined) {
737 tr.center = LngLat.convert(options.center);
738 }
739
740 if ('bearing' in options && tr.bearing !== +options.bearing) {
741 bearingChanged = true;
742 tr.bearing = +options.bearing;
743 }
744
745 if ('pitch' in options && tr.pitch !== +options.pitch) {
746 pitchChanged = true;
747 tr.pitch = +options.pitch;
748 }
749
750 if (options.padding != null && !tr.isPaddingEqual(options.padding)) {
751 tr.padding = options.padding;
752 }
753
754 this.fire(new Event('movestart', eventData))
755 .fire(new Event('move', eventData));
756
757 if (zoomChanged) {
758 this.fire(new Event('zoomstart', eventData))
759 .fire(new Event('zoom', eventData))
760 .fire(new Event('zoomend', eventData));
761 }
762
763 if (bearingChanged) {
764 this.fire(new Event('rotatestart', eventData))
765 .fire(new Event('rotate', eventData))
766 .fire(new Event('rotateend', eventData));
767 }
768
769 if (pitchChanged) {
770 this.fire(new Event('pitchstart', eventData))
771 .fire(new Event('pitch', eventData))
772 .fire(new Event('pitchend', eventData));
773 }
774
775 return this.fire(new Event('moveend', eventData));
776 }
777
778 /**
779 * Changes any combination of `center`, `zoom`, `bearing`, `pitch`, and `padding` with an animated transition
780 * between old and new values. The map will retain its current values for any
781 * details not specified in `options`.
782 *
783 * Note: The transition will happen instantly if the user has enabled
784 * the `reduced motion` accesibility feature enabled in their operating system,
785 * unless `options` includes `essential: true`.
786 *
787 * @memberof Map#
788 * @param options Options describing the destination and animation of the transition.
789 * Accepts {@link CameraOptions} and {@link AnimationOptions}.
790 * @param eventData Additional properties to be added to event objects of events triggered by this method.
791 * @fires movestart
792 * @fires zoomstart
793 * @fires pitchstart
794 * @fires rotate
795 * @fires move
796 * @fires zoom
797 * @fires pitch
798 * @fires moveend
799 * @fires zoomend
800 * @fires pitchend
801 * @returns {Map} `this`
802 * @see [Navigate the map with game-like controls](https://maplibre.org/maplibre-gl-js-docs/example/game-controls/)
803 */
804 easeTo(options: EaseToOptions & {
805 easeId?: string;
806 noMoveStart?: boolean;
807 }, eventData?: any) {
808 this._stop(false, options.easeId);
809
810 options = extend({
811 offset: [0, 0],
812 duration: 500,
813 easing: defaultEasing
814 }, options);
815
816 if (options.animate === false || (!options.essential && browser.prefersReducedMotion)) options.duration = 0;
817
818 const tr = this.transform,
819 startZoom = this.getZoom(),
820 startBearing = this.getBearing(),
821 startPitch = this.getPitch(),
822 startPadding = this.getPadding(),
823
824 zoom = 'zoom' in options ? +options.zoom : startZoom,
825 bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing,
826 pitch = 'pitch' in options ? +options.pitch : startPitch,
827 padding = 'padding' in options ? options.padding : tr.padding;
828
829 const offsetAsPoint = Point.convert(options.offset);
830 let pointAtOffset = tr.centerPoint.add(offsetAsPoint);
831 const locationAtOffset = tr.pointLocation(pointAtOffset);
832 const center = LngLat.convert(options.center || locationAtOffset);
833 this._normalizeCenter(center);
834
835 const from = tr.project(locationAtOffset);
836 const delta = tr.project(center).sub(from);
837 const finalScale = tr.zoomScale(zoom - startZoom);
838
839 let around, aroundPoint;
840
841 if (options.around) {
842 around = LngLat.convert(options.around);
843 aroundPoint = tr.locationPoint(around);
844 }
845
846 const currently = {
847 moving: this._moving,
848 zooming: this._zooming,
849 rotating: this._rotating,
850 pitching: this._pitching
851 };
852
853 this._zooming = this._zooming || (zoom !== startZoom);
854 this._rotating = this._rotating || (startBearing !== bearing);
855 this._pitching = this._pitching || (pitch !== startPitch);
856 this._padding = !tr.isPaddingEqual(padding as PaddingOptions);
857
858 this._easeId = options.easeId;
859 this._prepareEase(eventData, options.noMoveStart, currently);
860
861 this._ease((k) => {
862 if (this._zooming) {
863 tr.zoom = interpolate(startZoom, zoom, k);
864 }
865 if (this._rotating) {
866 tr.bearing = interpolate(startBearing, bearing, k);
867 }
868 if (this._pitching) {
869 tr.pitch = interpolate(startPitch, pitch, k);
870 }
871 if (this._padding) {
872 tr.interpolatePadding(startPadding, padding as PaddingOptions, k);
873 // When padding is being applied, Transform#centerPoint is changing continously,
874 // thus we need to recalculate offsetPoint every frame
875 pointAtOffset = tr.centerPoint.add(offsetAsPoint);
876 }
877
878 if (around) {
879 tr.setLocationAtPoint(around, aroundPoint);
880 } else {
881 const scale = tr.zoomScale(tr.zoom - startZoom);
882 const base = zoom > startZoom ?
883 Math.min(2, finalScale) :
884 Math.max(0.5, finalScale);
885 const speedup = Math.pow(base, 1 - k);
886 const newCenter = tr.unproject(from.add(delta.mult(k * speedup)).mult(scale));
887 tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset);
888 }
889
890 this._fireMoveEvents(eventData);
891
892 }, (interruptingEaseId?: string) => {
893 this._afterEase(eventData, interruptingEaseId);
894 }, options as any);
895
896 return this;
897 }
898
899 _prepareEase(eventData: any, noMoveStart: boolean, currently: any = {}) {
900 this._moving = true;
901
902 if (!noMoveStart && !currently.moving) {
903 this.fire(new Event('movestart', eventData));
904 }
905 if (this._zooming && !currently.zooming) {
906 this.fire(new Event('zoomstart', eventData));
907 }
908 if (this._rotating && !currently.rotating) {
909 this.fire(new Event('rotatestart', eventData));
910 }
911 if (this._pitching && !currently.pitching) {
912 this.fire(new Event('pitchstart', eventData));
913 }
914 }
915
916 _fireMoveEvents(eventData?: any) {
917 this.fire(new Event('move', eventData));
918 if (this._zooming) {
919 this.fire(new Event('zoom', eventData));
920 }
921 if (this._rotating) {
922 this.fire(new Event('rotate', eventData));
923 }
924 if (this._pitching) {
925 this.fire(new Event('pitch', eventData));
926 }
927 }
928
929 _afterEase(eventData?: any, easeId?: string) {
930 // if this easing is being stopped to start another easing with
931 // the same id then don't fire any events to avoid extra start/stop events
932 if (this._easeId && easeId && this._easeId === easeId) {
933 return;
934 }
935 delete this._easeId;
936
937 const wasZooming = this._zooming;
938 const wasRotating = this._rotating;
939 const wasPitching = this._pitching;
940 this._moving = false;
941 this._zooming = false;
942 this._rotating = false;
943 this._pitching = false;
944 this._padding = false;
945
946 if (wasZooming) {
947 this.fire(new Event('zoomend', eventData));
948 }
949 if (wasRotating) {
950 this.fire(new Event('rotateend', eventData));
951 }
952 if (wasPitching) {
953 this.fire(new Event('pitchend', eventData));
954 }
955 this.fire(new Event('moveend', eventData));
956 }
957
958 /**
959 * Changes any combination of center, zoom, bearing, and pitch, animating the transition along a curve that
960 * evokes flight. The animation seamlessly incorporates zooming and panning to help
961 * the user maintain her bearings even after traversing a great distance.
962 *
963 * Note: The animation will be skipped, and this will behave equivalently to `jumpTo`
964 * if the user has the `reduced motion` accesibility feature enabled in their operating system,
965 * unless 'options' includes `essential: true`.
966 *
967 * @memberof Map#
968 * @param {FlyToOptions} options Options describing the destination and animation of the transition.
969 * Accepts {@link CameraOptions}, {@link AnimationOptions},
970 * and the following additional options.
971 * @param {number} [options.curve=1.42] The zooming "curve" that will occur along the
972 * flight path. A high value maximizes zooming for an exaggerated animation, while a low
973 * value minimizes zooming for an effect closer to {@link Map#easeTo}. 1.42 is the average
974 * value selected by participants in the user study discussed in
975 * [van Wijk (2003)](https://www.win.tue.nl/~vanwijk/zoompan.pdf). A value of
976 * `Math.pow(6, 0.25)` would be equivalent to the root mean squared average velocity. A
977 * value of 1 would produce a circular motion.
978 * @param {number} [options.minZoom] The zero-based zoom level at the peak of the flight path. If
979 * `options.curve` is specified, this option is ignored.
980 * @param {number} [options.speed=1.2] The average speed of the animation defined in relation to
981 * `options.curve`. A speed of 1.2 means that the map appears to move along the flight path
982 * by 1.2 times `options.curve` screenfuls every second. A _screenful_ is the map's visible span.
983 * It does not correspond to a fixed physical distance, but varies by zoom level.
984 * @param {number} [options.screenSpeed] The average speed of the animation measured in screenfuls
985 * per second, assuming a linear timing curve. If `options.speed` is specified, this option is ignored.
986 * @param {number} [options.maxDuration] The animation's maximum duration, measured in milliseconds.
987 * If duration exceeds maximum duration, it resets to 0.
988 * @param eventData Additional properties to be added to event objects of events triggered by this method.
989 * @fires movestart
990 * @fires zoomstart
991 * @fires pitchstart
992 * @fires move
993 * @fires zoom
994 * @fires rotate
995 * @fires pitch
996 * @fires moveend
997 * @fires zoomend
998 * @fires pitchend
999 * @returns {Map} `this`
1000 * @example
1001 * // fly with default options to null island
1002 * map.flyTo({center: [0, 0], zoom: 9});
1003 * // using flyTo options
1004 * map.flyTo({
1005 * center: [0, 0],
1006 * zoom: 9,
1007 * speed: 0.2,
1008 * curve: 1,
1009 * easing(t) {
1010 * return t;
1011 * }
1012 * });
1013 * @see [Fly to a location](https://maplibre.org/maplibre-gl-js-docs/example/flyto/)
1014 * @see [Slowly fly to a location](https://maplibre.org/maplibre-gl-js-docs/example/flyto-options/)
1015 * @see [Fly to a location based on scroll position](https://maplibre.org/maplibre-gl-js-docs/example/scroll-fly-to/)
1016 */
1017 flyTo(options: FlyToOptions, eventData?: any) {
1018 // Fall through to jumpTo if user has set prefers-reduced-motion
1019 if (!options.essential && browser.prefersReducedMotion) {
1020 const coercedOptions = pick(options, ['center', 'zoom', 'bearing', 'pitch', 'around']) as CameraOptions;
1021 return this.jumpTo(coercedOptions, eventData);
1022 }
1023
1024 // This method implements an “optimal path” animation, as detailed in:
1025 //
1026 // Van Wijk, Jarke J.; Nuij, Wim A. A. “Smooth and efficient zooming and panning.” INFOVIS
1027 // ’03. pp. 15–22. <https://www.win.tue.nl/~vanwijk/zoompan.pdf#page=5>.
1028 //
1029 // Where applicable, local variable documentation begins with the associated variable or
1030 // function in van Wijk (2003).
1031
1032 this.stop();
1033
1034 options = extend({
1035 offset: [0, 0],
1036 speed: 1.2,
1037 curve: 1.42,
1038 easing: defaultEasing
1039 }, options);
1040
1041 const tr = this.transform,
1042 startZoom = this.getZoom(),
1043 startBearing = this.getBearing(),
1044 startPitch = this.getPitch(),
1045 startPadding = this.getPadding();
1046
1047 const zoom = 'zoom' in options ? clamp(+options.zoom, tr.minZoom, tr.maxZoom) : startZoom;
1048 const bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing;
1049 const pitch = 'pitch' in options ? +options.pitch : startPitch;
1050 const padding = 'padding' in options ? options.padding : tr.padding;
1051
1052 const scale = tr.zoomScale(zoom - startZoom);
1053 const offsetAsPoint = Point.convert(options.offset);
1054 let pointAtOffset = tr.centerPoint.add(offsetAsPoint);
1055 const locationAtOffset = tr.pointLocation(pointAtOffset);
1056 const center = LngLat.convert(options.center || locationAtOffset);
1057 this._normalizeCenter(center);
1058
1059 const from = tr.project(locationAtOffset);
1060 const delta = tr.project(center).sub(from);
1061
1062 let rho = options.curve;
1063
1064 // w₀: Initial visible span, measured in pixels at the initial scale.
1065 const w0 = Math.max(tr.width, tr.height),
1066 // w₁: Final visible span, measured in pixels with respect to the initial scale.
1067 w1 = w0 / scale,
1068 // Length of the flight path as projected onto the ground plane, measured in pixels from
1069 // the world image origin at the initial scale.
1070 u1 = delta.mag();
1071
1072 if ('minZoom' in options) {
1073 const minZoom = clamp(Math.min(options.minZoom, startZoom, zoom), tr.minZoom, tr.maxZoom);
1074 // w<sub>m</sub>: Maximum visible span, measured in pixels with respect to the initial
1075 // scale.
1076 const wMax = w0 / tr.zoomScale(minZoom - startZoom);
1077 rho = Math.sqrt(wMax / u1 * 2);
1078 }
1079
1080 // ρ²
1081 const rho2 = rho * rho;
1082
1083 /**
1084 * rᵢ: Returns the zoom-out factor at one end of the animation.
1085 *
1086 * @param i 0 for the ascent or 1 for the descent.
1087 * @private
1088 */
1089 function r(i) {
1090 const b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1);
1091 return Math.log(Math.sqrt(b * b + 1) - b);
1092 }
1093
1094 function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
1095 function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
1096 function tanh(n) { return sinh(n) / cosh(n); }
1097
1098 // r₀: Zoom-out factor during ascent.
1099 const r0 = r(0);
1100
1101 // w(s): Returns the visible span on the ground, measured in pixels with respect to the
1102 // initial scale. Assumes an angular field of view of 2 arctan ½ ≈ 53°.
1103 let w: (_: number) => number = function (s) {
1104 return (cosh(r0) / cosh(r0 + rho * s));
1105 };
1106
1107 // u(s): Returns the distance along the flight path as projected onto the ground plane,
1108 // measured in pixels from the world image origin at the initial scale.
1109 let u: (_: number) => number = function (s) {
1110 return w0 * ((cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2) / u1;
1111 };
1112
1113 // S: Total length of the flight path, measured in ρ-screenfuls.
1114 let S = (r(1) - r0) / rho;
1115
1116 // When u₀ = u₁, the optimal path doesn’t require both ascent and descent.
1117 if (Math.abs(u1) < 0.000001 || !isFinite(S)) {
1118 // Perform a more or less instantaneous transition if the path is too short.
1119 if (Math.abs(w0 - w1) < 0.000001) return this.easeTo(options, eventData);
1120
1121 const k = w1 < w0 ? -1 : 1;
1122 S = Math.abs(Math.log(w1 / w0)) / rho;
1123
1124 u = function() { return 0; };
1125 w = function(s) { return Math.exp(k * rho * s); };
1126 }
1127
1128 if ('duration' in options) {
1129 options.duration = +options.duration;
1130 } else {
1131 const V = 'screenSpeed' in options ? +options.screenSpeed / rho : +options.speed;
1132 options.duration = 1000 * S / V;
1133 }
1134
1135 if (options.maxDuration && options.duration > options.maxDuration) {
1136 options.duration = 0;
1137 }
1138
1139 this._zooming = true;
1140 this._rotating = (startBearing !== bearing);
1141 this._pitching = (pitch !== startPitch);
1142 this._padding = !tr.isPaddingEqual(padding as PaddingOptions);
1143
1144 this._prepareEase(eventData, false);
1145
1146 this._ease((k) => {
1147 // s: The distance traveled along the flight path, measured in ρ-screenfuls.
1148 const s = k * S;
1149 const scale = 1 / w(s);
1150 tr.zoom = k === 1 ? zoom : startZoom + tr.scaleZoom(scale);
1151
1152 if (this._rotating) {
1153 tr.bearing = interpolate(startBearing, bearing, k);
1154 }
1155 if (this._pitching) {
1156 tr.pitch = interpolate(startPitch, pitch, k);
1157 }
1158 if (this._padding) {
1159 tr.interpolatePadding(startPadding, padding as PaddingOptions, k);
1160 // When padding is being applied, Transform#centerPoint is changing continously,
1161 // thus we need to recalculate offsetPoint every frame
1162 pointAtOffset = tr.centerPoint.add(offsetAsPoint);
1163 }
1164
1165 const newCenter = k === 1 ? center : tr.unproject(from.add(delta.mult(u(s))).mult(scale));
1166 tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset);
1167
1168 this._fireMoveEvents(eventData);
1169
1170 }, () => this._afterEase(eventData), options);
1171
1172 return this;
1173 }
1174
1175 isEasing() {
1176 return !!this._easeFrameId;
1177 }
1178
1179 /**
1180 * Stops any animated transition underway.
1181 *
1182 * @memberof Map#
1183 * @returns {Map} `this`
1184 */
1185 stop(): this {
1186 return this._stop();
1187 }
1188
1189 _stop(allowGestures?: boolean, easeId?: string): this {
1190 if (this._easeFrameId) {
1191 this._cancelRenderFrame(this._easeFrameId);
1192 delete this._easeFrameId;
1193 delete this._onEaseFrame;
1194 }
1195
1196 if (this._onEaseEnd) {
1197 // The _onEaseEnd function might emit events which trigger new
1198 // animation, which sets a new _onEaseEnd. Ensure we don't delete
1199 // it unintentionally.
1200 const onEaseEnd = this._onEaseEnd;
1201 delete this._onEaseEnd;
1202 onEaseEnd.call(this, easeId);
1203 }
1204 if (!allowGestures) {
1205 const handlers = (this as any).handlers;
1206 if (handlers) handlers.stop(false);
1207 }
1208 return this;
1209 }
1210
1211 _ease(frame: (_: number) => void,
1212 finish: () => void,
1213 options: {
1214 animate?: boolean;
1215 duration?: number;
1216 easing?: (_: number) => number;
1217 }) {
1218 if (options.animate === false || options.duration === 0) {
1219 frame(1);
1220 finish();
1221 } else {
1222 this._easeStart = browser.now();
1223 this._easeOptions = options;
1224 this._onEaseFrame = frame;
1225 this._onEaseEnd = finish;
1226 this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback);
1227 }
1228 }
1229
1230 // Callback for map._requestRenderFrame
1231 _renderFrameCallback() {
1232 const t = Math.min((browser.now() - this._easeStart) / this._easeOptions.duration, 1);
1233 this._onEaseFrame(this._easeOptions.easing(t));
1234 if (t < 1) {
1235 this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback);
1236 } else {
1237 this.stop();
1238 }
1239 }
1240
1241 // convert bearing so that it's numerically close to the current one so that it interpolates properly
1242 _normalizeBearing(bearing: number, currentBearing: number) {
1243 bearing = wrap(bearing, -180, 180);
1244 const diff = Math.abs(bearing - currentBearing);
1245 if (Math.abs(bearing - 360 - currentBearing) < diff) bearing -= 360;
1246 if (Math.abs(bearing + 360 - currentBearing) < diff) bearing += 360;
1247 return bearing;
1248 }
1249
1250 // If a path crossing the antimeridian would be shorter, extend the final coordinate so that
1251 // interpolating between the two endpoints will cross it.
1252 _normalizeCenter(center: LngLat) {
1253 const tr = this.transform;
1254 if (!tr.renderWorldCopies || tr.lngRange) return;
1255
1256 const delta = center.lng - tr.center.lng;
1257 center.lng +=
1258 delta > 180 ? -360 :
1259 delta < -180 ? 360 : 0;
1260 }
1261}
1262
1263// In debug builds, check that camera change events are fired in the correct order.
1264// - ___start events needs to be fired before ___ and ___end events
1265// - another ___start event can't be fired before a ___end event has been fired for the previous one
1266function addAssertions(camera: Camera) { //eslint-disable-line
1267 Debug.run(() => {
1268 const inProgress = {} as any;
1269
1270 ['drag', 'zoom', 'rotate', 'pitch', 'move'].forEach(name => {
1271 inProgress[name] = false;
1272
1273 camera.on(`${name}start`, () => {
1274 assert(!inProgress[name], `"${name}start" fired twice without a "${name}end"`);
1275 inProgress[name] = true;
1276 assert(inProgress.move);
1277 });
1278
1279 camera.on(name, () => {
1280 assert(inProgress[name]);
1281 assert(inProgress.move);
1282 });
1283
1284 camera.on(`${name}end`, () => {
1285 assert(inProgress.move);
1286 assert(inProgress[name]);
1287 inProgress[name] = false;
1288 });
1289 });
1290
1291 // Canary used to test whether this function is stripped in prod build
1292 canary = 'canary debug run'; // eslint-disable-line
1293 });
1294}
1295
1296let canary; // eslint-disable-line
1297
1298export default Camera;