leaflet
Version:
JavaScript library for mobile-friendly interactive maps
415 lines (323 loc) • 10.5 kB
JavaScript
import {Layer} from '../Layer.js';
import {IconDefault} from './Icon.Default.js';
import * as Util from '../../core/Util.js';
import {LatLng} from '../../geo/LatLng.js';
import {Point} from '../../geometry/Point.js';
import * as DomUtil from '../../dom/DomUtil.js';
import * as DomEvent from '../../dom/DomEvent.js';
import {MarkerDrag} from './Marker.Drag.js';
/*
* @class Marker
* @inherits Interactive layer
* Marker is used to display clickable/draggable icons on the map. Extends `Layer`.
*
* @example
*
* ```js
* new Marker([50.5, 30.5]).addTo(map);
* ```
*/
// @constructor Marker(latlng: LatLng, options? : Marker options)
// Instantiates a Marker object given a geographical point and optionally an options object.
export class Marker extends Layer {
static {
// @section
// @aka Marker options
this.setDefaultOptions({
// @option icon: Icon = *
// Icon instance to use for rendering the marker.
// See [Icon documentation](#Icon) for details on how to customize the marker icon.
// If not specified, a common instance of `Icon.Default` is used.
icon: new IconDefault(),
// Option inherited from "Interactive layer" abstract class
interactive: true,
// @option keyboard: Boolean = true
// Whether the marker can be tabbed to with a keyboard and clicked by pressing enter.
keyboard: true,
// @option title: String = ''
// Text for the browser tooltip that appear on marker hover (no tooltip by default).
// [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled).
title: '',
// @option alt: String = 'Marker'
// Text for the `alt` attribute of the icon image.
// [Useful for accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled).
alt: 'Marker',
// @option zIndexOffset: Number = 0
// By default, marker images zIndex is set automatically based on its latitude. Use this option if you want to put the marker on top of all others (or below), specifying a high value like `1000` (or high negative value, respectively).
zIndexOffset: 0,
// @option opacity: Number = 1.0
// The opacity of the marker.
opacity: 1,
// @option riseOnHover: Boolean = false
// If `true`, the marker will get on top of others when you hover the pointer over it.
riseOnHover: false,
// @option riseOffset: Number = 250
// The z-index offset used for the `riseOnHover` feature.
riseOffset: 250,
// @option pane: String = 'markerPane'
// `Map pane` where the markers icon will be added.
pane: 'markerPane',
// @option shadowPane: String = 'shadowPane'
// `Map pane` where the markers shadow will be added.
shadowPane: 'shadowPane',
// @option bubblingPointerEvents: Boolean = false
// When `true`, a pointer event on this marker will trigger the same event on the map
// (unless [`DomEvent.stopPropagation`](#domevent-stoppropagation) is used).
bubblingPointerEvents: false,
// @option autoPanOnFocus: Boolean = true
// When `true`, the map will pan whenever the marker is focused (via
// e.g. pressing `tab` on the keyboard) to ensure the marker is
// visible within the map's bounds
autoPanOnFocus: true,
// @section Draggable marker options
// @option draggable: Boolean = false
// Whether the marker is draggable with pointer or not.
draggable: false,
// @option autoPan: Boolean = false
// Whether to pan the map when dragging this marker near its edge or not.
autoPan: false,
// @option autoPanPadding: Point = Point(50, 50)
// Distance (in pixels to the left/right and to the top/bottom) of the
// map edge to start panning the map.
autoPanPadding: [50, 50],
// @option autoPanSpeed: Number = 10
// Number of pixels the map should pan by.
autoPanSpeed: 10
});
}
/* @section
*
* In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup methods](#Popup) like bindPopup() you can also use the following methods:
*/
initialize(latlng, options) {
Util.setOptions(this, options);
this._latlng = new LatLng(latlng);
}
onAdd(map) {
this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation;
if (this._zoomAnimated) {
map.on('zoomanim', this._animateZoom, this);
}
this._initIcon();
this.update();
}
onRemove(map) {
if (this.dragging?.enabled()) {
this.options.draggable = true;
this.dragging.removeHooks();
}
delete this.dragging;
if (this._zoomAnimated) {
map.off('zoomanim', this._animateZoom, this);
}
this._removeIcon();
this._removeShadow();
}
getEvents() {
return {
zoom: this.update,
viewreset: this.update
};
}
// @method getLatLng: LatLng
// Returns the current geographical position of the marker.
getLatLng() {
return this._latlng;
}
// @method setLatLng(latlng: LatLng): this
// Changes the marker position to the given point.
setLatLng(latlng) {
const oldLatLng = this._latlng;
this._latlng = new LatLng(latlng);
this.update();
// @event move: Event
// Fired when the marker is moved via [`setLatLng`](#marker-setlatlng) or by [dragging](#marker-dragging). Old and new coordinates are included in event arguments as `oldLatLng`, `latlng`.
return this.fire('move', {oldLatLng, latlng: this._latlng});
}
// @method setZIndexOffset(offset: Number): this
// Changes the [zIndex offset](#marker-zindexoffset) of the marker.
setZIndexOffset(offset) {
this.options.zIndexOffset = offset;
return this.update();
}
// @method getIcon: Icon
// Returns the current icon used by the marker
getIcon() {
return this.options.icon;
}
// @method setIcon(icon: Icon): this
// Changes the marker icon.
setIcon(icon) {
this.options.icon = icon;
if (this._map) {
this._initIcon();
this.update();
}
if (this._popup) {
this.bindPopup(this._popup, this._popup.options);
}
return this;
}
// @method getElement(): HTMLElement
// Returns the instance of [`HTMLElement`](https://developer.mozilla.org/docs/Web/API/HTMLElement)
// used by Marker layer.
getElement() {
return this._icon;
}
update() {
if (this._icon && this._map) {
const pos = this._map.latLngToLayerPoint(this._latlng).round();
this._setPos(pos);
}
return this;
}
_initIcon() {
const options = this.options,
classToAdd = `leaflet-zoom-${this._zoomAnimated ? 'animated' : 'hide'}`;
const icon = options.icon.createIcon(this._icon);
let addIcon = false;
// if we're not reusing the icon, remove the old one and init new one
if (icon !== this._icon) {
if (this._icon) {
this._removeIcon();
}
addIcon = true;
if (options.title) {
icon.title = options.title;
}
if (icon.tagName === 'IMG') {
icon.alt = options.alt ?? '';
}
}
icon.classList.add(classToAdd);
if (options.keyboard) {
icon.tabIndex = '0';
icon.setAttribute('role', 'button');
}
this._icon = icon;
if (options.riseOnHover) {
this.on({
pointerover: this._bringToFront,
pointerout: this._resetZIndex
});
}
if (this.options.autoPanOnFocus) {
DomEvent.on(icon, 'focus', this._panOnFocus, this);
}
const newShadow = options.icon.createShadow(this._shadow);
let addShadow = false;
if (newShadow !== this._shadow) {
this._removeShadow();
addShadow = true;
}
if (newShadow) {
newShadow.classList.add(classToAdd);
newShadow.alt = '';
}
this._shadow = newShadow;
if (options.opacity < 1) {
this._updateOpacity();
}
if (addIcon) {
this.getPane().appendChild(this._icon);
}
this._initInteraction();
if (newShadow && addShadow) {
this.getPane(options.shadowPane).appendChild(this._shadow);
}
}
_removeIcon() {
if (this.options.riseOnHover) {
this.off({
pointerover: this._bringToFront,
pointerout: this._resetZIndex
});
}
if (this.options.autoPanOnFocus) {
DomEvent.off(this._icon, 'focus', this._panOnFocus, this);
}
this._icon.remove();
this.removeInteractiveTarget(this._icon);
this._icon = null;
}
_removeShadow() {
this._shadow?.remove();
this._shadow = null;
}
_setPos(pos) {
if (this._icon) {
DomUtil.setPosition(this._icon, pos);
}
if (this._shadow) {
DomUtil.setPosition(this._shadow, pos);
}
this._zIndex = pos.y + this.options.zIndexOffset;
this._resetZIndex();
}
_updateZIndex(offset) {
if (this._icon) {
this._icon.style.zIndex = this._zIndex + offset;
}
}
_animateZoom(opt) {
const pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round();
this._setPos(pos);
}
_initInteraction() {
if (!this.options.interactive) { return; }
this._icon.classList.add('leaflet-interactive');
this.addInteractiveTarget(this._icon);
if (MarkerDrag) {
let draggable = this.options.draggable;
if (this.dragging) {
draggable = this.dragging.enabled();
this.dragging.disable();
}
this.dragging = new MarkerDrag(this);
if (draggable) {
this.dragging.enable();
}
}
}
// @method setOpacity(opacity: Number): this
// Changes the opacity of the marker.
setOpacity(opacity) {
this.options.opacity = opacity;
if (this._map) {
this._updateOpacity();
}
return this;
}
_updateOpacity() {
const opacity = this.options.opacity;
if (this._icon) {
this._icon.style.opacity = opacity;
}
if (this._shadow) {
this._shadow.style.opacity = opacity;
}
}
_bringToFront() {
this._updateZIndex(this.options.riseOffset);
}
_resetZIndex() {
this._updateZIndex(0);
}
_panOnFocus() {
const map = this._map;
if (!map) { return; }
const iconOpts = this.options.icon.options;
const size = iconOpts.iconSize ? new Point(iconOpts.iconSize) : new Point(0, 0);
const anchor = iconOpts.iconAnchor ? new Point(iconOpts.iconAnchor) : new Point(0, 0);
map.panInside(this._latlng, {
paddingTopLeft: anchor,
paddingBottomRight: size.subtract(anchor)
});
}
_getPopupAnchor() {
return this.options.icon.options.popupAnchor;
}
_getTooltipAnchor() {
return this.options.icon.options.tooltipAnchor;
}
}