maplibre / maplibre-gl-geocoder

Geocoding for MapLibre
ISC License
57 stars 19 forks source link

MaplibreGeocoderOptions needs a maxZoom option #181

Open mvl22 opened 19 hours ago

mvl22 commented 19 hours ago

On selecting a result, the map will move to the supplied bbox by default if present.

However, sometimes the bbox can be very small, e.g. a part of a street.

It would be useful if there was a maxZoom option to prevent flying in extremely closely. For instance, in the case of a small street, being flown into zoom 19 is unhelpful because there is no visual context as to where that street is.

Although there is a zoom option, this is essentially a brute-force option, resulting a specific zoom that will be the same for e.g. an entire country vs a minor street.

While the map as a whole can have maxZoom, that often needs to be a different setting - a user may want to zoom closely into the map to examine a section, but for the purposes of a geocoding result, such closeness is by contrast not helpful.

HarelM commented 19 hours ago

The results are controlled by the user of this library, one can wrap the resulting API and check if the bbox is too small and increase its size to avoid this issue. I'm not saying it's the best solution, but it's pragmatic workaround. Since there is zoom parameter for a similar behavior I would like to avoid maxZoom if possible.

mvl22 commented 19 hours ago

I'm not saying it's the best solution, but it's pragmatic workaround.

I agree that is an approach, but I think this is likely to be error-prone since it involves unfamiliar bbox/zoom maths conversion that is probably beyond many Javascript coders. Can anyone point to an example implementation of such an algorithm?

Given this is a usability problem that every user of the bbox data implementation will run into, I think it far better supported at the library level if possible.

I did look to see if the underlying CameraOptions support this, but I believe there is no maxZoom there.

HarelM commented 8 hours ago

Camera options are designed to move you to a place you desire, the constraint are from the map definitions.

As to the code, I've asked cloude to write me a bbox to zoom conversion method to allow checking if the expected zoom is "too high" and got this, I didn't treat it, but it looks good in general: It is a bit long to write for every person that needs it, so maxZoom might be a needed addition to this lib. Feel free to open a PR.

/**
 * Converts a bounding box to a zoom level for MapLibre
 * @param {Object} bbox - Bounding box coordinates in [west, south, east, north] format
 * @param {number} viewportWidth - Width of the map container in pixels
 * @param {number} viewportHeight - Height of the map container in pixels
 * @param {number} [padding=0] - Optional padding in pixels
 * @returns {number} Zoom level
 */
function bboxToZoom(bbox, viewportWidth, viewportHeight, padding = 0) {
    const [west, south, east, north] = bbox;

    // Apply padding to viewport dimensions
    const paddedWidth = Math.max(0, viewportWidth - 2 * padding);
    const paddedHeight = Math.max(0, viewportHeight - 2 * padding);

    // Calculate the longitude and latitude spans
    const lonSpan = east - west;
    const latSpan = north - south;

    // Guard against zero spans
    if (lonSpan === 0 && latSpan === 0) {
        return 0;
    }

    // Calculate the center latitude for better accuracy
    const centerLat = (south + north) / 2;

    // Convert latitude to radians for math operations
    const centerLatRad = centerLat * Math.PI / 180;

    // Calculate the number of pixels per degree at zoom level 0
    const TILE_SIZE = 512; // MapLibre's default tile size
    const pixelsPerLonDegreeAtZoom0 = TILE_SIZE / 360;
    const pixelsPerLatDegreeAtZoom0 = TILE_SIZE / (2 * Math.PI);

    // Calculate the number of pixels needed for the given bounding box
    const lonPixels = lonSpan * pixelsPerLonDegreeAtZoom0;
    const latPixels = Math.log(Math.tan(Math.PI / 4 + centerLatRad / 2)) * pixelsPerLatDegreeAtZoom0;

    // Calculate zoom based on both longitude and latitude spans
    const zoomH = Math.log2(paddedWidth / lonPixels);
    const zoomV = Math.log2(paddedHeight / Math.abs(latPixels));

    // Use the smaller zoom level to ensure the entire bounding box is visible
    const zoom = Math.min(zoomH, zoomV);

    // Clamp zoom level between 0 and 24 (MapLibre's zoom range)
    return Math.min(Math.max(zoom, 0), 24);
}

/**
 * Helper function to convert zoom level to scale
 * @param {number} zoom - Zoom level
 * @returns {number} Scale
 */
function zoomToScale(zoom) {
    return Math.pow(2, zoom);
}

/**
 * Helper function to convert scale to zoom level
 * @param {number} scale - Scale
 * @returns {number} Zoom level
 */
function scaleToZoom(scale) {
    return Math.log2(scale);
}

// Example usage:
const bbox = [-122.4194, 37.7749, -122.4089, 37.7858]; // San Francisco area
const zoom = bboxToZoom(bbox, 800, 600, 20); // 800x600 viewport with 20px padding