144 lines
4.8 KiB
TypeScript

/* global document */
import * as React from 'react';
import {createPortal} from 'react-dom';
import {useEffect, useState, useRef, useContext} from 'react';
import mapboxgl from '../utils/mapboxgl';
import type {MarkerDragEvent, MapboxPopup, PointLike, Anchor, Alignment} from '../types';
import {MapContext} from './map';
import {arePointsEqual} from '../utils/deep-equal';
export type MarkerProps = {
/** Longitude of the anchor location */
longitude: number;
/** Latitude of the anchor location */
latitude: number;
/** A string indicating the part of the Marker that should be positioned closest to the coordinate set via Marker.setLngLat.
* Options are `'center'`, `'top'`, `'bottom'`, `'left'`, `'right'`, `'top-left'`, `'top-right'`, `'bottom-left'`, and `'bottom-right'`.
* @default "center"
*/
anchor?: Anchor;
/**
* 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 (0) is to inherit map's clickTolerance.
*/
clickTolerance?: number;
/** The color to use for the default marker if options.element is not provided.
* @default "#3FB1CE"
*/
color?: string;
/** A boolean indicating whether or not a marker is able to be dragged to a new position on the map.
* @default false
*/
draggable?: boolean;
/** The offset in pixels as a PointLike object to apply relative to the element's center. Negatives indicate left and up. */
offset?: PointLike;
/** `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`.
* @default "auto"
*/
pitchAlignment?: Alignment;
/** The rotation angle of the marker in degrees, relative to its `rotationAlignment` setting. A positive value will rotate the marker clockwise.
* @default 0
*/
rotation?: number;
/** `map` aligns the `Marker`'s rotation relative to the map, maintaining a bearing as the map rotates.
* `viewport` aligns the `Marker`'s rotation relative to the viewport, agnostic to map rotations.
* `auto` is equivalent to `viewport`.
* @default "auto"
*/
rotationAlignment?: Alignment;
/** The scale to use for the default marker if options.element is not provided.
* The default scale (1) corresponds to a height of `41px` and a width of `27px`.
* @default 1
*/
scale?: number;
/** A Popup instance that is bound to the marker */
popup?: MapboxPopup;
onDragStart?: (e: MarkerDragEvent) => void;
onDrag?: (e: MarkerDragEvent) => void;
onDragEnd?: (e: MarkerDragEvent) => void;
children?: React.ReactNode;
};
const defaultProps: Partial<MarkerProps> = {
draggable: false,
popup: null,
rotation: 0,
rotationAlignment: 'auto',
pitchAlignment: 'auto'
};
function Marker(props: MarkerProps) {
const map = useContext(MapContext);
const [marker] = useState(() => {
let hasChildren = false;
React.Children.forEach(props.children, el => {
if (el) {
hasChildren = true;
}
});
const options = {
...props,
element: hasChildren ? document.createElement('div') : null
};
return new mapboxgl.Marker(options).setLngLat([props.longitude, props.latitude]).addTo(map);
});
const thisRef = useRef({props});
thisRef.current.props = props;
useEffect(() => {
marker.on('dragstart', e => {
const evt = e as MarkerDragEvent;
evt.lngLat = marker.getLngLat();
thisRef.current.props.onDragStart?.(evt);
});
marker.on('drag', e => {
const evt = e as MarkerDragEvent;
evt.lngLat = marker.getLngLat();
thisRef.current.props.onDrag?.(evt);
});
marker.on('dragend', e => {
const evt = e as MarkerDragEvent;
evt.lngLat = marker.getLngLat();
thisRef.current.props.onDragEnd?.(evt);
});
return () => {
marker.remove();
};
}, []);
if (marker.getLngLat().lng !== props.longitude || marker.getLngLat().lat !== props.latitude) {
marker.setLngLat([props.longitude, props.latitude]);
}
if (props.offset && !arePointsEqual(marker.getOffset(), props.offset)) {
marker.setOffset(props.offset);
}
if (marker.isDraggable() !== props.draggable) {
marker.setDraggable(props.draggable);
}
if (marker.getRotation() !== props.rotation) {
marker.setRotation(props.rotation);
}
if (marker.getRotationAlignment() !== props.rotationAlignment) {
marker.setRotationAlignment(props.rotationAlignment);
}
if (marker.getPitchAlignment() !== props.pitchAlignment) {
marker.setPitchAlignment(props.pitchAlignment);
}
if (marker.getPopup() !== props.popup) {
marker.setPopup(props.popup);
}
return createPortal(props.children, marker.getElement());
}
Marker.defaultProps = defaultProps;
// @ts-ignore
export default React.memo(Marker);