maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
293 lines (261 loc) • 11 kB
text/typescript
import Point from '@mapbox/point-geometry';
import {DOM} from '../../util/dom';
import {extend, getAngleDelta} from '../../util/util';
import {DragHandler, type DragMoveHandler, type DragRotateResult} from '../handler/drag_handler';
import {MouseOrTouchMoveStateManager} from '../handler/drag_move_state_manager';
import type {Map} from '../map';
import type {IControl} from './control';
/**
* The {@link NavigationControl} options object
*/
type NavigationControlOptions = {
/**
* If `true` the compass button is included.
*/
showCompass?: boolean;
/**
* If `true` the zoom-in and zoom-out buttons are included.
*/
showZoom?: boolean;
/**
* If `true` the pitch is visualized by rotating X-axis of compass.
*/
visualizePitch?: boolean;
/**
* If `true` the roll is visualized by rotating the compass.
*/
visualizeRoll?: boolean;
};
const defaultOptions: NavigationControlOptions = {
showCompass: true,
showZoom: true,
visualizePitch: false,
visualizeRoll: true
};
/**
* A `NavigationControl` control contains zoom buttons and a compass.
*
* @group Markers and Controls
*
* @example
* ```ts
* let nav = new NavigationControl();
* map.addControl(nav, 'top-left');
* ```
* @see [Display map navigation controls](https://maplibre.org/maplibre-gl-js/docs/examples/navigation/)
*/
export class NavigationControl implements IControl {
_map: Map;
options: NavigationControlOptions;
_container: HTMLElement;
_zoomInButton: HTMLButtonElement;
_zoomOutButton: HTMLButtonElement;
_compass: HTMLButtonElement;
_compassIcon: HTMLElement;
_handler: MouseRotateWrapper;
/**
* @param options - the control's options
*/
constructor(options?: NavigationControlOptions) {
this.options = extend({}, defaultOptions, options);
this._container = DOM.create('div', 'maplibregl-ctrl maplibregl-ctrl-group');
this._container.addEventListener('contextmenu', (e) => e.preventDefault());
if (this.options.showZoom) {
this._zoomInButton = this._createButton('maplibregl-ctrl-zoom-in', (e) => this._map.zoomIn({}, {originalEvent: e}));
DOM.create('span', 'maplibregl-ctrl-icon', this._zoomInButton).setAttribute('aria-hidden', 'true');
this._zoomOutButton = this._createButton('maplibregl-ctrl-zoom-out', (e) => this._map.zoomOut({}, {originalEvent: e}));
DOM.create('span', 'maplibregl-ctrl-icon', this._zoomOutButton).setAttribute('aria-hidden', 'true');
}
if (this.options.showCompass) {
this._compass = this._createButton('maplibregl-ctrl-compass', (e) => {
if (this.options.visualizePitch) {
this._map.resetNorthPitch({}, {originalEvent: e});
} else {
this._map.resetNorth({}, {originalEvent: e});
}
});
this._compassIcon = DOM.create('span', 'maplibregl-ctrl-icon', this._compass);
this._compassIcon.setAttribute('aria-hidden', 'true');
}
}
_updateZoomButtons = () => {
const zoom = this._map.getZoom();
const isMax = zoom === this._map.getMaxZoom();
const isMin = zoom === this._map.getMinZoom();
this._zoomInButton.disabled = isMax;
this._zoomOutButton.disabled = isMin;
this._zoomInButton.setAttribute('aria-disabled', isMax.toString());
this._zoomOutButton.setAttribute('aria-disabled', isMin.toString());
};
_rotateCompassArrow = () => {
if (this.options.visualizePitch && this.options.visualizeRoll) {
this._compassIcon.style.transform = `scale(${1 / Math.pow(Math.cos(this._map.transform.pitchInRadians), 0.5)}) rotateZ(${-this._map.transform.roll}deg) rotateX(${this._map.transform.pitch}deg) rotateZ(${-this._map.transform.bearing}deg)`;
return;
}
if (this.options.visualizePitch) {
this._compassIcon.style.transform = `scale(${1 / Math.pow(Math.cos(this._map.transform.pitchInRadians), 0.5)}) rotateX(${this._map.transform.pitch}deg) rotateZ(${-this._map.transform.bearing}deg)`;
return;
}
if (this.options.visualizeRoll) {
this._compassIcon.style.transform = `rotate(${-this._map.transform.bearing - this._map.transform.roll}deg)`;
return;
}
this._compassIcon.style.transform = `rotate(${-this._map.transform.bearing}deg)`;
};
/** {@inheritDoc IControl.onAdd} */
onAdd(map: Map) {
this._map = map;
if (this.options.showZoom) {
this._setButtonTitle(this._zoomInButton, 'ZoomIn');
this._setButtonTitle(this._zoomOutButton, 'ZoomOut');
this._map.on('zoom', this._updateZoomButtons);
this._updateZoomButtons();
}
if (this.options.showCompass) {
this._setButtonTitle(this._compass, 'ResetBearing');
if (this.options.visualizePitch) {
this._map.on('pitch', this._rotateCompassArrow);
}
if (this.options.visualizeRoll) {
this._map.on('roll', this._rotateCompassArrow);
}
this._map.on('rotate', this._rotateCompassArrow);
this._rotateCompassArrow();
this._handler = new MouseRotateWrapper(this._map, this._compass, this.options.visualizePitch);
}
return this._container;
}
/** {@inheritDoc IControl.onRemove} */
onRemove() {
DOM.remove(this._container);
if (this.options.showZoom) {
this._map.off('zoom', this._updateZoomButtons);
}
if (this.options.showCompass) {
if (this.options.visualizePitch) {
this._map.off('pitch', this._rotateCompassArrow);
}
if (this.options.visualizeRoll) {
this._map.off('roll', this._rotateCompassArrow);
}
this._map.off('rotate', this._rotateCompassArrow);
this._handler.off();
delete this._handler;
}
delete this._map;
}
_createButton(className: string, fn: (e?: any) => unknown) {
const a = DOM.create('button', className, this._container) as HTMLButtonElement;
a.type = 'button';
a.addEventListener('click', fn);
return a;
}
_setButtonTitle = (button: HTMLButtonElement, title: 'ZoomIn' | 'ZoomOut' | 'ResetBearing') => {
const str = this._map._getUIString(`NavigationControl.${title}`);
button.title = str;
button.setAttribute('aria-label', str);
};
}
class MouseRotateWrapper {
map: Map;
_clickTolerance: number;
element: HTMLElement;
_rotatePitchHanlder: DragMoveHandler<DragRotateResult, MouseEvent | TouchEvent>;
_startPos: Point;
_lastPos: Point;
constructor(map: Map, element: HTMLElement, pitch: boolean = false) {
this._clickTolerance = 10;
this.element = element;
const moveStateManager = new MouseOrTouchMoveStateManager();
this._rotatePitchHanlder = new DragHandler<DragRotateResult, MouseEvent | TouchEvent>({
clickTolerance: 3,
move: (lastPoint: Point, currentPoint: Point) => {
const rect = element.getBoundingClientRect();
const center = new Point((rect.bottom - rect.top) / 2, (rect.right - rect.left) / 2);
const bearingDelta = getAngleDelta(new Point(lastPoint.x, currentPoint.y), currentPoint, center);
const pitchDelta = pitch ? (currentPoint.y - lastPoint.y) * -0.5 : undefined;
return {bearingDelta, pitchDelta};
},
moveStateManager,
enable: true,
assignEvents: () => {},
});
this.map = map;
DOM.addEventListener(element, 'mousedown', this.mousedown);
DOM.addEventListener(element, 'touchstart', this.touchstart, {passive: false});
DOM.addEventListener(element, 'touchcancel', this.reset);
}
startMove(e: MouseEvent | TouchEvent, point: Point) {
this._rotatePitchHanlder.dragStart(e, point);
DOM.disableDrag();
}
move(e: MouseEvent | TouchEvent, point: Point) {
const map = this.map;
const {bearingDelta, pitchDelta} = this._rotatePitchHanlder.dragMove(e, point) || {};
if (bearingDelta) map.setBearing(map.getBearing() + bearingDelta);
if (pitchDelta) map.setPitch(map.getPitch() + pitchDelta);
}
off() {
const element = this.element;
DOM.removeEventListener(element, 'mousedown', this.mousedown);
DOM.removeEventListener(element, 'touchstart', this.touchstart, {passive: false});
DOM.removeEventListener(window, 'touchmove', this.touchmove, {passive: false});
DOM.removeEventListener(window, 'touchend', this.touchend);
DOM.removeEventListener(element, 'touchcancel', this.reset);
this.offTemp();
}
offTemp() {
DOM.enableDrag();
DOM.removeEventListener(window, 'mousemove', this.mousemove);
DOM.removeEventListener(window, 'mouseup', this.mouseup);
DOM.removeEventListener(window, 'touchmove', this.touchmove, {passive: false});
DOM.removeEventListener(window, 'touchend', this.touchend);
}
mousedown = (e: MouseEvent) => {
this.startMove(e, DOM.mousePos(this.element, e));
DOM.addEventListener(window, 'mousemove', this.mousemove);
DOM.addEventListener(window, 'mouseup', this.mouseup);
};
mousemove = (e: MouseEvent) => {
this.move(e, DOM.mousePos(this.element, e));
};
mouseup = (e: MouseEvent) => {
this._rotatePitchHanlder.dragEnd(e);
this.offTemp();
};
touchstart = (e: TouchEvent) => {
if (e.targetTouches.length !== 1) {
this.reset();
} else {
this._startPos = this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0];
this.startMove(e, this._startPos);
DOM.addEventListener(window, 'touchmove', this.touchmove, {passive: false});
DOM.addEventListener(window, 'touchend', this.touchend);
}
};
touchmove = (e: TouchEvent) => {
if (e.targetTouches.length !== 1) {
this.reset();
} else {
this._lastPos = DOM.touchPos(this.element, e.targetTouches)[0];
this.move(e, this._lastPos);
}
};
touchend = (e: TouchEvent) => {
if (e.targetTouches.length === 0 &&
this._startPos &&
this._lastPos &&
this._startPos.dist(this._lastPos) < this._clickTolerance) {
this.element.click();
}
delete this._startPos;
delete this._lastPos;
this.offTemp();
};
reset = () => {
this._rotatePitchHanlder.reset();
delete this._startPos;
delete this._lastPos;
this.offTemp();
};
}