UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

227 lines (192 loc) • 6.21 kB
import {Class} from '../../core/Class.js'; import {setOptions} from '../../core/Util.js'; import {Point} from '../../geometry/Point.js'; /** @import {Defaults, IconDefaults, IconOptions, PointExpression} from '../../types.js' */ const defaultOptions = { /** * **(required)** The URL to the icon image (absolute or relative to your script path). * @type {string | null} */ iconUrl: null, /** * The URL to a retina sized version of the icon image (absolute or relative to your script path). Used for * Retina screen devices. * @type {string | null} */ iconRetinaUrl: null, /** * Size of the icon image in pixels. A number sets both width and height. * @type {PointExpression | number | null} */ iconSize: null, /** * The coordinates of the "tip" of the icon (relative to its top left corner). The icon will be aligned so that * this point is at the marker's geographical location. Centered by default if size is specified, also can be * set in CSS with negative margins. * @type {PointExpression | null} */ iconAnchor: null, /** * The coordinates of the point from which popups will "open", relative to the icon anchor. * @type {PointExpression} */ popupAnchor: [0, 0], /** * The coordinates of the point from which tooltips will "open", relative to the icon anchor. * @type {PointExpression} */ tooltipAnchor: [0, 0], /** * The URL to the icon shadow image. If not specified, no shadow image will be created. * @type {string | null} */ shadowUrl: null, /** * The URL to a retina sized version of the shadow image. Used for Retina screen devices. * @type {string | null} */ shadowRetinaUrl: null, /** * Size of the shadow image in pixels. A number sets both width and height. * @type {PointExpression | number | null} */ shadowSize: null, /** * The coordinates of the "tip" of the shadow (relative to its top left corner) (the same as iconAnchor if not * specified). * @type {PointExpression | null} */ shadowAnchor: null, /** A custom class name to assign to both icon and shadow images. */ className: '', /** * Whether the crossOrigin attribute will be added to the icon images. If a String is provided, all images will * have their crossOrigin attribute set to the String provided. Refer to [CORS * Settings](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for valid String values. * @type {boolean | string} */ crossOrigin: false }; /** @typedef {typeof defaultOptions} IconDefaultsValues */ /** * Represents an icon to provide when creating a marker. * @example * ```js * const myIcon = new Icon({ * iconUrl: 'my-icon.png', * iconRetinaUrl: 'my-icon@2x.png', * iconSize: [38, 95], * iconAnchor: [22, 94], * popupAnchor: [-3, -76], * shadowUrl: 'my-icon-shadow.png', * shadowRetinaUrl: 'my-icon-shadow@2x.png', * shadowSize: [68, 95], * shadowAnchor: [22, 94] * }); * * new Marker([50.505, 30.57], {icon: myIcon}).addTo(map); * ``` * * `DefaultIcon` extends `Icon` and is the blue icon Leaflet uses for markers by default. * @template {IconOptions} [O=IconOptions] * @extends {Class<O & IconDefaults>} */ export class Icon extends Class { static initHooks = true; /** @type {Defaults<IconOptions>} */ static defaultOptions = defaultOptions; /** * Creates an icon instance with the given options. * @param {NoInfer<Partial<O>>} [options] */ constructor(options) { super(); setOptions(this, options); this.callInitHooks(); } /** * Called internally when the icon has to be shown, returns * a `<img>` HTML element styled according to the options. * @param {HTMLElement | null} [oldIcon] * @returns {HTMLElement} */ createIcon(oldIcon) { return this._createIcon('icon', oldIcon); } /** * As `createIcon`, but for the shadow beneath it. * @param {HTMLElement | null} [oldIcon] */ createShadow(oldIcon) { return this._createIcon('shadow', oldIcon); } /** * @overload * @param {'icon'} name * @param {HTMLElement | null} [oldIcon] * @returns {HTMLImageElement} */ /** * @overload * @param {'icon' | 'shadow'} name * @param {HTMLElement | null} [oldIcon] * @returns {HTMLImageElement | null} */ /** * @param {'icon' | 'shadow'} name * @param {HTMLElement | null} [oldIcon] */ _createIcon(name, oldIcon) { const src = this._getIconUrl(name); if (!src) { if (name === 'icon') { throw new Error('iconUrl not set in Icon options (see the docs).'); } return null; } const img = this._createImg(src, oldIcon instanceof HTMLImageElement ? oldIcon : null); this._setIconStyles(img, name); if (this.options.crossOrigin || this.options.crossOrigin === '') { // the DOM types leave out '', which is how the attribute spells 'anonymous' img.crossOrigin = /** @type {HTMLImageElement['crossOrigin']} */ (this.options.crossOrigin === true ? '' : this.options.crossOrigin); } return img; } /** * @param {HTMLElement} img * @param {'icon' | 'shadow'} name */ _setIconStyles(img, name) { const options = this.options; let sizeOption = options[`${name}Size`]; if (typeof sizeOption === 'number') { sizeOption = [sizeOption, sizeOption]; } const size = Point.validate(sizeOption) && new Point(sizeOption); const anchorPosition = name === 'shadow' && options.shadowAnchor || options.iconAnchor || size && size.divideBy(2); const anchor = Point.validate(anchorPosition) && new Point(anchorPosition); img.className = `leaflet-marker-${name} ${options.className || ''}`; if (anchor) { img.style.marginLeft = `${-anchor.x}px`; img.style.marginTop = `${-anchor.y}px`; img.style.transformOrigin = `${anchor.x}px ${anchor.y}px`; } if (size) { img.style.width = `${size.x}px`; img.style.height = `${size.y}px`; } } /** * @param {string} src * @param {HTMLImageElement | null} el */ _createImg(src, el) { el ??= document.createElement('img'); el.src = src; return el; } /** @param {'icon' | 'shadow'} name */ _getIconUrl(name) { return window.devicePixelRatio > 1 && this.options[`${name}RetinaUrl`] || this.options[`${name}Url`]; } }