Source code for pygeohash.bounding_box

"""Bounding box operations for geohashes.

This module provides functions for working with geohash bounding boxes,
including calculating the bounding box for a geohash and operations
related to geospatial regions.
"""

from __future__ import annotations

from typing import List, NamedTuple, Set, Iterator

from pygeohash.geohash import decode_exactly, encode
from pygeohash.logging import get_logger

logger = get_logger(__name__)


[docs]class BoundingBox(NamedTuple): """Named tuple representing a geospatial bounding box. Attributes: min_lat (float): The minimum (southern) latitude of the box in decimal degrees. min_lon (float): The minimum (western) longitude of the box in decimal degrees. max_lat (float): The maximum (northern) latitude of the box in decimal degrees. max_lon (float): The maximum (eastern) longitude of the box in decimal degrees. """ min_lat: float min_lon: float max_lat: float max_lon: float
[docs]def get_bounding_box(geohash: str) -> BoundingBox: """Calculate the bounding box for a geohash. Args: geohash (str): The geohash string to calculate the bounding box for. Returns: BoundingBox: A named tuple containing the minimum and maximum latitude and longitude values that define the bounding box of the geohash. Example: >>> get_bounding_box("u4pruyd") BoundingBox(min_lat=57.649, min_lon=10.407, max_lat=57.649, max_lon=10.407) Note: The precision of the coordinates in the bounding box depends on the length of the geohash. Longer geohashes result in smaller bounding boxes with more precise coordinates. """ logger.debug("Calculating bounding box for geohash: %s", geohash) # Get the center point and error margins lat, lon, lat_err, lon_err = decode_exactly(geohash) logger.debug("Center point: (lat=%f, lon=%f) with errors: (lat_err=%f, lon_err=%f)", lat, lon, lat_err, lon_err) # Calculate the bounding box coordinates min_lat: float = lat - lat_err min_lon: float = lon - lon_err max_lat: float = lat + lat_err max_lon: float = lon + lon_err result = BoundingBox(min_lat, min_lon, max_lat, max_lon) logger.debug("Calculated bounding box: %s", result) return result
[docs]def is_point_in_box(lat: float, lon: float, bbox: BoundingBox) -> bool: """Check if a point is within a bounding box. Args: lat (float): The latitude of the point to check. lon (float): The longitude of the point to check. bbox (BoundingBox): The bounding box to check against. Returns: bool: True if the point is within the bounding box, False otherwise. Example: >>> bbox = get_bounding_box("u4pruyd") >>> is_point_in_box(57.649, 10.407, bbox) True >>> is_point_in_box(40.0, 10.0, bbox) False """ logger.debug("Checking if point (lat=%f, lon=%f) is in box: %s", lat, lon, bbox) result = bbox.min_lat <= lat <= bbox.max_lat and bbox.min_lon <= lon <= bbox.max_lon logger.debug("Point is %s the box", "inside" if result else "outside") return result
[docs]def is_point_in_geohash(lat: float, lon: float, geohash: str) -> bool: """Check if a point is within a geohash's bounding box. Args: lat (float): The latitude of the point to check. lon (float): The longitude of the point to check. geohash (str): The geohash to check against. Returns: bool: True if the point is within the geohash's bounding box, False otherwise. Example: >>> is_point_in_geohash(57.649, 10.407, "u4pruyd") True >>> is_point_in_geohash(40.0, 10.0, "u4pruyd") False """ logger.debug("Checking if point (lat=%f, lon=%f) is in geohash: %s", lat, lon, geohash) bbox: BoundingBox = get_bounding_box(geohash) return is_point_in_box(lat, lon, bbox)
[docs]def do_boxes_intersect(bbox1: BoundingBox, bbox2: BoundingBox) -> bool: """Check if two bounding boxes intersect. Args: bbox1 (BoundingBox): The first bounding box. bbox2 (BoundingBox): The second bounding box. Returns: bool: True if the bounding boxes intersect, False otherwise. Example: >>> box1 = BoundingBox(10.0, 20.0, 30.0, 40.0) >>> box2 = BoundingBox(20.0, 30.0, 40.0, 50.0) >>> do_boxes_intersect(box1, box2) True """ logger.debug("Checking intersection between boxes: %s and %s", bbox1, bbox2) result = not ( bbox1.max_lat < bbox2.min_lat or bbox1.min_lat > bbox2.max_lat or bbox1.max_lon < bbox2.min_lon or bbox1.min_lon > bbox2.max_lon ) logger.debug("Boxes %s intersect", "do" if result else "do not") return result
[docs]def geohashes_in_box(bbox: BoundingBox, precision: int = 6) -> List[str]: """Find geohashes that intersect with a given bounding box. Args: bbox (BoundingBox): The bounding box to find geohashes for. precision (int, optional): The precision of the geohashes to return. Defaults to 6. Returns: List[str]: A list of geohashes that intersect with the bounding box. Example: >>> box = BoundingBox(57.64, 10.40, 57.65, 10.41) >>> geohashes_in_box(box, precision=5) ['u4pru', 'u4prv'] Note: The number of geohashes returned depends on the size of the bounding box and the precision requested. Higher precision values will result in more geohashes for the same bounding box. """ logger.debug("Finding geohashes in box %s with precision %d", bbox, precision) # Find a geohash at the center of the bounding box center_lat: float = (bbox.min_lat + bbox.max_lat) / 2 center_lon: float = (bbox.min_lon + bbox.max_lon) / 2 center_geohash: str = encode(center_lat, center_lon, precision) logger.debug("Center geohash: %s at (lat=%f, lon=%f)", center_geohash, center_lat, center_lon) # Get the size of a geohash at this precision center_bbox: BoundingBox = get_bounding_box(center_geohash) lat_step: float = center_bbox.max_lat - center_bbox.min_lat lon_step: float = center_bbox.max_lon - center_bbox.min_lon logger.debug("Geohash size at precision %d: lat_step=%f, lon_step=%f", precision, lat_step, lon_step) # Create a set to store unique geohashes result: Set[str] = set() # Calculate the starting points slightly outside the bounding box # to ensure we cover the entire area start_lat: float = bbox.min_lat - lat_step end_lat: float = bbox.max_lat + lat_step start_lon: float = bbox.min_lon - lon_step end_lon: float = bbox.max_lon + lon_step logger.debug("Search area: lat=[%f, %f], lon=[%f, %f]", start_lat, end_lat, start_lon, end_lon) # Sample points in a grid pattern with spacing based on geohash size # This ensures we get all geohashes that intersect with the bounding box for lat in _float_range(start_lat, end_lat, lat_step / 2): for lon in _float_range(start_lon, end_lon, lon_step / 2): if bbox.min_lat <= lat <= bbox.max_lat or bbox.min_lon <= lon <= bbox.max_lon: gh: str = encode(lat, lon, precision) gh_bbox: BoundingBox = get_bounding_box(gh) # Only add geohashes that actually intersect with our bounding box if do_boxes_intersect(bbox, gh_bbox): result.add(gh) logger.debug("Found %d intersecting geohashes", len(result)) return list(result)
def _float_range(start: float, stop: float, step: float) -> Iterator[float]: """Helper function to create a range of float values. Args: start (float): The start value. stop (float): The stop value (inclusive). step (float): The step size. Returns: Iterator[float]: An iterator of float values from start to stop with the given step size. """ current: float = start while current <= stop: yield current current += step