1

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: '&copy; <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 &amp; Add Mngr 2 &amp; 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>

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.