UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

166 lines (146 loc) • 5.13 kB
/** * Various utility functions, used by Leaflet internally. * @module Util */ /** Last unique ID used by [`stamp()`](#util-stamp) */ export let lastId = 0; /** * Returns the unique ID of an object, assigning it one if it doesn't have it. * @param {object} obj * @returns {number} */ export function stamp(obj) { if (!('_leaflet_id' in obj)) { /** @type {any} */ (obj)['_leaflet_id'] = ++lastId; } return /** @type {any} */ (obj)._leaflet_id; } /** * Returns a function which executes function `fn` with the given scope `context` (so that the `this` keyword * refers to `context` inside `fn`'s code). The function `fn` will be called no more than one time per given * amount of `time`. The arguments received by the bound function will be any arguments passed when binding * the function, followed by any arguments passed when invoking the bound function. * @template {any[]} A * @param {(...args: A) => void} fn * @param {number} time * @param {object} [context] */ export function throttle(fn, time, context) { /** @type {boolean | undefined} */ let lock; /** @type {A | false | undefined} */ let queuedArgs; function later() { // reset lock and call if queued lock = false; if (queuedArgs) { wrapperFn.apply(context, queuedArgs); queuedArgs = false; } } /** @param {A} args */ function wrapperFn(...args) { if (lock) { // called too soon, queue to call later queuedArgs = args; } else { // call and lock until later fn.apply(context, args); setTimeout(later, time); lock = true; } } return wrapperFn; } /** * Returns the number `num` modulo `range` in such a way so it lies within `range[0]` and `range[1]`. * The returned value will always be smaller than `range[1]` unless `includeMax` is set to `true`. * @param {number} num * @param {number[]} range * @param {boolean} [includeMax] */ export function wrapNum(num, range, includeMax) { const max = range[1], min = range[0], d = max - min; return num === max && includeMax ? num : ((num - min) % d + d) % d + min; } /** * Always returns `false`. Handy as a listener or callback that does nothing. * @returns {false} */ export function falseFn() { return false; } /** * Returns the number `num` rounded with specified `precision`. The default `precision` value is 6 decimal * places. `false` can be passed to skip any processing (can be useful to avoid round-off errors). * @param {number} num * @param {number | false} [precision] */ export function formatNum(num, precision) { if (precision === false) { return num; } const pow = 10 ** (precision === undefined ? 6 : precision); return Math.round(num * pow) / pow; } const WHITESPACE_RE = /\s+/; /** * Trims and splits the string on whitespace and returns the array of parts. * @param {string} str */ export function splitWords(str) { return str.trim().split(WHITESPACE_RE); } /** * Merges the given properties to the `options` of the `obj` object, returning the resulting options. See * [Class options](#class-options). * @template {object} O * @param {{options?: O}} obj * @param {Partial<O>} [options] */ export function setOptions(obj, options) { if (!Object.hasOwn(obj, 'options')) { const defaults = getDefaultOptions(obj.constructor) ?? obj.options; obj.options = defaults ? Object.create(defaults) : /** @type {O} */ ({}); } for (const i in options) { if (Object.hasOwn(options, i)) { /** @type {any} */ (obj.options)[i] = options[i]; } } return /** @type {O} */ (obj.options); } /** @type {WeakMap<Function, object>} */ const chainedDefaults = new WeakMap(); // class -> the defaultOptions object it was chained with // Links the `defaultOptions` of each class in the chain to its parent's, so defaults are inherited and stay live. // Done on first use because a static field can't do it in its initializer and stay tree-shakeable. /** @param {Function & {defaultOptions?: object}} cls */ function getDefaultOptions(cls) { const defaults = cls?.defaultOptions; if (defaults && chainedDefaults.get(cls) !== defaults) { for (let c = cls; c.defaultOptions; c = Object.getPrototypeOf(c)) { if (Object.hasOwn(c, 'defaultOptions')) { Object.setPrototypeOf(c.defaultOptions, Object.getPrototypeOf(c).defaultOptions ?? Object.prototype); } } chainedDefaults.set(cls, defaults); } return defaults; } const templateRe = /\{ *([\w_ -]+) *\}/g; /** * Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'` and a data object * like `{a: 'foo', b: 'bar'}`, returns evaluated string `('Hello foo, bar')`. You can also specify functions * instead of strings for data values — they will be evaluated passing `data` as an argument. * @param {string} str * @param {object} data */ export function template(str, data) { return str.replace(templateRe, (str, key) => { let value = /** @type {Record<string, unknown>} */ (data)[key]; if (value === undefined) { throw new Error(`No value provided for variable ${str}`); } else if (typeof value === 'function') { value = value(data); } return /** @type {string} */ (value); }); }