I keep struggling with keeping track of center (with zoom) and bounds in parallel while (re)combining them as I collect further markers and shape bounds to focus on map.
What I wish is to have a single object like centerOrBounds, which I can extend by any number of latLng, e.g. centerOrBounds.addLatLng(latLng) and bounds e.g. centerOrBounds.addBouns(bounds) and finally: centerOrBounds.getFocusedOn(map). See and try the code below which I’m using in my solution.
I haven’t found an out of the box solution for my problem yet and have the feeling like reinventing the wheel. Is there a solution to my problem that I might have missed?
Or is there a better way to handle combining bounds and lat/lng inputs for focusing a map in Leaflet?
The problem is, that bounds vs center is logically different stuff, with accordingly different handling - either: map.setView(center, zoom), or map.fitBounds(bounds) and fitBounds currently cannot handle bounds with just 1 coordinate, or 2 identical coordinates.
On some events, e.g. on shape or marker editing in the map I need to recollect my centerOrBounds.
(I need this in plain HTML/JavaScript/CSS page environ of my django-leaflet project.)
// Initialize the map
var map = L.map('map');
// Add OpenStreetMap tile layer
var tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
class MapFocusManager {
constructor(initialZoom = 15) {
this.bounds = L.latLngBounds();
this.standaloneCoords = undefined;
this.initialZoom = initialZoom;
}
addBounds(bounds) {
if (bounds && bounds instanceof L.LatLngBounds && bounds.isValid()) {
// fill bounds property for the first time if empty or just extend it if already filled:
this.bounds.extend(bounds);
if (this.standaloneCoords) {
// if there was just standaloneCoords before, integrate it in bounds and clear it:
this.bounds.extend(this.standaloneCoords);
this.standaloneCoords = undefined;
}
}
}
addLatLng(latLng) {
if (latLng) {
if (Array.isArray(latLng) && latLng.length === 2) {
// try to redirect - relying on: L.LatLng
this.addLatLng(L.latLng(latLng[0], latLng[1]));
} else if (latLng instanceof L.LatLng) {
if (this.standaloneCoords) {
// Combine the stored standaloneCoords with latLng to bounds and empty it:
this.bounds.extend([this.standaloneCoords, [latLng.lat, latLng.lng]]);
this.standaloneCoords = undefined;
} else if (this.bounds.isValid()) {
// simply extend bounds it if already filled:
this.bounds.extend(latLng);
} else {
// entering 1st coords in an onw empty state:
this.standaloneCoords = [latLng.lat, latLng.lng];
}
}
}
}
addLatLngArray(latLngArray) {
if (latLngArray && Array.isArray(latLngArray) &&
latLngArray.length > 0 && Array.isArray(latLngArray[0])
) {
latLngArray.forEach(coords => this.addLatLng(coords));
}
}
addCoordinates(lat, lng) {
this.addLatLng(L.latLng(lat, lng));
}
addMapFocusManager(manager) {
if (!manager) return;
if (!(manager instanceof MapFocusManager)) {
throw new Error('MapFocusManager must be provided.');
}
if (manager.bounds.isValid()) {
this.addBounds(manager.bounds);
}
if (manager.standaloneCoords) {
this.addLatLng(manager.standaloneCoords);
}
}
clone() {
const clonedManager = new MapFocusManager(this.initialZoom);
if (this.bounds.isValid()) {
clonedManager.addBounds(this.bounds);
}
if (this.standaloneCoords) {
clonedManager.addLatLng(this.standaloneCoords);
}
return clonedManager;
}
isEmpty() {
return !this.bounds.isValid() && !this.standaloneCoords;
}
focusMapTo(map) {
if (!map) {
throw new Error('Map must be provided to focus.');
}
const currentTileLayerMaxZoom = mapGetCurrentTileLayerMaxZoom(map);
if (this.bounds.isValid()) {
console.log(`focusMapTo: fitBounds: currentTileLayerMaxZoom: ${currentTileLayerMaxZoom}`, this);
map.fitBounds(this.bounds, currentTileLayerMaxZoom ? { maxZoom: currentTileLayerMaxZoom } : undefined);
} else if (this.standaloneCoords) {
const finalZoom = Math.min(this.initialZoom, currentTileLayerMaxZoom ?? this.initialZoom);
console.log(`focusMapTo: setView: ${this.initialZoom}, currentTileLayerMaxZoom: ${currentTileLayerMaxZoom}, finalZoom: ${finalZoom}`, this);
map.setView(this.standaloneCoords, finalZoom);
}
}
static create(input = null, initialZoom = 15) {
let manager = new MapFocusManager(initialZoom);
if (!input) {
return manager;
}
if (Array.isArray(input)) {
if (input.length === 2 && typeof input[0] === 'number' && typeof input[1] === 'number') {
manager.addLatLng(input);
} else {
manager.addLatLngArray(input);
}
} else if (input instanceof L.LatLngBounds) {
manager.addBounds(input);
} else if (input instanceof L.LatLng) {
manager.addLatLng(input);
}
return manager;
}
}
const mapGetCurrentTileLayerMaxZoom = function(map, fallbackMaxZoom = 20) {
if (!map) {
throw new Error('Map must be provided to get currentTileLayerMaxZoom.');
}
let maxTileLayerZoom = undefined;
// Find the current tile layer and extract the maxZoom if available
map.eachLayer(function(layer) {
if (layer instanceof L.TileLayer && layer.options.maxZoom) {
maxTileLayerZoom = layer.options.maxZoom;
console.log(`mapGetCurrentTileLayerMaxZoom: found L.TileLayer with maxTileLayerZoom: ${maxTileLayerZoom}`, layer);
}
});
return maxTileLayerZoom ?? fallbackMaxZoom;
};
// Example usage:
var latLngCoords1 = [49.603630, 10.158256];
var latLngCoords2 = [49.613928, 10.179981];
var latLngCoords3 = [49.619242, 10.187804];
var latLngCoords4 = [49.626327, 10.143388];
var latLngCoords5 = [49.621538, 10.15684];
L.marker(latLngCoords1).addTo(map).bindTooltip(`<b>Map Manager 1</b><br/>Initial center: ${latLngCoords1}<br/>Initial zoom: 17`);
L.marker(latLngCoords2).addTo(map).bindTooltip(`<b>Map Manager 2</b><br/>Bouds: <i>South West</i><br/>${latLngCoords2}`);
L.marker(latLngCoords3).addTo(map).bindTooltip(`<b>Map Manager 2</b><br/>Bouds: <i>North East</i><br/>${latLngCoords3}`);
L.marker(latLngCoords4).addTo(map).bindTooltip(`<b>Map Manager 3</b><br/>LatLng: <i>North West</i><br/>${latLngCoords4}`);
L.marker(latLngCoords5).addTo(map).bindTooltip(`<b>Map Manager 3</b><br/>LatLng: <i>South East</i><br/>${latLngCoords5}`);
var latLngArrayX1 = [latLngCoords2, latLngCoords3];
var initialBounds = L.latLngBounds(latLngArrayX1);
var latLngArrayX2 = [latLngCoords4, latLngCoords5];
var mapFocusManager1 = MapFocusManager.create(latLngCoords1, 17);
var mapFocusManager2 = MapFocusManager.create(initialBounds, 14);
var mapFocusManager3 = MapFocusManager.create(latLngArrayX2);
// Clone mapFocusManager1
var clonedMapFocusManager = mapFocusManager1.clone();
// Add mapFocusManager2 into mapFocusManager3
clonedMapFocusManager.addMapFocusManager(mapFocusManager2);
clonedMapFocusManager.addMapFocusManager(mapFocusManager3);
mapFocusManager1.focusMapTo(map);
#map {
height: calc(65vh);
width: 100%;
}
.btn-container {
margin-top: 15px;
text-align: center;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Leaflet Map Focus Manager</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
</head>
<body>
<div class="container mt-5">
<div class="row">
<div class="col-md-12">
<h1 class="text-center">Leaflet Map with Focus Manager</h1>
<div id="map"></div>
<div class="btn-container">
<button class="btn btn-primary" onclick="mapFocusManager1.focusMapTo(map)">Focus Map Manager 1</button>
<button class="btn btn-success" onclick="mapFocusManager2.focusMapTo(map)">Focus Map Manager 2</button>
<button class="btn btn-info" onclick="mapFocusManager3.focusMapTo(map)">Focus Map Manager 3</button>
<button class="btn btn-warning" onclick="clonedMapFocusManager.focusMapTo(map)">Clone Mngr 1 & Add Mngr 2 & 3</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap 5 JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<!-- Leaflet JavaScript -->
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
</body>
</html>