mapbox-gl
Version:
A WebGL interactive maps library
887 lines (795 loc) • 34.4 kB
JavaScript
// @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;
}
}