UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

574 lines (467 loc) • 15.3 kB
import {Layer} from '../Layer.js'; import {DefaultIcon} from './DefaultIcon.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'; import {pointToGeoJSON} from '../GeoJSONUtil.js'; /** @import {Feature, Point as GeoJSONPoint} from 'geojson' */ /** @import {Defaults, DragEndEvent, LatLngExpression, LeafletEvent, MapListeners, MarkerDefaults, MarkerEventMap, * MarkerMoveEvent, MarkerOptions, PointExpression, ZoomAnimEvent} from '../../types.js' */ /** @import {LeafletMap} from '../../map/Map.js' */ /** @import {Icon} from './Icon.js' */ const defaultOptions = { /** * 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 `DefaultIcon` is used. * @type {Icon} */ icon: /* @__PURE__ */ new DefaultIcon(), // Option inherited from "Interactive layer" abstract class interactive: true, /** Whether the marker can be tabbed to with a keyboard and, if interactive, clicked by pressing Enter or Space. */ keyboard: true, /** * 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: '', /** * Text for the `alt` attribute of the icon image. [Useful for * accessibility](https://leafletjs.com/examples/accessibility/#markers-must-be-labelled). */ alt: 'Marker', /** * 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, /** The opacity of the marker. */ opacity: 1, /** * Rotation of the marker icon in degrees, clockwise, around the icon anchor. A rotated marker, even at 0°, has * no shadow, since the shadow of an upright pin looks wrong under a tilted one. `null` means no rotation. * @type {number | null} */ rotation: null, /** If `true`, the marker will get on top of others when you hover the pointer over it. */ riseOnHover: false, /** The z-index offset used for the `riseOnHover` feature. */ riseOffset: 250, /** `Map pane` where the markers icon will be added. */ pane: 'markerPane', /** `Map pane` where the markers shadow will be added. */ shadowPane: 'shadowPane', /** * 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, /** * 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, /** * Whether the marker is draggable with pointer or not. A focused draggable marker can also be moved with the arrow * keys, by 10 pixels or 30 with Shift. * @category Draggable marker options */ draggable: false, /** * Whether to pan the map when dragging this marker near its edge or not. * @category Draggable marker options */ autoPan: false, /** * Distance (in pixels to the left/right and to the top/bottom) of the map edge to start panning the map. * @category Draggable marker options * @type {PointExpression} */ autoPanPadding: [50, 50], /** * Number of pixels the map should pan by. * @category Draggable marker options */ autoPanSpeed: 10 }; /** @typedef {typeof defaultOptions} MarkerDefaultsValues */ /** * @typedef {Object} MarkerEvents * @property {MarkerMoveEvent} move 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`. */ /** * @category Dragging events * @typedef {Object} MarkerDragEvents * @property {LeafletEvent} dragstart Fired when the user starts dragging the marker. * @property {LeafletEvent} movestart Fired when the marker starts moving (because of dragging). * @property {MarkerMoveEvent} drag Fired repeatedly while the user drags the marker. * @property {DragEndEvent} dragend Fired when the user stops dragging the marker. * @property {LeafletEvent} moveend Fired when the marker stops moving (because of dragging). */ /** * @typedef {Object} MarkerMoveEventProps * @property {LatLng} oldLatLng The previous position of the marker. * @property {LatLng} latlng The new position of the marker. */ /** * Marker is used to display clickable/draggable icons on the map. Extends `Layer`. * @example * ```js * new Marker([50.5, 30.5]).addTo(map); * ``` * @categoryDescription Methods * In addition to [shared layer methods](#Layer) like `addTo()` and `remove()` and [popup * methods](#Popup) like bindPopup() you can also use the following methods: * @categoryDescription Interaction handlers * Interaction handlers are properties of a marker instance that allow you to control interaction behavior in * runtime, enabling or disabling certain features such as dragging (see `Handler` methods). Example: * * ```js * marker.dragging.disable(); * ``` * @template {MarkerOptions} [O=MarkerOptions] * @template {MarkerEventMap} [E=MarkerEventMap] * @extends {Layer<O & MarkerDefaults, E>} */ export class Marker extends Layer { static initHooks = true; /** @type {Defaults<MarkerOptions>} */ static defaultOptions = defaultOptions; /** * Instantiates a Marker object given a geographical point and optionally an options object. * @param {LatLngExpression} latlng * @param {NoInfer<Partial<O>>} [options] */ constructor(latlng, options) { super(); Util.setOptions(this, options); this._latlng = new LatLng(latlng); this.callInitHooks(); } /** @param {LeafletMap} map */ onAdd(map) { this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation; if (this._zoomAnimated) { map.on('zoomanim', this._animateZoom, this); } this._initIcon(); this.update(); } /** @param {LeafletMap} map */ onRemove(map) { if (this.dragging) { // remember whether dragging is on or off (`dragging.disable()`) for when the marker is added again this.options.draggable = this.dragging.enabled(); if (this.options.draggable) { this.dragging.removeHooks(); } } delete this.dragging; if (this._zoomAnimated) { map.off('zoomanim', this._animateZoom, this); } this._removeIcon(); this._removeShadow(); } /** @returns {MapListeners} */ getEvents() { return { zoom: this.update, viewreset: this.update }; } /** Returns the current geographical position of the marker. */ getLatLng() { return this._latlng; } /** * Changes the marker position to the given point. * @param {LatLngExpression} latlng */ setLatLng(latlng) { const oldLatLng = this._latlng; this._latlng = new LatLng(latlng); this.update(); return this.fire('move', {oldLatLng, latlng: this._latlng}); } /** * Changes the [zIndex offset](#marker-zindexoffset) of the marker. * @param {number} offset */ setZIndexOffset(offset) { this.options.zIndexOffset = offset; return this.update(); } /** * Returns the current icon used by the marker * @returns {Icon} */ getIcon() { return this.options.icon; } /** * Changes the marker icon. * @param {Icon} icon */ setIcon(icon) { this.options.icon = icon; if (this._map) { this._initIcon(); this.update(); } return this; } /** * Returns the instance of [`HTMLElement`](https://developer.mozilla.org/docs/Web/API/HTMLElement) * used by Marker layer. */ getElement() { return this._icon; } /** Updates the marker position on the map, e.g. after changing its `zIndexOffset` option directly. */ 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 instanceof HTMLImageElement) { icon.alt = options.alt ?? ''; } } icon.classList.add(classToAdd); if (options.keyboard) { icon.tabIndex = 0; if (options.interactive) { icon.setAttribute('role', 'button'); DomEvent.on(icon, 'keydown', this._onKeyDown, this); } } /** @type {HTMLElement | null | undefined} */ 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) ?? null; let addShadow = false; if (newShadow !== this._shadow) { this._removeShadow(); addShadow = true; } if (newShadow) { newShadow.classList.add(classToAdd); newShadow.alt = ''; } /** @type {HTMLImageElement | null | undefined} */ this._shadow = newShadow; if (options.opacity < 1) { this._updateOpacity(); } if (addIcon) { this.getPane().appendChild(icon); } this._initInteraction(); if (newShadow && addShadow) { this.getPane(options.shadowPane).appendChild(newShadow); } } _removeIcon() { if (this.options.riseOnHover) { this.off({ pointerover: this._bringToFront, pointerout: this._resetZIndex }); } const icon = /** @type {HTMLElement} */ (this._icon); if (this.options.autoPanOnFocus) { DomEvent.off(icon, 'focus', this._panOnFocus, this); } DomEvent.off(icon, 'keydown', this._onKeyDown, this); icon.remove(); this.removeInteractiveTarget(icon); this._icon = null; } _removeShadow() { this._shadow?.remove(); this._shadow = null; } /** @param {Point} pos */ _setPos(pos) { this._positionElements(pos); /** @type {number} */ this._zIndex = pos.y + this.options.zIndexOffset; this._resetZIndex(); } /** @param {Point} pos */ _positionElements(pos) { const rotation = this.options.rotation; if (this._icon) { DomUtil.setPosition(this._icon, pos); if (rotation != null) { this._icon.style.transform += ` rotate(${rotation}deg)`; } } if (this._shadow) { DomUtil.setPosition(this._shadow, pos); this._shadow.style.display = rotation == null ? '' : 'none'; } } /** @param {number} offset */ _updateZIndex(offset) { if (this._icon) { this._icon.style.zIndex = `${this._zIndex + offset}`; } } /** @param {ZoomAnimEvent} opt */ _animateZoom(opt) { const pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center)._round(); this._setPos(pos); } _initInteraction() { if (!this.options.interactive) { return; } const icon = /** @type {HTMLElement} */ (this._icon); icon.classList.add('leaflet-interactive'); this.addInteractiveTarget(icon); if (MarkerDrag) { // When the icon element is reused, the drag handler still refers to it. Keep that handler, because // replacing it ends a drag in progress and forgets that the marker has just been dragged. if (this.dragging?._draggable?._element === icon) { // the icon has reset the class names of the element icon.classList.toggle('leaflet-marker-draggable', this.dragging.enabled()); return; } let draggable = this.options.draggable; if (this.dragging) { draggable = this.dragging.enabled(); this.dragging.disable(); } /** * Marker dragging handler. Only valid when the marker is on the map (Otherwise set * [`marker.options.draggable`](#marker-draggable)). * @category Interaction handlers * @type {MarkerDrag | undefined} */ this.dragging = new MarkerDrag(this); if (draggable) { this.dragging.enable(); } } } /** Returns the current [rotation](#marker-rotation) of the marker in degrees. */ getRotation() { return this.options.rotation; } /** * Changes the [rotation](#marker-rotation) of the marker; `null` removes it. * @param {number | null} rotation */ setRotation(rotation) { this.options.rotation = rotation; // mid-zoom, the icon is transitioning to a position that update() can't compute yet; the zoom end applies it if (this._icon && !this._map._animatingZoom) { this._positionElements(DomUtil.getPosition(this._icon)); this._popup?._updatePosition(); this._tooltip?._updatePosition(); } return this; } /** * Changes the opacity of the marker. * @param {number} opacity */ setOpacity(opacity) { this.options.opacity = opacity; if (this._map) { this._updateOpacity(); } return this; } /** * Coordinates values are rounded with [`formatNum`](#util-formatnum) function * with given `precision`. Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) * representation of the marker (as a GeoJSON `Point` Feature). * @category Other methods * @param {number | false} [precision] * @returns {Feature<GeoJSONPoint>} */ toGeoJSON(precision) { return pointToGeoJSON(this, precision); } _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); } /** @param {KeyboardEvent} e */ _onKeyDown(e) { // a key pressed on a focusable element inside a DivIcon (e.g. a <button>) already clicks it natively if (e.target === this._icon && !e.repeat && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); // stop Space from scrolling the page this._icon?.click(); } } _panOnFocus() { const map = this._map; if (!map) { return; } const icon = /** @type {HTMLElement} */ (this._icon), style = getComputedStyle(icon), anchor = new Point(-parseFloat(style.marginLeft) || 0, -parseFloat(style.marginTop) || 0), size = new Point(icon.offsetWidth, icon.offsetHeight); map.panInside(this._latlng, { paddingTopLeft: anchor, paddingBottomRight: size.subtract(anchor) }); } _getPopupAnchor() { return this._rotateAnchor(this.options.icon.options.popupAnchor); } _getTooltipAnchor() { return this._rotateAnchor(this.options.icon.options.tooltipAnchor); } /** * Anchors are offsets from the icon anchor, which is also the rotation origin. * @param {PointExpression} anchor */ _rotateAnchor(anchor) { const angle = (this.options.rotation ?? 0) * Math.PI / 180; if (!angle || !anchor) { return anchor; } const {x, y} = new Point(anchor), cos = Math.cos(angle), sin = Math.sin(angle); return new Point(x * cos - y * sin, x * sin + y * cos); } }