leaflet
Version:
JavaScript library for mobile-friendly interactive maps
1,628 lines (1,368 loc) • 412 kB
JavaScript
/* @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