UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

1,628 lines (1,368 loc) 412 kB
/* @preserve * Leaflet 2.0.0-alpha.1, a JS library for interactive maps. https://leafletjs.com * (c) 2010-2025 Volodymyr Agafonkin, (c) 2010-2011 CloudMade */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.leaflet = {})); })(this, (function (exports) { 'use strict'; /* * @namespace Util * * Various utility functions, used by Leaflet internally. */ // @property lastId: Number // Last unique ID used by [`stamp()`](#util-stamp) let lastId = 0; // @function stamp(obj: Object): Number // Returns the unique ID of an object, assigning it one if it doesn't have it. function stamp(obj) { if (!('_leaflet_id' in obj)) { obj['_leaflet_id'] = ++lastId; } return obj._leaflet_id; } // @function throttle(fn: Function, time: Number, context: Object): Function // 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. function throttle(fn, time, context) { let lock, queuedArgs; function later() { // reset lock and call if queued lock = false; if (queuedArgs) { wrapperFn.apply(context, queuedArgs); queuedArgs = false; } } 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; } // @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number // Returns the number `num` modulo `range` in such a way so it lies within // `range[0]` and `range[1]`. The returned value will be always smaller than // `range[1]` unless `includeMax` is set to `true`. function wrapNum(x, range, includeMax) { const max = range[1], min = range[0], d = max - min; return x === max && includeMax ? x : ((x - min) % d + d) % d + min; } // @function falseFn(): Function // Returns a function which always returns `false`. function falseFn() { return false; } // @function formatNum(num: Number, precision?: Number|false): Number // 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). function formatNum(num, precision) { if (precision === false) { return num; } const pow = 10 ** (precision === undefined ? 6 : precision); return Math.round(num * pow) / pow; } // @function splitWords(str: String): String[] // Trims and splits the string on whitespace and returns the array of parts. function splitWords(str) { return str.trim().split(/\s+/); } // @function setOptions(obj: Object, options: Object): Object // Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. function setOptions(obj, options) { if (!Object.hasOwn(obj, 'options')) { obj.options = obj.options ? Object.create(obj.options) : {}; } for (const i in options) { if (Object.hasOwn(options, i)) { obj.options[i] = options[i]; } } return obj.options; } const templateRe = /\{ *([\w_ -]+) *\}/g; // @function template(str: String, data: Object): String // 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. function template(str, data) { return str.replace(templateRe, (str, key) => { let value = data[key]; if (value === undefined) { throw new Error(`No value provided for variable ${str}`); } else if (typeof value === 'function') { value = value(data); } return value; }); } // @property emptyImageUrl: String // Data URI string containing a base64-encoded empty GIF image. // Used as a hack to free memory from unused images on WebKit-powered // mobile devices (by setting image `src` to this string). const emptyImageUrl = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; var Util = { __proto__: null, emptyImageUrl: emptyImageUrl, falseFn: falseFn, formatNum: formatNum, get lastId () { return lastId; }, setOptions: setOptions, splitWords: splitWords, stamp: stamp, template: template, throttle: throttle, wrapNum: wrapNum }; // @class Class // @section // @uninheritable // Thanks to John Resig and Dean Edwards for inspiration! class Class { // @function extend(props: Object): Function // [Extends the current class](#class-inheritance) given the properties to be included. // Deprecated - use `class X extends Class` instead! // Returns a Javascript function that is a class constructor (to be called with `new`). static extend({statics, includes, ...props}) { const NewClass = class extends this {}; // inherit parent's static properties Object.setPrototypeOf(NewClass, this); const parentProto = this.prototype; const proto = NewClass.prototype; // mix static properties into the class if (statics) { Object.assign(NewClass, statics); } // mix includes into the prototype if (Array.isArray(includes)) { for (const include of includes) { Object.assign(proto, include); } } else if (includes) { Object.assign(proto, includes); } // mix given properties into the prototype Object.assign(proto, props); // merge options if (proto.options) { proto.options = parentProto.options ? Object.create(parentProto.options) : {}; Object.assign(proto.options, props.options); } proto._initHooks = []; return NewClass; } // @function include(properties: Object): this // [Includes a mixin](#class-includes) into the current class. static include(props) { const parentOptions = this.prototype.options; Object.assign(this.prototype, props); if (props.options) { this.prototype.options = parentOptions; this.mergeOptions(props.options); } return this; } // @function setDefaultOptions(options: Object): this // Configures the [default `options`](#class-options) on the prototype of this class. static setDefaultOptions(options) { setOptions(this.prototype, options); return this; } // @function mergeOptions(options: Object): this // [Merges `options`](#class-options) into the defaults of the class. static mergeOptions(options) { this.prototype.options ??= {}; Object.assign(this.prototype.options, options); return this; } // @function addInitHook(fn: Function): this // Adds a [constructor hook](#class-constructor-hooks) to the class. static addInitHook(fn, ...args) { // (Function) || (String, args...) const init = typeof fn === 'function' ? fn : function () { this[fn].apply(this, args); }; this.prototype._initHooks ??= []; this.prototype._initHooks.push(init); return this; } constructor(...args) { this._initHooksCalled = false; setOptions(this); // call the constructor if (this.initialize) { this.initialize(...args); } // call all constructor hooks this.callInitHooks(); } initialize(/* ...args */) { // Override this method in subclasses to implement custom initialization logic. // This method is called automatically when a new instance of the class is created. } callInitHooks() { if (this._initHooksCalled) { return; } // collect all prototypes in chain const prototypes = []; let current = this; while ((current = Object.getPrototypeOf(current)) !== null) { prototypes.push(current); } // reverse so the parent prototype is first prototypes.reverse(); // call init hooks on each prototype for (const proto of prototypes) { for (const hook of proto._initHooks ?? []) { hook.call(this); } } this._initHooksCalled = true; } } /* * @class Evented * @inherits Class * * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event). * * @example * * ```js * map.on('click', function(e) { * alert(e.latlng); * } ); * ``` * * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function: * * ```js * function onClick(e) { ... } * * map.on('click', onClick); * map.off('click', onClick); * ``` */ class Evented extends Class { /* @method on(type: String, fn: Function, context?: Object): this * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`). * * @alternative * @method on(eventMap: Object): this * Adds a set of type/listener pairs, e.g. `{click: onClick, pointermove: onPointerMove}` */ on(types, fn, context) { // types can be a map of types/handlers if (typeof types === 'object') { for (const [type, f] of Object.entries(types)) { // we don't process space-separated events here for performance; // it's a hot path since Layer uses the on(obj) syntax this._on(type, f, fn); } } else { // types can be a string of space-separated words for (const type of splitWords(types)) { this._on(type, fn, context); } } return this; } /* @method off(type: String, fn?: Function, context?: Object): this * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener. * * @alternative * @method off(eventMap: Object): this * Removes a set of type/listener pairs. * * @alternative * @method off: this * Removes all listeners to all events on the object. This includes implicitly attached events. */ off(types, fn, context) { if (!arguments.length) { // clear all listeners if called without arguments delete this._events; } else if (typeof types === 'object') { for (const [type, f] of Object.entries(types)) { this._off(type, f, fn); } } else { const removeAll = arguments.length === 1; for (const type of splitWords(types)) { if (removeAll) { this._off(type); } else { this._off(type, fn, context); } } } return this; } // attach listener (without syntactic sugar now) _on(type, fn, context, _once) { if (typeof fn !== 'function') { console.warn(`wrong listener type: ${typeof fn}`); return; } // check if fn already there if (this._listens(type, fn, context) !== false) { return; } if (context === this) { // Less memory footprint. context = undefined; } const newListener = {fn, ctx: context}; if (_once) { newListener.once = true; } this._events ??= {}; this._events[type] ??= []; this._events[type].push(newListener); } _off(type, fn, context) { if (!this._events) { return; } let listeners = this._events[type]; if (!listeners) { return; } if (arguments.length === 1) { // remove all if (this._firingCount) { // Set all removed listeners to noop // so they are not called if remove happens in fire for (const listener of listeners) { listener.fn = falseFn; } } // clear all listeners for a type if function isn't specified delete this._events[type]; return; } if (typeof fn !== 'function') { console.warn(`wrong listener type: ${typeof fn}`); return; } // find fn and remove it const index = this._listens(type, fn, context); if (index !== false) { const listener = listeners[index]; if (this._firingCount) { // set the removed listener to noop so that's not called if remove happens in fire listener.fn = falseFn; /* copy array in case events are being fired */ this._events[type] = listeners = listeners.slice(); } listeners.splice(index, 1); } } // @method fire(type: String, data?: Object, propagate?: Boolean): this // Fires an event of the specified type. You can optionally provide a data // object — the first argument of the listener function will contain its // properties. The event can optionally be propagated to event parents. fire(type, data, propagate) { if (!this.listens(type, propagate)) { return this; } const event = { ...data, type, target: this, sourceTarget: data?.sourceTarget || this }; if (this._events) { const listeners = this._events[type]; if (listeners) { this._firingCount = (this._firingCount + 1) || 1; for (const l of listeners) { // off overwrites l.fn, so we need to copy fn to a variable const fn = l.fn; if (l.once) { this.off(type, fn, l.ctx); } fn.call(l.ctx || this, event); } this._firingCount--; } } if (propagate) { // propagate the event to parents (set with addEventParent) this._propagateEvent(event); } return this; } // @method listens(type: String, propagate?: Boolean): Boolean // @method listens(type: String, fn: Function, context?: Object, propagate?: Boolean): Boolean // Returns `true` if a particular event type has any listeners attached to it. // The verification can optionally be propagated, it will return `true` if parents have the listener attached to it. listens(type, fn, context, propagate) { if (typeof type !== 'string') { console.warn('"string" type argument expected'); } // we don't overwrite the input `fn` value, because we need to use it for propagation let _fn = fn; if (typeof fn !== 'function') { propagate = !!fn; _fn = undefined; context = undefined; } if (this._events?.[type]?.length) { if (this._listens(type, _fn, context) !== false) { return true; } } if (propagate) { // also check parents for listeners if event propagates for (const p of Object.values(this._eventParents ?? {})) { if (p.listens(type, fn, context, propagate)) { return true; } } } return false; } // returns the index (number) or false _listens(type, fn, context) { if (!this._events) { return false; } const listeners = this._events[type] ?? []; if (!fn) { return !!listeners.length; } if (context === this) { // Less memory footprint. context = undefined; } const index = listeners.findIndex(l => l.fn === fn && l.ctx === context); return index === -1 ? false : index; } // @method once(…): this // Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed. once(types, fn, context) { // types can be a map of types/handlers if (typeof types === 'object') { for (const [type, f] of Object.entries(types)) { // we don't process space-separated events here for performance; // it's a hot path since Layer uses the on(obj) syntax this._on(type, f, fn, true); } } else { // types can be a string of space-separated words for (const type of splitWords(types)) { this._on(type, fn, context, true); } } return this; } // @method addEventParent(obj: Evented): this // Adds an event parent - an `Evented` that will receive propagated events addEventParent(obj) { this._eventParents ??= {}; this._eventParents[stamp(obj)] = obj; return this; } // @method removeEventParent(obj: Evented): this // Removes an event parent, so it will stop receiving propagated events removeEventParent(obj) { if (this._eventParents) { delete this._eventParents[stamp(obj)]; } return this; } _propagateEvent(e) { for (const p of Object.values(this._eventParents ?? {})) { p.fire(e.type, { propagatedFrom: e.target, ...e }, true); } } } /* * @class Point * * Represents a point with `x` and `y` coordinates in pixels. * * @example * * ```js * const point = new Point(200, 300); * ``` * * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent: * * ```js * map.panBy([200, 300]); * map.panBy(new Point(200, 300)); * ``` * * Note that `Point` does not inherit from Leaflet's `Class` object, * which means new classes can't inherit from it, and new methods * can't be added to it with the `include` function. */ // @constructor Point(x: Number, y: Number, round?: Boolean) // Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values. // @alternative // @constructor Point(coords: Number[]) // Expects an array of the form `[x, y]` instead. // @alternative // @constructor Point(coords: Object) // Expects a plain object of the form `{x: Number, y: Number}` instead. class Point { constructor(x, y, round) { const valid = Point.validate(x, y); if (!valid) { throw new Error(`Invalid Point object: (${x}, ${y})`); } let _x, _y; if (x instanceof Point) { // We can use the same object, no need to clone it // eslint-disable-next-line no-constructor-return return x; } else if (Array.isArray(x)) { _x = x[0]; _y = x[1]; } else if (typeof x === 'object' && 'x' in x && 'y' in x) { _x = x.x; _y = x.y; } else { _x = x; _y = y; } // @property x: Number; The `x` coordinate of the point this.x = (round ? Math.round(_x) : _x); // @property y: Number; The `y` coordinate of the point this.y = (round ? Math.round(_y) : _y); } // @section // There are several static functions which can be called without instantiating Point: // @function validate(x: Number, y: Number): Boolean // Returns `true` if the Point object can be properly initialized. // @alternative // @function validate(coords: Number[]): Boolean // Expects an array of the form `[x, y]`. Returns `true` if the Point object can be properly initialized. // @alternative // @function validate(coords: Object): Boolean // Returns `true` if the Point object can be properly initialized. static validate(x, y) { if (x instanceof Point || Array.isArray(x)) { return true; } else if (x && typeof x === 'object' && 'x' in x && 'y' in x) { return true; } else if ((x || x === 0) && (y || y === 0)) { return true; } return false; } // @method clone(): Point // Returns a copy of the current point. clone() { // to skip the validation in the constructor we need to initialize with 0 and then set the values later const p = new Point(0, 0); p.x = this.x; p.y = this.y; return p; } // @method add(otherPoint: Point): Point // Returns the result of addition of the current and the given points. add(point) { // non-destructive, returns a new point return this.clone()._add(new Point(point)); } _add(point) { // destructive, used directly for performance in situations where it's safe to modify existing point this.x += point.x; this.y += point.y; return this; } // @method subtract(otherPoint: Point): Point // Returns the result of subtraction of the given point from the current. subtract(point) { return this.clone()._subtract(new Point(point)); } _subtract(point) { this.x -= point.x; this.y -= point.y; return this; } // @method divideBy(num: Number): Point // Returns the result of division of the current point by the given number. divideBy(num) { return this.clone()._divideBy(num); } _divideBy(num) { this.x /= num; this.y /= num; return this; } // @method multiplyBy(num: Number): Point // Returns the result of multiplication of the current point by the given number. multiplyBy(num) { return this.clone()._multiplyBy(num); } _multiplyBy(num) { this.x *= num; this.y *= num; return this; } // @method scaleBy(scale: Point): Point // Multiply each coordinate of the current point by each coordinate of // `scale`. In linear algebra terms, multiply the point by the // [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation) // defined by `scale`. scaleBy(point) { return new Point(this.x * point.x, this.y * point.y); } // @method unscaleBy(scale: Point): Point // Inverse of `scaleBy`. Divide each coordinate of the current point by // each coordinate of `scale`. unscaleBy(point) { return new Point(this.x / point.x, this.y / point.y); } // Returns a copy of the current point with rounded coordinates. round() { return this.clone()._round(); } _round() { this.x = Math.round(this.x); this.y = Math.round(this.y); return this; } // @method floor(): Point // Returns a copy of the current point with floored coordinates (rounded down). floor() { return this.clone()._floor(); } _floor() { this.x = Math.floor(this.x); this.y = Math.floor(this.y); return this; } // @method ceil(): Point // Returns a copy of the current point with ceiled coordinates (rounded up). ceil() { return this.clone()._ceil(); } _ceil() { this.x = Math.ceil(this.x); this.y = Math.ceil(this.y); return this; } // Returns a copy of the current point with truncated coordinates (rounded towards zero). trunc() { return this.clone()._trunc(); } _trunc() { this.x = Math.trunc(this.x); this.y = Math.trunc(this.y); return this; } // @method distanceTo(otherPoint: Point): Number // Returns the cartesian distance between the current and the given points. distanceTo(point) { point = new Point(point); const x = point.x - this.x, y = point.y - this.y; return Math.sqrt(x * x + y * y); } // @method equals(otherPoint: Point): Boolean // Returns `true` if the given point has the same coordinates. equals(point) { point = new Point(point); return point.x === this.x && point.y === this.y; } // @method contains(otherPoint: Point): Boolean // Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values). contains(point) { point = new Point(point); return Math.abs(point.x) <= Math.abs(this.x) && Math.abs(point.y) <= Math.abs(this.y); } // @method toString(): String // Returns a string representation of the point for debugging purposes. toString() { return `Point(${formatNum(this.x)}, ${formatNum(this.y)})`; } } /* * @class Bounds * * Represents a rectangular area in pixel coordinates. * * @example * * ```js * const p1 = new Point(10, 10), * p2 = new Point(40, 60), * bounds = new Bounds(p1, p2); * ``` * * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: * * ```js * otherBounds.intersects([[10, 10], [40, 60]]); * ``` * * Note that `Bounds` does not inherit from Leaflet's `Class` object, * which means new classes can't inherit from it, and new methods * can't be added to it with the `include` function. */ // @constructor Bounds(corner1: Point, corner2: Point) // Creates a Bounds object from two corners coordinate pairs. // @alternative // @constructor Bounds(points: Point[]) // Creates a Bounds object from the given array of points. class Bounds { constructor(a, b) { if (!a) { return; } if (a instanceof Bounds) { // We can use the same object, no need to clone it // eslint-disable-next-line no-constructor-return return a; } const points = b ? [a, b] : a; for (const point of points) { this.extend(point); } } // @method extend(point: Point): this // Extends the bounds to contain the given point. // @alternative // @method extend(otherBounds: Bounds): this // Extend the bounds to contain the given bounds extend(obj) { let min2, max2; if (!obj) { return this; } if (obj instanceof Point || typeof obj[0] === 'number' || 'x' in obj) { min2 = max2 = new Point(obj); } else { obj = new Bounds(obj); min2 = obj.min; max2 = obj.max; if (!min2 || !max2) { return this; } } // @property min: Point // The top left corner of the rectangle. // @property max: Point // The bottom right corner of the rectangle. if (!this.min && !this.max) { this.min = min2.clone(); this.max = max2.clone(); } else { this.min.x = Math.min(min2.x, this.min.x); this.max.x = Math.max(max2.x, this.max.x); this.min.y = Math.min(min2.y, this.min.y); this.max.y = Math.max(max2.y, this.max.y); } return this; } // @method getCenter(round?: Boolean): Point // Returns the center point of the bounds. getCenter(round) { return new Point( (this.min.x + this.max.x) / 2, (this.min.y + this.max.y) / 2, round); } // @method getBottomLeft(): Point // Returns the bottom-left point of the bounds. getBottomLeft() { return new Point(this.min.x, this.max.y); } // @method getTopRight(): Point // Returns the top-right point of the bounds. getTopRight() { // -> Point return new Point(this.max.x, this.min.y); } // @method getTopLeft(): Point // Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)). getTopLeft() { return this.min; // left, top } // @method getBottomRight(): Point // Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)). getBottomRight() { return this.max; // right, bottom } // @method getSize(): Point // Returns the size of the given bounds getSize() { return this.max.subtract(this.min); } // @method contains(otherBounds: Bounds): Boolean // Returns `true` if the rectangle contains the given one. // @alternative // @method contains(point: Point): Boolean // Returns `true` if the rectangle contains the given point. contains(obj) { let min, max; if (typeof obj[0] === 'number' || obj instanceof Point) { obj = new Point(obj); } else { obj = new Bounds(obj); } if (obj instanceof Bounds) { min = obj.min; max = obj.max; } else { min = max = obj; } return (min.x >= this.min.x) && (max.x <= this.max.x) && (min.y >= this.min.y) && (max.y <= this.max.y); } // @method intersects(otherBounds: Bounds): Boolean // Returns `true` if the rectangle intersects the given bounds. Two bounds // intersect if they have at least one point in common. intersects(bounds) { // (Bounds) -> Boolean bounds = new Bounds(bounds); const min = this.min, max = this.max, min2 = bounds.min, max2 = bounds.max, xIntersects = (max2.x >= min.x) && (min2.x <= max.x), yIntersects = (max2.y >= min.y) && (min2.y <= max.y); return xIntersects && yIntersects; } // @method overlaps(otherBounds: Bounds): Boolean // Returns `true` if the rectangle overlaps the given bounds. Two bounds // overlap if their intersection is an area. overlaps(bounds) { // (Bounds) -> Boolean bounds = new Bounds(bounds); const min = this.min, max = this.max, min2 = bounds.min, max2 = bounds.max, xOverlaps = (max2.x > min.x) && (min2.x < max.x), yOverlaps = (max2.y > min.y) && (min2.y < max.y); return xOverlaps && yOverlaps; } // @method isValid(): Boolean // Returns `true` if the bounds are properly initialized. isValid() { return !!(this.min && this.max); } // @method pad(bufferRatio: Number): Bounds // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction. // For example, a ratio of 0.5 extends the bounds by 50% in each direction. // Negative values will retract the bounds. pad(bufferRatio) { const min = this.min, max = this.max, heightBuffer = Math.abs(min.x - max.x) * bufferRatio, widthBuffer = Math.abs(min.y - max.y) * bufferRatio; return new Bounds( new Point(min.x - heightBuffer, min.y - widthBuffer), new Point(max.x + heightBuffer, max.y + widthBuffer)); } // @method equals(otherBounds: Bounds): Boolean // Returns `true` if the rectangle is equivalent to the given bounds. equals(bounds) { if (!bounds) { return false; } bounds = new Bounds(bounds); return this.min.equals(bounds.getTopLeft()) && this.max.equals(bounds.getBottomRight()); } } /* * @class LatLngBounds * * Represents a rectangular geographical area on a map. * * @example * * ```js * const corner1 = new LatLng(40.712, -74.227), * corner2 = new LatLng(40.774, -74.125), * bounds = new LatLngBounds(corner1, corner2); * ``` * * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this: * * ```js * map.fitBounds([ * [40.712, -74.227], * [40.774, -74.125] * ]); * ``` * * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range. * * Note that `LatLngBounds` does not inherit from Leaflet's `Class` object, * which means new classes can't inherit from it, and new methods * can't be added to it with the `include` function. */ // TODO International date line? // @constructor LatLngBounds(corner1: LatLng, corner2: LatLng) // Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle. // @alternative // @constructor LatLngBounds(latlngs: LatLng[]) // Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds). class LatLngBounds { constructor(corner1, corner2) { // (LatLng, LatLng) or (LatLng[]) if (!corner1) { return; } if (corner1 instanceof LatLngBounds) { // We can use the same object, no need to clone it // eslint-disable-next-line no-constructor-return return corner1; } const latlngs = corner2 ? [corner1, corner2] : corner1; for (const latlng of latlngs) { this.extend(latlng); } } // @method extend(latlng: LatLng): this // Extend the bounds to contain the given point // @alternative // @method extend(otherBounds: LatLngBounds): this // Extend the bounds to contain the given bounds extend(obj) { const sw = this._southWest, ne = this._northEast; let sw2, ne2; if (obj instanceof LatLng) { sw2 = obj; ne2 = obj; } else if (obj instanceof LatLngBounds) { sw2 = obj._southWest; ne2 = obj._northEast; if (!sw2 || !ne2) { return this; } } else { if (!obj) { return this; } if (LatLng.validate(obj)) { return this.extend(new LatLng(obj)); } return this.extend(new LatLngBounds(obj)); } if (!sw && !ne) { this._southWest = new LatLng(sw2.lat, sw2.lng); this._northEast = new LatLng(ne2.lat, ne2.lng); } else { sw.lat = Math.min(sw2.lat, sw.lat); sw.lng = Math.min(sw2.lng, sw.lng); ne.lat = Math.max(ne2.lat, ne.lat); ne.lng = Math.max(ne2.lng, ne.lng); } return this; } // @method pad(bufferRatio: Number): LatLngBounds // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction. // For example, a ratio of 0.5 extends the bounds by 50% in each direction. // Negative values will retract the bounds. pad(bufferRatio) { const sw = this._southWest, ne = this._northEast, heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio, widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio; return new LatLngBounds( new LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer), new LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer)); } // @method getCenter(): LatLng // Returns the center point of the bounds. getCenter() { return new LatLng( (this._southWest.lat + this._northEast.lat) / 2, (this._southWest.lng + this._northEast.lng) / 2); } // @method getSouthWest(): LatLng // Returns the south-west point of the bounds. getSouthWest() { return this._southWest; } // @method getNorthEast(): LatLng // Returns the north-east point of the bounds. getNorthEast() { return this._northEast; } // @method getNorthWest(): LatLng // Returns the north-west point of the bounds. getNorthWest() { return new LatLng(this.getNorth(), this.getWest()); } // @method getSouthEast(): LatLng // Returns the south-east point of the bounds. getSouthEast() { return new LatLng(this.getSouth(), this.getEast()); } // @method getWest(): Number // Returns the west longitude of the bounds getWest() { return this._southWest.lng; } // @method getSouth(): Number // Returns the south latitude of the bounds getSouth() { return this._southWest.lat; } // @method getEast(): Number // Returns the east longitude of the bounds getEast() { return this._northEast.lng; } // @method getNorth(): Number // Returns the north latitude of the bounds getNorth() { return this._northEast.lat; } // @method contains(otherBounds: LatLngBounds): Boolean // Returns `true` if the rectangle contains the given one. // @alternative // @method contains (latlng: LatLng): Boolean // Returns `true` if the rectangle contains the given point. contains(obj) { // (LatLngBounds) or (LatLng) -> Boolean if (LatLng.validate(obj)) { obj = new LatLng(obj); } else { obj = new LatLngBounds(obj); } const sw = this._southWest, ne = this._northEast; let sw2, ne2; if (obj instanceof LatLngBounds) { sw2 = obj.getSouthWest(); ne2 = obj.getNorthEast(); } else { sw2 = ne2 = obj; } return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) && (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng); } // @method intersects(otherBounds: LatLngBounds): Boolean // Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common. intersects(bounds) { bounds = new LatLngBounds(bounds); const sw = this._southWest, ne = this._northEast, sw2 = bounds.getSouthWest(), ne2 = bounds.getNorthEast(), latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat), lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng); return latIntersects && lngIntersects; } // @method overlaps(otherBounds: LatLngBounds): Boolean // Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area. overlaps(bounds) { bounds = new LatLngBounds(bounds); const sw = this._southWest, ne = this._northEast, sw2 = bounds.getSouthWest(), ne2 = bounds.getNorthEast(), latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat), lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng); return latOverlaps && lngOverlaps; } // @method toBBoxString(): String // Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data. toBBoxString() { return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(','); } // @method equals(otherBounds: LatLngBounds, maxMargin?: Number): Boolean // Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. The margin of error can be overridden by setting `maxMargin` to a small number. equals(bounds, maxMargin) { if (!bounds) { return false; } bounds = new LatLngBounds(bounds); return this._southWest.equals(bounds.getSouthWest(), maxMargin) && this._northEast.equals(bounds.getNorthEast(), maxMargin); } // @method isValid(): Boolean // Returns `true` if the bounds are properly initialized. isValid() { return !!(this._southWest && this._northEast); } } /* @class LatLng * * Represents a geographical point with a certain latitude and longitude. * * @example * * ``` * const latlng = new LatLng(50.5, 30.5); * ``` * * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent: * * ``` * map.panTo([50, 30]); * map.panTo({lat: 50, lng: 30}); * map.panTo({lat: 50, lon: 30}); * map.panTo(new LatLng(50, 30)); * ``` * * Note that `LatLng` does not inherit from Leaflet's `Class` object, * which means new classes can't inherit from it, and new methods * can't be added to it with the `include` function. */ // @constructor LatLng(latitude: Number, longitude: Number, altitude?: Number): LatLng // Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude). // @alternative // @constructor LatLng(coords: Array): LatLng // Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead. // @alternative // @constructor LatLng(coords: Object): LatLng // Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead. // You can also use `lon` in place of `lng` in the object form. class LatLng { constructor(lat, lng, alt) { const valid = LatLng.validate(lat, lng, alt); if (!valid) { throw new Error(`Invalid LatLng object: (${lat}, ${lng})`); } let _lat, _lng, _alt; if (lat instanceof LatLng) { // We can use the same object, no need to clone it // eslint-disable-next-line no-constructor-return return lat; } else if (Array.isArray(lat) && typeof lat[0] !== 'object') { if (lat.length === 3) { _lat = lat[0]; _lng = lat[1]; _alt = lat[2]; } else if (lat.length === 2) { _lat = lat[0]; _lng = lat[1]; } } else if (typeof lat === 'object' && 'lat' in lat) { _lat = lat.lat; _lng = 'lng' in lat ? lat.lng : lat.lon; _alt = lat.alt; } else { _lat = lat; _lng = lng; _alt = alt; } // @property lat: Number // Latitude in degrees this.lat = +_lat; // @property lng: Number // Longitude in degrees this.lng = +_lng; // @property alt: Number // Altitude in meters (optional) if (_alt !== undefined) { this.alt = +_alt; } } // @section // There are several static functions which can be called without instantiating LatLng: // @function validate(latitude: Number, longitude: Number, altitude?: Number): Boolean // Returns `true` if the LatLng object can be properly initialized. // @alternative // @function validate(coords: Array): Boolean // Expects an array of the form `[Number, Number]` or `[Number, Number, Number]`. // Returns `true` if the LatLng object can be properly initialized. // @alternative // @function validate(coords: Object): Boolean // Returns `true` if the LatLng object can be properly initialized. // eslint-disable-next-line no-unused-vars static validate(lat, lng, alt) { if (lat instanceof LatLng || (typeof lat === 'object' && 'lat' in lat)) { return true; } else if (lat && Array.isArray(lat) && typeof lat[0] !== 'object') { if (lat.length === 3 || lat.length === 2) { return true; } return false; } else if ((lat || lat === 0) && (lng || lng === 0)) { return true; } return false; } // @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean // Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overridden by setting `maxMargin` to a small number. equals(obj, maxMargin) { if (!obj) { return false; } obj = new LatLng(obj); const margin = Math.max( Math.abs(this.lat - obj.lat), Math.abs(this.lng - obj.lng)); return margin <= (maxMargin ?? 1.0E-9); } // @method toString(precision?: Number): String // Returns a string representation of the point (for debugging purposes). toString(precision) { return `LatLng(${formatNum(this.lat, precision)}, ${formatNum(this.lng, precision)})`; } // @method distanceTo(otherLatLng: LatLng): Number // Returns the distance (in meters) to the given `LatLng` calculated using the [Haversine formula](https://en.wikipedia.org/wiki/Haversine_formula). distanceTo(other) { return Earth.distance(this, new LatLng(other)); } // @method wrap(): LatLng // Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees. wrap() { return Earth.wrapLatLng(this); } // @method toBounds(sizeInMeters: Number): LatLngBounds // Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`. toBounds(sizeInMeters) { const latAccuracy = 180 * sizeInMeters / 40075017, lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat); return new LatLngBounds( [this.lat - latAccuracy, this.lng - lngAccuracy], [this.lat + latAccuracy, this.lng + lngAccuracy]); } // @method clone(): LatLng // Returns a copy of the current LatLng. clone() { // to skip the validation in the constructor we need to initialize with 0 and then set the values later const latlng = new LatLng(0, 0); latlng.lat = this.lat; latlng.lng = this.lng; latlng.alt = this.alt; return latlng; } } /* * @namespace CRS * @crs CRS.Base * Object that defines coordinate reference systems for projecting * geographical points into pixel (screen) coordinates and back (and to * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See * [spatial reference system](https://en.wikipedia.org/wiki/Spatial_reference_system). * * Leaflet defines the most usual CRSs by default. If you want to use a * CRS not defined by default, take a look at the * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin. * * Note that the CRS instances do not inherit from Leaflet's `Class` object, * and can't be instantiated. Also, new classes can't inherit from them, * and methods can't be added to them with the `include` function. */ class CRS { static projection = undefined; static transformation = undefined; // @method latLngToPoint(latlng: LatLng, zoom: Number): Point // Projects geographical coordinates into pixel coordinates for a given zoom. static latLngToPoint(latlng, zoom) { const projectedPoint = this.projection.project(latlng), scale = this.scale(zoom); return this.transformation._transform(projectedPoint, scale); } // @method pointToLatLng(point: Point, zoom: Number): LatLng // The inverse of `latLngToPoint`. Projects pixel coordinates on a given // zoom into geographical coordinates. static pointToLatLng(point, zoom) { const scale = this.scale(zoom), untransformedPoint = this.transformation.untransform(point, scale); return this.projection.unproject(untransformedPoint); } // @method project(latlng: LatLng): Point // Projects geographical coordinates into coordinates in units accepted for // this CRS (e.g. meters for EPSG:3857, for passing it to WMS services). static project(latlng) { return this.projection.project(latlng); } // @method unproject(point: Point): LatLng // Given a projected coordinate returns the corresponding LatLng. // The inverse of `project`. static unproject(point) { return this.projection.unproject(point); } // @method scale(zoom: Number): Number // Returns the scale used when transforming projected coordinates into // pixel coordinates for a particular zoom. For example, it returns // `256 * 2^zoom` for Mercator-based CRS. static scale(zoom) { return 256 * 2 ** zoom; } // @method zoom(scale: Number): Number // Inverse of `scale()`, returns the zoom level corresponding to a scale // factor of `scale`. static zoom(scale) { return Math.log(scale / 256) / Math.LN2; } // @method getProjectedBounds(zoom: Number): Bounds // Returns the projection's bounds scaled and transformed for the provided `zoom`. static getProjectedBounds(zoom) { if (this.infinite) { return null; } const b = this.projection.bounds, s = this.scale(zoom), min = this.transformation.transform(b.min, s), max = this.transformation.transform(b.max, s); return new Bounds(min, max); } // @method distance(latlng1: LatLng, latlng2: LatLng): Number // Returns the distance between two geographical coordinates. // @property code: String // Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`) // // @property wrapLng: Number[] // An array of two numbers defining whether the longitude (horizontal) coordinate // axis wraps around a given range and how. Defaults to `[-180, 180]` in most // geographical CRSs. If `undefined`, the longitude axis does not wrap around. // // @property wrapLat: Number[] // Like `wrapLng`, but for the latitude (vertical) axis. // wrapLng: [min, max], // wrapLat: [min, max], // @property infinite: Boolean // If true, the coordinate space will be unbounded (infinite in both axes) static infinite = false; // @method wrapLatLng(latlng: LatLng): LatLng // Returns a `LatLng` where lat and lng has been wrapped according to the // CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds. static wrapLatLng(latlng) { latlng = new LatLng(latlng); const lng = this.wrapLng ? wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng, lat = this.wrapLat ? wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat, alt = latlng.alt; return new LatLng(lat, lng, alt); } // @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds // Returns a `LatLngBounds` with the same size as the given one, ensuring // that its center is within the CRS's bounds. static wrapLatLngBounds(bounds) { bounds = new LatLngBounds(bounds); const center = bounds.getCenter(), newCenter = this.wrapLatLng(center), latShift = center.lat - newCenter.lat, lngShift = center.lng - newCenter.lng; if (latShift === 0 && lngShift === 0) { return bounds; } const sw = bounds.getSouthWest(), ne = bounds.getNorthEast(), newSw = new LatLng(sw.lat - latShift, sw.lng - lngShift), newNe = new LatLng(ne.lat - latShift, ne.lng - lngShift); return new LatLngBounds(newSw, newNe); } } /* * @namespace CRS * @crs CRS.Earth * * Serves as the base for CRS that are global such that they cover the earth. * Can only be used as the base for other CRS and cannot be used directly, * since it does not have a `code`, `projection` or `transformation`. `distance()` returns * meters. */ class Earth extends CRS { static wrapLng = [-180, 180]; // Mean Earth Radius, as recommended for use by // the International Union of Geodesy and Geophysics, // see https://rosettacode.org/wiki/Haversine_formula static R = 6371000; // distance between two geographical points using Haversine approximation static distance(latlng1, latlng2) { const rad = Math.PI / 180, lat1 = latlng1.lat * rad, lat2 = latlng2.lat * rad, sinDLat = Mat