leaflet
Version:
JavaScript library for mobile-friendly interactive maps
90 lines (72 loc) • 3.09 kB
JavaScript
import {CircleMarker} from './CircleMarker.js';
import {LatLngBounds} from '../../geo/LatLngBounds.js';
import {EarthCRS} from '../../geo/crs/EarthCRS.js';
/** @import {CircleMarkerEventMap, CircleOptions, LatLngExpression, PointExpression} from '../../types.js' */
/**
* A class for drawing circle overlays on a map with radius specified in meters. Extends `CircleMarker`.
*
* It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion).
* @example
* ```js
* new Circle([50.5, 30.5], {radius: 200}).addTo(map);
* ```
* @template {CircleOptions} [O=CircleOptions]
* @template {CircleMarkerEventMap} [E=CircleMarkerEventMap]
* @extends {CircleMarker<O, E>}
*/
export class Circle extends CircleMarker {
static initHooks = true;
/**
* Instantiates a circle object given a geographical point, and an options object which contains the circle
* radius.
* @param {LatLngExpression} latlng
* @param {NoInfer<Partial<O>>} [options]
*/
constructor(latlng, options) {
if (typeof options === 'number') {
throw new Error('Circle: the radius is an option in Leaflet 2. Use new Circle(latlng, {radius}).');
}
super(latlng, options);
if (isNaN(this.options.radius)) { throw new Error('Circle radius cannot be NaN'); }
this.callInitHooks();
}
/** Returns the `LatLngBounds` of the path. When the circle isn't on a map, they're approximated on a sphere. */
getBounds() {
if (!this._map) { return this._latlng.toBounds(this._radius * 2); }
/** @type {PointExpression} */
const half = [this._pxRadius, this._pxRadiusY ?? this._pxRadius];
return new LatLngBounds(
this._map.layerPointToLatLng(this._point.subtract(half)),
this._map.layerPointToLatLng(this._point.add(half)));
}
_project() {
const lng = this._latlng.lng,
lat = this._latlng.lat,
map = this._map,
crs = map.options.crs;
if (crs.distance === EarthCRS.distance) {
const d = Math.PI / 180,
latR = (this._radius / EarthCRS.R) / d,
top = map.project([lat + latR, lng]),
bottom = map.project([lat - latR, lng]),
p = top.add(bottom).divideBy(2),
lat2 = map.unproject(p).lat;
// Longitude span from the haversine form of the spherical law of cosines. The plain acos() form
// loses precision for radii of about a metre and less, where cos(latR) is too close to 1 (#6120).
let lngR = 2 * Math.asin(Math.sqrt(
(Math.sin(latR * d / 2) ** 2 - Math.sin((lat2 - lat) * d / 2) ** 2) /
(Math.cos(lat * d) * Math.cos(lat2 * d)))) / d;
if (isNaN(lngR) || lngR === 0) {
lngR = latR / Math.cos(Math.PI / 180 * lat); // Fallback for edge case, #2425
}
this._point = p.subtract(map.getPixelOrigin());
this._pxRadius = isNaN(lngR) ? 0 : p.x - map.project([lat2, lng - lngR]).x;
this._pxRadiusY = p.y - top.y;
} else {
const latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._radius, 0]));
this._point = map.latLngToLayerPoint(this._latlng);
this._pxRadius = Math.abs(this._point.x - map.latLngToLayerPoint(latlng2).x);
}
this._updateBounds();
}
}