mapbox / mapbox-gl-js

Interactive, thoroughly customizable maps in the browser, powered by vector tiles and WebGL
https://docs.mapbox.com/mapbox-gl-js/
Other
11.22k stars 2.23k forks source link

The boxzoomend event should provide the box coordinates #12405

Closed mholt closed 1 year ago

mholt commented 1 year ago

Motivation

I'm trying to implement a "bounding box search" feature: the user can click+drag a box over the map to find results within that box.

Conveniently, Mapbox GL JS already has a function to allow the user to draw a box. It even zooms into the selected area!

Relevant events:

boxzoomstart: https://docs.mapbox.com/mapbox-gl-js/api/map/#map.event:boxzoomstart boxzoomend: https://docs.mapbox.com/mapbox-gl-js/api/map/#map.event:boxzoomend

Design Alternatives

Currently, doing this requires a tedious amount of code to wrangle event handling, drawing a polygon, and setting the cursor.

All I really need are the 4 sides of the bounding box in lat/lon (max+min latitude, max+min longitude), after the box is created.

Design

Add a coords property to the MapBoxZoomEvent object with the 4 sides of the box.

Mock-Up

map.on('boxzoomend', event => {
    console.log(event.coords);
});

^ From that, I can perform a search in my application and display relevant results on the map.

Of course, since the primary purpose of this function is to zoom the viewbox, my application has a button to toggle the "create bounding box" state. If the user clicks the button, then draws a box, we use that box as a search parameter and deactivate the button state. This solves the problem of the user unintentionally revising their search when they mean only to zoom.

Concepts

Uses only existing concepts as far as I can tell. Just providing the coordinates through the event.

Implementation

When the event is generated, simply add coords to the event object's properties.

Thank you for considering!

stepankuzmin commented 1 year ago

Hi @mholt,

You can also use the "Highlight features within a bounding box" example as a reference. As far as I understand your use case, you'd only need to unproject the bbox from pixel coordinates to the geographical coordinates like this:

const latLng1 = map.unproject(bbox[0]);
const latLng2 = map.unproject(bbox[1]);
mholt commented 1 year ago

@stepankuzmin Thank you! I had actually found that example and used it as a reference. I didn't know about unproject() at the time though, so I reworked that example to use a map event instead of a canvas event, along these lines:

on('click', '#bbox-toggle', event => {
    if ($('#bbox-toggle').classList.contains('active')) {
        // disable panning and box zooming (so as not to conflict with our custom box drawing)
        map.boxZoom.disable();
        map.dragPan.disable();

        // show useful cursor indicative of creating a box
        $('.mapboxgl-canvas-container').style.cursor = 'crosshair';

        // when user clicks down on map, start drawing process
        map.on('mousedown', mouseDown);

        const canvas = map.getCanvasContainer();

        // Variable to hold the starting xy point and
        // coordinates when mousedown occured.
        let startPt, startCoord;

        // Variable for the draw box element.
        let box;

        function mouseDown(e) {
            map.on('mousemove', onMouseMove);
            map.on('mouseup', onMouseUp);

            // for cancellation
            document.addEventListener('keydown', onKeyDown);

            // Capture the first xy coordinates
            startPt = e.point;
            startCoord = e.lngLat;
        }

        function onMouseMove(e) {
            // Capture the ongoing xy coordinates
            let current = e.point;

            if (!box) {
                box = document.createElement('div');
                box.classList.add('boxdraw');
                canvas.appendChild(box);
            }

            const minX = Math.min(startPt.x, current.x),
                maxX = Math.max(startPt.x, current.x),
                minY = Math.min(startPt.y, current.y),
                maxY = Math.max(startPt.y, current.y);

            // Adjust width and xy position of the box element ongoing
            const pos = `translate(${minX}px, ${minY}px)`;
            box.style.transform = pos;
            box.style.width = maxX - minX + 'px';
            box.style.height = maxY - minY + 'px';
        }

        function onMouseUp(e) {
            finish([startCoord, e.lngLat]);
        }

        function onKeyDown(e) {
            if (e.keyCode === 27) finish();
        }

        function finish(bbox) {
            // remove the box polygon (literally a "poly-gone", ha...)
            if (box) {
                box.remove();
                box = null;
            }

            if (bbox) {
                const bboxEl = $('#bbox');
                bboxEl.value = `(${bbox[0].lat.toFixed(4)}, ${bbox[0].lng.toFixed(4)}) (${bbox[1].lat.toFixed(4)}, ${bbox[1].lat.toFixed(4)})`
                bboxEl.dataset.lat1 = bbox[0].lat;
                bboxEl.dataset.lon1 = bbox[0].lng;
                bboxEl.dataset.lat2 = bbox[1].lat;
                bboxEl.dataset.lon2 = bbox[1].lng;
                trigger('#bbox', 'change');
            }

            map.dragPan.enable();
            map.boxZoom.enable();

            map.off('mousedown', mouseDown);
            map.off('mousemove', onMouseMove);
            map.off('mouseup', onMouseUp);

            $('.mapboxgl-canvas-container').style.cursor = '';

            $('#bbox-toggle').classList.remove('active');
        }
    }
});

(in case it helps anyone else who wants to do this in the future)

My code is based on the example you linked to.

My page has a #bbox-toggle element that, when active, should allow the user to draw a box (instead of pan). I use map events to get the geo coordinates instead of using canvas events which only have x,y coordinates (because I didn't know about unproject). My code seems to work, but maybe unproject will make things simpler!

Thanks again for the help. Mapbox is pretty cool.