leaflet
Version:
JavaScript library for mobile-friendly interactive maps
242 lines (205 loc) • 6.14 kB
JavaScript
import {Layer} from './Layer.js';
import * as Util from '../core/Util.js';
import {getFeature, asFeature} from './GeoJSONUtil.js';
/** @import {Feature, FeatureCollection, MultiPoint, Position} from 'geojson' */
/** @import {InteractiveLayerEventMap, InteractiveLayerOptions} from '../types.js' */
/** @import {LatLng} from '../geo/LatLng.js' */
/** @import {LatLngBounds} from '../geo/LatLngBounds.js' */
/** @import {LeafletMap} from '../map/Map.js' */
/** @import {Marker} from './marker/Marker.js' */
/** @import {LayerBase} from './LayerBase.js' */
// a layer with the methods that only some layers have, which groups call when present
/**
* @template {LayerBase} [L=Layer]
* @typedef {L & {
* setZIndex?(zIndex: number): unknown,
* setStyle?(style?: object): unknown,
* bringToFront?(): unknown,
* bringToBack?(): unknown,
* getBounds?(): LatLngBounds,
* getLatLng?(): LatLng,
* toGeoJSON?(precision?: number | false): Feature | FeatureCollection
* }} MemberLayer
*/
/**
* Used to group several layers and handle them as one. If you add it to the map, any layers added
* or removed from the group will be added/removed on the map as well. Extends `Layer`.
* @example
* ```js
* new LayerGroup([marker1, marker2])
* .addLayer(polyline)
* .addTo(map);
* ```
* @template {LayerBase} [L=Layer]
* @template {InteractiveLayerOptions} [O=InteractiveLayerOptions]
* @template {InteractiveLayerEventMap} [E=InteractiveLayerEventMap]
* @extends {Layer<O, E>}
*/
export class LayerGroup extends Layer {
static initHooks = true;
/**
* Create a layer group, optionally given an initial set of layers and an `options` object.
* @param {NoInfer<L>[]} [layers]
* @param {NoInfer<Partial<O>>} [options]
*/
constructor(layers, options) {
super();
Util.setOptions(this, options);
/** @type {{[id: number]: MemberLayer<L>}} */
this._layers = {};
for (const layer of layers ?? []) {
this.addLayer(layer);
}
this.callInitHooks();
}
/**
* Adds the given layer to the group.
* @param {L} layer
*/
addLayer(layer) {
const id = this.getLayerId(layer);
this._layers[id] = layer;
this._map?.addLayer(layer);
return this;
}
/**
* Removes the given layer, or the layer with the given internal ID, from the group.
* @param {L | number} layer
*/
removeLayer(layer) {
const id = typeof layer === 'number' ? layer : this.getLayerId(layer);
if (this._map && this._layers[id]) {
this._map.removeLayer(this._layers[id]);
}
delete this._layers[id];
return this;
}
/**
* Returns `true` if the given layer, or the layer with the given internal ID, is currently added to the group.
* @param {L | number} layer
*/
hasLayer(layer) {
const layerId = typeof layer === 'number' ? layer : this.getLayerId(layer);
return layerId in this._layers;
}
/** Removes all the layers from the group. */
clearLayers() {
return this.eachLayer(this.removeLayer, this);
}
/** @param {LeafletMap} map */
onAdd(map) {
this.eachLayer(map.addLayer, map);
}
/** @param {LeafletMap} map */
onRemove(map) {
this.eachLayer(map.removeLayer, map);
}
/**
* Iterates over the layers of the group, optionally specifying context of the iterator function.
* ```js
* group.eachLayer(layer => layer.bindPopup('Hello'));
* ```
* @param {(layer: L) => void} method
* @param {unknown} [context]
*/
eachLayer(method, context) {
for (const layer of Object.values(this._layers)) {
method.call(context, layer);
}
return this;
}
/**
* Returns the layer with the given internal ID.
* @param {number} id
* @returns {L | undefined}
*/
getLayer(id) {
return this._layers[id];
}
/**
* Returns an array of all the layers added to the group.
* @returns {L[]}
*/
getLayers() {
/** @type {L[]} */
const layers = [];
this.eachLayer(layer => layers.push(layer));
return layers;
}
/**
* Calls `setZIndex` on every layer contained in this group, passing the z-index.
* @param {number} zIndex
*/
setZIndex(zIndex) {
for (const layer of Object.values(this._layers)) {
layer.setZIndex?.(zIndex);
}
return this;
}
/**
* Returns the internal ID for a layer
* @param {L} layer
*/
getLayerId(layer) {
return Util.stamp(layer);
}
/**
* Returns a GeoJSON `MultiPoint` feature with the positions of the group's markers. `precision` is the number of
* decimal places for coordinates, as in [`toGeoJSON`](#layergroup-togeojson).
* @param {number | false} [precision]
* @returns {Feature<MultiPoint>}
*/
toMultiPoint(precision) {
/** @type {Position[]} */
const coords = [];
this.eachLayer((layer) => {
coords.push(/** @type {Marker} */ (/** @type {LayerBase} */ (layer)).toGeoJSON(precision).geometry.coordinates);
});
return getFeature(this, {
type: 'MultiPoint',
coordinates: coords
});
}
/**
* Coordinates values are rounded with [`formatNum`](#util-formatnum) function with given `precision`.
* Returns a [`GeoJSON`](https://en.wikipedia.org/wiki/GeoJSON) representation of the layer group
* (as a GeoJSON `FeatureCollection`, `GeometryCollection`, or `MultiPoint`).
* @param {number | false} [precision]
* @returns {Feature | FeatureCollection}
*/
toGeoJSON(precision) {
const type = this.feature?.geometry?.type;
if (type === 'MultiPoint') {
return this.toMultiPoint(precision);
}
const isGeometryCollection = type === 'GeometryCollection',
/** @type {any[]} */
jsons = [];
for (const layer of Object.values(this._layers)) {
if (layer.toGeoJSON) {
const json = layer.toGeoJSON(precision);
if (isGeometryCollection) {
jsons.push(/** @type {Feature} */ (json).geometry);
} else {
const feature = asFeature(json);
// Squash nested feature collections
if (feature.type === 'FeatureCollection') {
jsons.push(...feature.features);
} else {
jsons.push(feature);
}
}
}
}
if (isGeometryCollection) {
return getFeature(this, {
geometries: jsons,
type: 'GeometryCollection'
});
}
return {
type: 'FeatureCollection',
features: jsons
};
}
}