MOON
Server: Apache
System: Linux nserver.cafsindia.com 4.18.0-553.104.1.lve.el8.x86_64 #1 SMP Tue Feb 10 20:07:30 UTC 2026 x86_64
User: cafsindia (1002)
PHP: 8.2.30
Disabled: NONE
Upload Files
File: //home/cafsindia/snap.cafsinfotech.in/node_modules/mapbox-gl/src/ui/marker.js
// @flow

import * as DOM from '../util/dom.js';
import window from '../util/window.js';
import LngLat from '../geo/lng_lat.js';
import Point from '@mapbox/point-geometry';
import smartWrap from '../util/smart_wrap.js';
import {bindAll, extend, radToDeg, smoothstep} from '../util/util.js';
import {type Anchor, anchorTranslate} from './anchor.js';
import {Event, Evented} from '../util/evented.js';
import type Map from './map.js';
import type Popup from './popup.js';
import type {LngLatLike} from "../geo/lng_lat.js";
import type {MapMouseEvent, MapTouchEvent} from './events.js';
import type {PointLike} from '@mapbox/point-geometry';
import {globeTiltAtLngLat, globeCenterToScreenPoint, isLngLatBehindGlobe, GLOBE_ZOOM_THRESHOLD_MAX} from '../geo/projection/globe_util.js';
import assert from 'assert';

type Options = {
    element?: HTMLElement,
    offset?: PointLike,
    anchor?: Anchor,
    color?: string,
    scale?: number,
    draggable?: boolean,
    clickTolerance?: number,
    rotation?: number,
    rotationAlignment?: string,
    pitchAlignment?: string,
    occludedOpacity?: number
};

/**
 * Creates a marker component.
 *
 * @param {Object} [options]
 * @param {HTMLElement} [options.element] DOM element to use as a marker. The default is a light blue, droplet-shaped SVG marker.
 * @param {string} [options.anchor='center'] A string indicating the part of the Marker that should be positioned closest to the coordinate set via {@link Marker#setLngLat}.
 *   Options are `'center'`, `'top'`, `'bottom'`, `'left'`, `'right'`, `'top-left'`, `'top-right'`, `'bottom-left'`, and `'bottom-right'`.
 * @param {PointLike} [options.offset] The offset in pixels as a {@link PointLike} object to apply relative to the element's center. Negatives indicate left and up.
 * @param {string} [options.color='#3FB1CE'] The color to use for the default marker if `options.element` is not provided. The default is light blue.
 * @param {number} [options.scale=1] The scale to use for the default marker if `options.element` is not provided. The default scale corresponds to a height of `41px` and a width of `27px`.
 * @param {boolean} [options.draggable=false] A boolean indicating whether or not a marker is able to be dragged to a new position on the map.
 * @param {number} [options.clickTolerance=0] The max number of pixels a user can shift the mouse pointer during a click on the marker for it to be considered a valid click (as opposed to a marker drag). The default is to inherit map's `clickTolerance`.
 * @param {number} [options.rotation=0] The rotation angle of the marker in degrees, relative to its respective `rotationAlignment` setting. A positive value will rotate the marker clockwise.
 * @param {string} [options.pitchAlignment='auto'] `'map'` aligns the `Marker` to the plane of the map. `'viewport'` aligns the `Marker` to the plane of the viewport. `'auto'` automatically matches the value of `rotationAlignment`.
 * @param {string} [options.rotationAlignment='auto'] The alignment of the marker's rotation.`'map'` is aligned with the map plane, consistent with the cardinal directions as the map rotates. `'viewport'` is screenspace-aligned. `'horizon'` is aligned according to the nearest horizon, on non-globe projections it is equivalent to `'viewport'`. `'auto'` is equivalent to `'viewport'`.
 * @param {number} [options.occludedOpacity=0.2] The opacity of a marker that's occluded by 3D terrain.
 * @example
 * // Create a new marker.
 * const marker = new mapboxgl.Marker()
 *     .setLngLat([30.5, 50.5])
 *     .addTo(map);
 * @example
 * // Set marker options.
 * const marker = new mapboxgl.Marker({
 *     color: "#FFFFFF",
 *     draggable: true
 * }).setLngLat([30.5, 50.5])
 *     .addTo(map);
 * @see [Example: Add custom icons with Markers](https://www.mapbox.com/mapbox-gl-js/example/custom-marker-icons/)
 * @see [Example: Create a draggable Marker](https://www.mapbox.com/mapbox-gl-js/example/drag-a-marker/)
 */
export default class Marker extends Evented {
    _map: ?Map;
    _anchor: Anchor;
    _offset: Point;
    _element: HTMLElement;
    _popup: ?Popup;
    _lngLat: LngLat;
    _pos: ?Point;
    _color: string;
    _scale: number;
    _defaultMarker: boolean;
    _draggable: boolean;
    _clickTolerance: number;
    _isDragging: boolean;
    _state: 'inactive' | 'pending' | 'active'; // used for handling drag events
    _positionDelta: ?Point;
    _pointerdownPos: ?Point;
    _rotation: number;
    _pitchAlignment: string;
    _rotationAlignment: string;
    _originalTabIndex: ?string; // original tabindex of _element
    _fadeTimer: ?TimeoutID;
    _updateFrameId: number;
    _updateMoving: () => void;
    _occludedOpacity: number;

    constructor(options?: Options, legacyOptions?: Options) {
        super();
        // For backward compatibility -- the constructor used to accept the element as a
        // required first argument, before it was made optional.
        if (options instanceof window.HTMLElement || legacyOptions) {
            options = extend({element: options}, legacyOptions);
        }

        bindAll([
            '_update',
            '_onMove',
            '_onUp',
            '_addDragHandler',
            '_onMapClick',
            '_onKeyPress',
            '_clearFadeTimer'
        ], this);

        this._anchor = (options && options.anchor) || 'center';
        this._color = (options && options.color) || '#3FB1CE';
        this._scale = (options && options.scale) || 1;
        this._draggable = (options && options.draggable) || false;
        this._clickTolerance = (options && options.clickTolerance) || 0;
        this._isDragging = false;
        this._state = 'inactive';
        this._rotation = (options && options.rotation) || 0;
        this._rotationAlignment = (options && options.rotationAlignment) || 'auto';
        this._pitchAlignment = (options && options.pitchAlignment && options.pitchAlignment) || 'auto';
        this._updateMoving = () => this._update(true);
        this._occludedOpacity = (options && options.occludedOpacity) || 0.2;

        if (!options || !options.element) {
            this._defaultMarker = true;
            this._element = DOM.create('div');

            // create default map marker SVG

            const DEFAULT_HEIGHT = 41;
            const DEFAULT_WIDTH = 27;

            const svg = DOM.createSVG('svg', {
                display: 'block',
                height: `${DEFAULT_HEIGHT * this._scale}px`,
                width: `${DEFAULT_WIDTH * this._scale}px`,
                viewBox: `0 0 ${DEFAULT_WIDTH} ${DEFAULT_HEIGHT}`
            }, this._element);

            const gradient = DOM.createSVG('radialGradient', {id: 'shadowGradient'}, DOM.createSVG('defs', {}, svg));
            DOM.createSVG('stop', {offset: '10%', 'stop-opacity': 0.4}, gradient);
            DOM.createSVG('stop', {offset: '100%', 'stop-opacity': 0.05}, gradient);
            DOM.createSVG('ellipse', {cx: 13.5, cy: 34.8, rx: 10.5, ry: 5.25, fill: 'url(#shadowGradient)'}, svg); // shadow

            DOM.createSVG('path', { // marker shape
                fill: this._color,
                d: 'M27,13.5C27,19.07 20.25,27 14.75,34.5C14.02,35.5 12.98,35.5 12.25,34.5C6.75,27 0,19.22 0,13.5C0,6.04 6.04,0 13.5,0C20.96,0 27,6.04 27,13.5Z'
            }, svg);
            DOM.createSVG('path', { // border
                opacity: 0.25,
                d: 'M13.5,0C6.04,0 0,6.04 0,13.5C0,19.22 6.75,27 12.25,34.5C13,35.52 14.02,35.5 14.75,34.5C20.25,27 27,19.07 27,13.5C27,6.04 20.96,0 13.5,0ZM13.5,1C20.42,1 26,6.58 26,13.5C26,15.9 24.5,19.18 22.22,22.74C19.95,26.3 16.71,30.14 13.94,33.91C13.74,34.18 13.61,34.32 13.5,34.44C13.39,34.32 13.26,34.18 13.06,33.91C10.28,30.13 7.41,26.31 5.02,22.77C2.62,19.23 1,15.95 1,13.5C1,6.58 6.58,1 13.5,1Z'
            }, svg);

            DOM.createSVG('circle', {fill: 'white', cx: 13.5, cy: 13.5, r: 5.5}, svg); // circle

            // if no element and no offset option given apply an offset for the default marker
            // the -14 as the y value of the default marker offset was determined as follows
            //
            // the marker tip is at the center of the shadow ellipse from the default svg
            // the y value of the center of the shadow ellipse relative to the svg top left is 34.8
            // offset to the svg center "height (41 / 2)" gives 34.8 - (41 / 2) and rounded for an integer pixel offset gives 14
            // negative is used to move the marker up from the center so the tip is at the Marker lngLat
            this._offset = Point.convert((options && options.offset) || [0, -14]);
        } else {
            this._element = options.element;
            this._offset = Point.convert((options && options.offset) || [0, 0]);
        }

        if (!this._element.hasAttribute('aria-label')) this._element.setAttribute('aria-label', 'Map marker');
        this._element.classList.add('mapboxgl-marker');
        this._element.addEventListener('dragstart', (e: DragEvent) => {
            e.preventDefault();
        });
        this._element.addEventListener('mousedown', (e: MouseEvent) => {
            // prevent focusing on click
            e.preventDefault();
        });
        const classList = this._element.classList;
        for (const key in anchorTranslate) {
            classList.remove(`mapboxgl-marker-anchor-${key}`);
        }
        classList.add(`mapboxgl-marker-anchor-${this._anchor}`);

        this._popup = null;
    }

    /**
     * Attaches the `Marker` to a `Map` object.
     *
     * @param {Map} map The Mapbox GL JS map to add the marker to.
     * @returns {Marker} Returns itself to allow for method chaining.
     * @example
     * const marker = new mapboxgl.Marker()
     *     .setLngLat([30.5, 50.5])
     *     .addTo(map); // add the marker to the map
     */
    addTo(map: Map): this {
        if (map === this._map) {
            return this;
        }
        this.remove();
        this._map = map;
        map.getCanvasContainer().appendChild(this._element);
        map.on('move', this._updateMoving);
        // $FlowFixMe[method-unbinding]
        map.on('moveend', this._update);
        // $FlowFixMe[method-unbinding]
        map.on('remove', this._clearFadeTimer);
        map._addMarker(this);
        this.setDraggable(this._draggable);
        this._update();

        // If we attached the `click` listener to the marker element, the popup
        // would close once the event propogated to `map` due to the
        // `Popup#_onClickClose` listener.
        // $FlowFixMe[method-unbinding]
        map.on('click', this._onMapClick);

        return this;
    }

    /**
     * Removes the marker from a map.
     *
     * @example
     * const marker = new mapboxgl.Marker().addTo(map);
     * marker.remove();
     * @returns {Marker} Returns itself to allow for method chaining.
     */
    remove(): this {
        const map = this._map;
        if (map) {
            // $FlowFixMe[method-unbinding]
            map.off('click', this._onMapClick);
            map.off('move', this._updateMoving);
            // $FlowFixMe[method-unbinding]
            map.off('moveend', this._update);
            // $FlowFixMe[method-unbinding]
            map.off('mousedown', this._addDragHandler);
            // $FlowFixMe[method-unbinding]
            map.off('touchstart', this._addDragHandler);
            // $FlowFixMe[method-unbinding]
            map.off('mouseup', this._onUp);
            // $FlowFixMe[method-unbinding]
            map.off('touchend', this._onUp);
            // $FlowFixMe[method-unbinding]
            map.off('mousemove', this._onMove);
            // $FlowFixMe[method-unbinding]
            map.off('touchmove', this._onMove);
            // $FlowFixMe[method-unbinding]
            map.off('remove', this._clearFadeTimer);
            map._removeMarker(this);
            this._map = undefined;
        }
        this._clearFadeTimer();
        this._element.remove();
        if (this._popup) this._popup.remove();
        return this;
    }

    /**
     * Get the marker's geographical location.
     *
     * The longitude of the result may differ by a multiple of 360 degrees from the longitude previously
     * set by `setLngLat` because `Marker` wraps the anchor longitude across copies of the world to keep
     * the marker on screen.
     *
     * @returns {LngLat} A {@link LngLat} describing the marker's location.
    * @example
    * // Store the marker's longitude and latitude coordinates in a variable
    * const lngLat = marker.getLngLat();
    * // Print the marker's longitude and latitude values in the console
    * console.log(`Longitude: ${lngLat.lng}, Latitude: ${lngLat.lat}`);
    * @see [Example: Create a draggable Marker](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-marker/)
    */
    getLngLat(): LngLat {
        return this._lngLat;
    }

    /**
    * Set the marker's geographical position and move it.
     *
    * @param {LngLat} lnglat A {@link LngLat} describing where the marker should be located.
    * @returns {Marker} Returns itself to allow for method chaining.
    * @example
    * // Create a new marker, set the longitude and latitude, and add it to the map.
    * new mapboxgl.Marker()
    *     .setLngLat([-65.017, -16.457])
    *     .addTo(map);
    * @see [Example: Add custom icons with Markers](https://docs.mapbox.com/mapbox-gl-js/example/custom-marker-icons/)
    * @see [Example: Create a draggable Marker](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-marker/)
    * @see [Example: Add a marker using a place name](https://docs.mapbox.com/mapbox-gl-js/example/marker-from-geocode/)
    */
    setLngLat(lnglat: LngLatLike): this {
        this._lngLat = LngLat.convert(lnglat);
        this._pos = null;
        if (this._popup) this._popup.setLngLat(this._lngLat);
        this._update(true);
        return this;
    }

    /**
     * Returns the `Marker`'s HTML element.
     *
     * @returns {HTMLElement} Returns the marker element.
     * @example
     * const element = marker.getElement();
     */
    getElement(): HTMLElement {
        return this._element;
    }

    /**
     * Binds a {@link Popup} to the {@link Marker}.
     *
     * @param {Popup | null} popup An instance of the {@link Popup} class. If undefined or null, any popup
     * set on this {@link Marker} instance is unset.
     * @returns {Marker} Returns itself to allow for method chaining.
     * @example
     * const marker = new mapboxgl.Marker()
     *     .setLngLat([0, 0])
     *     .setPopup(new mapboxgl.Popup().setHTML("<h1>Hello World!</h1>")) // add popup
     *     .addTo(map);
     * @see [Example: Attach a popup to a marker instance](https://docs.mapbox.com/mapbox-gl-js/example/set-popup/)
     */
    setPopup(popup: ?Popup): this {
        if (this._popup) {
            this._popup.remove();
            this._popup = null;
            this._element.removeAttribute('role');
            // $FlowFixMe[method-unbinding]
            this._element.removeEventListener('keypress', this._onKeyPress);

            if (!this._originalTabIndex) {
                this._element.removeAttribute('tabindex');
            }
        }

        if (popup) {
            if (!('offset' in popup.options)) {
                const markerHeight = 41 - (5.8 / 2);
                const markerRadius = 13.5;
                const linearOffset = Math.sqrt(Math.pow(markerRadius, 2) / 2);
                popup.options.offset = this._defaultMarker ? {
                    'top': [0, 0],
                    'top-left': [0, 0],
                    'top-right': [0, 0],
                    'bottom': [0, -markerHeight],
                    'bottom-left': [linearOffset, (markerHeight - markerRadius + linearOffset) * -1],
                    'bottom-right': [-linearOffset, (markerHeight - markerRadius + linearOffset) * -1],
                    'left': [markerRadius, (markerHeight - markerRadius) * -1],
                    'right': [-markerRadius, (markerHeight - markerRadius) * -1]
                } : this._offset;
            }
            this._popup = popup;
            popup._marker = this;
            if (this._lngLat) this._popup.setLngLat(this._lngLat);

            this._element.setAttribute('role', 'button');
            this._originalTabIndex = this._element.getAttribute('tabindex');
            if (!this._originalTabIndex) {
                this._element.setAttribute('tabindex', '0');
            }
            // $FlowFixMe[method-unbinding]
            this._element.addEventListener('keypress', this._onKeyPress);
            this._element.setAttribute('aria-expanded', 'false');
        }

        return this;
    }

    _onKeyPress(e: KeyboardEvent) {
        const code = e.code;
        const legacyCode = e.charCode || e.keyCode;

        if (
            (code === 'Space') || (code === 'Enter') ||
            (legacyCode === 32) || (legacyCode === 13) // space or enter
        ) {
            this.togglePopup();
        }
    }

    _onMapClick(e: MapMouseEvent) {
        const targetElement = e.originalEvent.target;
        const element = this._element;

        if (this._popup && (targetElement === element || element.contains((targetElement: any)))) {
            this.togglePopup();
        }
    }

    /**
     * Returns the {@link Popup} instance that is bound to the {@link Marker}.
     *
     * @returns {Popup} Returns the popup.
     * @example
     * const marker = new mapboxgl.Marker()
     *     .setLngLat([0, 0])
     *     .setPopup(new mapboxgl.Popup().setHTML("<h1>Hello World!</h1>"))
     *     .addTo(map);
     *
     * console.log(marker.getPopup()); // return the popup instance
     */
    getPopup(): ?Popup {
        return this._popup;
    }

    /**
     * Opens or closes the {@link Popup} instance that is bound to the {@link Marker}, depending on the current state of the {@link Popup}.
     *
     * @returns {Marker} Returns itself to allow for method chaining.
     * @example
     * const marker = new mapboxgl.Marker()
     *     .setLngLat([0, 0])
     *     .setPopup(new mapboxgl.Popup().setHTML("<h1>Hello World!</h1>"))
     *     .addTo(map);
     *
     * marker.togglePopup(); // toggle popup open or closed
     */
    togglePopup(): this {
        const popup = this._popup;
        if (!popup) {
            return this;
        } else if (popup.isOpen()) {
            popup.remove();
            this._element.setAttribute('aria-expanded', 'false');
        } else if (this._map) {
            popup.addTo(this._map);
            this._element.setAttribute('aria-expanded', 'true');
        }
        return this;
    }

    _behindTerrain(): boolean {
        const map = this._map;
        const pos = this._pos;
        if (!map || !pos) return false;
        const unprojected = map.unproject(pos);
        const camera = map.getFreeCameraOptions();
        if (!camera.position) return false;
        const cameraLngLat = camera.position.toLngLat();
        const toClosestSurface = cameraLngLat.distanceTo(unprojected);
        const toMarker = cameraLngLat.distanceTo(this._lngLat);
        return toClosestSurface < toMarker * 0.9;

    }

    _evaluateOpacity() {
        const map = this._map;
        if (!map) return;

        const pos = this._pos;

        if (!pos || pos.x < 0 || pos.x > map.transform.width || pos.y < 0 || pos.y > map.transform.height) {
            this._clearFadeTimer();
            return;
        }
        const mapLocation = map.unproject(pos);
        let opacity;
        if (map._showingGlobe() && isLngLatBehindGlobe(map.transform, this._lngLat)) {
            opacity = 0;
        } else {
            opacity = 1 - map._queryFogOpacity(mapLocation);
            if (map.transform._terrainEnabled() && map.getTerrain() && this._behindTerrain()) {
                opacity *= this._occludedOpacity;
            }
        }

        this._element.style.opacity = `${opacity}`;
        this._element.style.pointerEvents = opacity > 0 ? 'auto' : 'none';
        if (this._popup) {
            this._popup._setOpacity(opacity);
        }

        this._fadeTimer = null;
    }

    _clearFadeTimer() {
        if (this._fadeTimer) {
            clearTimeout(this._fadeTimer);
            this._fadeTimer = null;
        }
    }

    _updateDOM() {
        const pos = this._pos;
        const map = this._map;
        if (!pos || !map) { return; }

        const offset = this._offset.mult(this._scale);

        this._element.style.transform = `
            translate(${pos.x}px,${pos.y}px)
            ${anchorTranslate[this._anchor]}
            ${this._calculateXYTransform()} ${this._calculateZTransform()}
            translate(${offset.x}px,${offset.y}px)
        `;
    }

    _calculateXYTransform(): string {
        const pos = this._pos;
        const map = this._map;
        const alignment = this.getPitchAlignment();

        // `viewport', 'auto' and invalid arugments do no pitch transformation.
        if (!map || !pos || alignment !== 'map') {
            return ``;
        }
        // 'map' alignment on a flat map
        if (!map._showingGlobe()) {
            const pitch = map.getPitch();
            return pitch ? `rotateX(${pitch}deg)` : '';
        }
        // 'map' alignment on globe
        const tilt = radToDeg(globeTiltAtLngLat(map.transform, this._lngLat));
        const posFromCenter = pos.sub(globeCenterToScreenPoint(map.transform));
        const manhattanDistance = (Math.abs(posFromCenter.x) + Math.abs(posFromCenter.y));
        if (manhattanDistance === 0) { return ''; }

        const tiltOverDist =  tilt / manhattanDistance;
        const yTilt = posFromCenter.x * tiltOverDist;
        const xTilt = -posFromCenter.y * tiltOverDist;
        return `rotateX(${xTilt}deg) rotateY(${yTilt}deg)`;

    }

    _calculateZTransform(): string {

        const pos = this._pos;
        const map = this._map;
        if (!map || !pos) { return ''; }

        let rotation = 0;
        const alignment = this.getRotationAlignment();
        if (alignment === 'map') {
            if (map._showingGlobe()) {
                const north = map.project(new LngLat(this._lngLat.lng, this._lngLat.lat + .001));
                const south = map.project(new LngLat(this._lngLat.lng, this._lngLat.lat - .001));
                const diff = south.sub(north);
                rotation = radToDeg(Math.atan2(diff.y, diff.x)) - 90;
            } else {
                rotation = -map.getBearing();
            }
        } else if (alignment === 'horizon') {
            const ALIGN_TO_HORIZON_BELOW_ZOOM = 4;
            const ALIGN_TO_SCREEN_ABOVE_ZOOM = 6;
            assert(ALIGN_TO_SCREEN_ABOVE_ZOOM <= GLOBE_ZOOM_THRESHOLD_MAX, 'Horizon-oriented marker transition should be complete when globe switches to Mercator');
            assert(ALIGN_TO_HORIZON_BELOW_ZOOM <= ALIGN_TO_SCREEN_ABOVE_ZOOM);

            const smooth = smoothstep(ALIGN_TO_HORIZON_BELOW_ZOOM, ALIGN_TO_SCREEN_ABOVE_ZOOM, map.getZoom());

            const centerPoint = globeCenterToScreenPoint(map.transform);
            centerPoint.y += smooth * map.transform.height;
            const rel = pos.sub(centerPoint);
            const angle = radToDeg(Math.atan2(rel.y, rel.x));
            const up = angle > 90 ? angle - 270 : angle + 90;
            rotation = up * (1 - smooth);
        }

        rotation += this._rotation;
        return rotation ? `rotateZ(${rotation}deg)` : '';
    }

    _update(delaySnap?: boolean) {
        window.cancelAnimationFrame(this._updateFrameId);
        const map = this._map;
        if (!map) return;

        if (map.transform.renderWorldCopies) {
            this._lngLat = smartWrap(this._lngLat, this._pos, map.transform);
        }

        this._pos = map.project(this._lngLat);

        // because rounding the coordinates at every `move` event causes stuttered zooming
        // we only round them when _update is called with `moveend` or when its called with
        // no arguments (when the Marker is initialized or Marker#setLngLat is invoked).
        if (delaySnap === true) {
            this._updateFrameId = window.requestAnimationFrame(() => {
                if (this._element && this._pos && this._anchor) {
                    this._pos = this._pos.round();
                    this._updateDOM();
                }
            });
        } else {
            this._pos = this._pos.round();
        }

        map._requestDomTask(() => {
            if (!this._map) return;

            if (this._element && this._pos && this._anchor) {
                this._updateDOM();
            }

            if ((map._showingGlobe() || map.getTerrain() || map.getFog()) && !this._fadeTimer) {
                // $FlowFixMe[method-unbinding]
                this._fadeTimer = setTimeout(this._evaluateOpacity.bind(this), 60);
            }
        });
    }

    /**
     * Get the marker's offset.
     *
     * @returns {Point} The marker's screen coordinates in pixels.
     * @example
     * const offset = marker.getOffset();
     */
    getOffset(): Point {
        return this._offset;
    }

    /**
     * Sets the offset of the marker.
     *
     * @param {PointLike} offset The offset in pixels as a {@link PointLike} object to apply relative to the element's center. Negatives indicate left and up.
     * @returns {Marker} Returns itself to allow for method chaining.
     * @example
     * marker.setOffset([0, 1]);
     */
    setOffset(offset: PointLike): this {
        this._offset = Point.convert(offset);
        this._update();
        return this;
    }

    _onMove(e: MapMouseEvent | MapTouchEvent) {
        const map = this._map;
        if (!map) return;

        const startPos = this._pointerdownPos;
        const posDelta = this._positionDelta;
        if (!startPos || !posDelta) return;

        if (!this._isDragging) {
            const clickTolerance = this._clickTolerance || map._clickTolerance;
            if (e.point.dist(startPos) < clickTolerance) return;
            this._isDragging = true;
        }

        this._pos = e.point.sub(posDelta);
        this._lngLat = map.unproject(this._pos);
        this.setLngLat(this._lngLat);
        // suppress click event so that popups don't toggle on drag
        this._element.style.pointerEvents = 'none';

        // make sure dragstart only fires on the first move event after mousedown.
        // this can't be on mousedown because that event doesn't necessarily
        // imply that a drag is about to happen.
        if (this._state === 'pending') {
            this._state = 'active';

            /**
             * Fired when dragging starts.
             *
             * @event dragstart
             * @memberof Marker
             * @instance
             * @type {Object}
             * @property {Marker} marker The object that is being dragged.
             */
            this.fire(new Event('dragstart'));
        }

        /**
         * Fired while dragging.
         *
         * @event drag
         * @memberof Marker
         * @instance
         * @type {Object}
         * @property {Marker} marker The object that is being dragged.
         */
        this.fire(new Event('drag'));
    }

    _onUp() {
        // revert to normal pointer event handling
        this._element.style.pointerEvents = 'auto';
        this._positionDelta = null;
        this._pointerdownPos = null;
        this._isDragging = false;

        const map = this._map;
        if (map) {
            // $FlowFixMe[method-unbinding]
            map.off('mousemove', this._onMove);
            // $FlowFixMe[method-unbinding]
            map.off('touchmove', this._onMove);
        }

        // only fire dragend if it was preceded by at least one drag event
        if (this._state === 'active') {
            /**
            * Fired when the marker is finished being dragged.
            *
            * @event dragend
            * @memberof Marker
            * @instance
            * @type {Object}
            * @property {Marker} marker The object that was dragged.
            */
            this.fire(new Event('dragend'));
        }

        this._state = 'inactive';
    }

    _addDragHandler(e: MapMouseEvent | MapTouchEvent) {
        const map = this._map;
        const pos = this._pos;
        if (!map || !pos) return;

        if (this._element.contains((e.originalEvent.target: any))) {
            e.preventDefault();

            // We need to calculate the pixel distance between the click point
            // and the marker position, with the offset accounted for. Then we
            // can subtract this distance from the mousemove event's position
            // to calculate the new marker position.
            // If we don't do this, the marker 'jumps' to the click position
            // creating a jarring UX effect.
            this._positionDelta = e.point.sub(pos);
            this._pointerdownPos = e.point;

            this._state = 'pending';
            // $FlowFixMe[method-unbinding]
            map.on('mousemove', this._onMove);
            // $FlowFixMe[method-unbinding]
            map.on('touchmove', this._onMove);
            // $FlowFixMe[method-unbinding]
            map.once('mouseup', this._onUp);
            // $FlowFixMe[method-unbinding]
            map.once('touchend', this._onUp);
        }
    }

    /**
     * Sets the `draggable` property and functionality of the marker.
     *
     * @param {boolean} [shouldBeDraggable=false] Turns drag functionality on/off.
     * @returns {Marker} Returns itself to allow for method chaining.
     * @example
     * marker.setDraggable(true);
     */
    setDraggable(shouldBeDraggable: boolean): this {
        this._draggable = !!shouldBeDraggable; // convert possible undefined value to false

        // handle case where map may not exist yet
        // for example, when setDraggable is called before addTo
        const map = this._map;
        if (map) {
            if (shouldBeDraggable) {
                // $FlowFixMe[method-unbinding]
                map.on('mousedown', this._addDragHandler);
                // $FlowFixMe[method-unbinding]
                map.on('touchstart', this._addDragHandler);
            } else {
                // $FlowFixMe[method-unbinding]
                map.off('mousedown', this._addDragHandler);
                // $FlowFixMe[method-unbinding]
                map.off('touchstart', this._addDragHandler);
            }
        }

        return this;
    }

    /**
     * Returns true if the marker can be dragged.
     *
     * @returns {boolean} True if the marker is draggable.
     * @example
     * const isMarkerDraggable = marker.isDraggable();
     */
    isDraggable(): boolean {
        return this._draggable;
    }

    /**
     * Sets the `rotation` property of the marker.
     *
     * @param {number} [rotation=0] The rotation angle of the marker (clockwise, in degrees), relative to its respective {@link Marker#setRotationAlignment} setting.
     * @returns {Marker} Returns itself to allow for method chaining.
     * @example
     * marker.setRotation(45);
     */
    setRotation(rotation: number): this {
        this._rotation = rotation || 0;
        this._update();
        return this;
    }

    /**
     * Returns the current rotation angle of the marker (in degrees).
     *
     * @returns {number} The current rotation angle of the marker.
     * @example
     * const rotation = marker.getRotation();
     */
    getRotation(): number {
        return this._rotation;
    }

    /**
     * Sets the `rotationAlignment` property of the marker.
     *
     * @param {string} [alignment='auto'] Sets the `rotationAlignment` property of the marker.
     * @returns {Marker} Returns itself to allow for method chaining.
     * @example
     * marker.setRotationAlignment('viewport');
     */
    setRotationAlignment(alignment: string): this {
        this._rotationAlignment = alignment || 'auto';
        this._update();
        return this;
    }

    /**
     * Returns the current `rotationAlignment` property of the marker.
     *
     * @returns {string} The current rotational alignment of the marker.
     * @example
     * const alignment = marker.getRotationAlignment();
     */
    getRotationAlignment(): string {
        if (this._rotationAlignment === 'auto')
            return 'viewport';
        if (this._rotationAlignment === 'horizon' && this._map && !this._map._showingGlobe())
            return 'viewport';
        return this._rotationAlignment;
    }

    /**
     * Sets the `pitchAlignment` property of the marker.
     *
     * @param {string} [alignment] Sets the `pitchAlignment` property of the marker. If alignment is 'auto', it will automatically match `rotationAlignment`.
     * @returns {Marker} Returns itself to allow for method chaining.
     * @example
     * marker.setPitchAlignment('map');
     */
    setPitchAlignment(alignment: string): this {
        this._pitchAlignment = alignment || 'auto';
        this._update();
        return this;
    }

    /**
     * Returns the current `pitchAlignment` behavior of the marker.
     *
     * @returns {string} The current pitch alignment of the marker.
     * @example
     * const alignment = marker.getPitchAlignment();
     */
    getPitchAlignment(): string {
        if (this._pitchAlignment === 'auto') {
            return this.getRotationAlignment();
        }
        return this._pitchAlignment;
    }

    /**
     * Sets the `occludedOpacity` property of the marker.
     * This opacity is used on the marker when the marker is occluded by terrain.
     *
     * @param {number} [opacity=0.2] Sets the `occludedOpacity` property of the marker.
     * @returns {Marker} Returns itself to allow for method chaining.
     * @example
     * marker.setOccludedOpacity(0.3);
     */
    setOccludedOpacity(opacity: number): this {
        this._occludedOpacity = opacity || 0.2;
        this._update();
        return this;
    }

    /**
     * Returns the current `occludedOpacity` of the marker.
     *
     * @returns {number} The opacity of a terrain occluded marker.
     * @example
     * const opacity = marker.getOccludedOpacity();
     */
    getOccludedOpacity(): number {
        return this._occludedOpacity;
    }
}