1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
|
"""GeoJSON distance helper."""
from __future__ import annotations
import logging
from haversine import haversine
from .geometries import Geometry, Point, Polygon
_LOGGER = logging.getLogger(__name__)
class GeoJsonDistanceHelper:
"""Helper to calculate distances between GeoJSON geometries."""
def __init__(self):
"""Initialise the geo distance helper."""
pass
@staticmethod
def extract_coordinates(geometry: Geometry) -> tuple[float, float] | None:
"""Extract the best coordinates from the feature for display."""
latitude = longitude = None
if isinstance(geometry, Point):
# Just extract latitude and longitude directly.
latitude, longitude = geometry.latitude, geometry.longitude
elif isinstance(geometry, Polygon):
centroid = geometry.centroid
latitude, longitude = centroid.latitude, centroid.longitude
_LOGGER.debug("Centroid of %s is %s", geometry, (latitude, longitude))
else:
_LOGGER.debug("Not implemented: %s", type(geometry))
return latitude, longitude
@staticmethod
def distance_to_geometry(
coordinates: tuple[float, float], geometry: Geometry
) -> float:
"""Calculate the distance between coordinates and geometry."""
distance = float("inf")
if isinstance(geometry, Point):
distance = GeoJsonDistanceHelper._distance_to_point(coordinates, geometry)
elif isinstance(geometry, Polygon):
distance = GeoJsonDistanceHelper._distance_to_polygon(coordinates, geometry)
else:
_LOGGER.debug("Not implemented: %s", type(geometry))
return distance
@staticmethod
def _distance_to_point(coordinates: tuple[float, float], point: Point) -> float:
"""Calculate the distance between coordinates and the point."""
# Swap coordinates to match: (latitude, longitude).
return GeoJsonDistanceHelper._distance_to_coordinates(
coordinates, (point.latitude, point.longitude)
)
@staticmethod
def _distance_to_polygon(
coordinates: tuple[float, float], polygon: Polygon
) -> float:
"""Calculate the distance between coordinates and the polygon."""
distance = float("inf")
# Check if coordinates are inside the polygon.
if polygon.is_inside(Point(coordinates[0], coordinates[1])):
return 0.0
# Calculate distance from polygon by calculating the distance
# to each point of the polygon.
for polygon_point in polygon.points:
distance = min(
distance,
GeoJsonDistanceHelper._distance_to_point(coordinates, polygon_point),
)
# Next calculate the distance to each edge of the polygon.
for edge in polygon.edges:
distance = min(
distance, GeoJsonDistanceHelper._distance_to_edge(coordinates, edge)
)
_LOGGER.debug("Distance between %s and %s: %s", coordinates, polygon, distance)
return distance
@staticmethod
def _distance_to_coordinates(
coordinates1: tuple[float, float], coordinates2: tuple[float, float]
) -> float:
"""Calculate the distance between two coordinates tuples.."""
# Expecting coordinates in format: (latitude, longitude).
return haversine(coordinates2, coordinates1)
@staticmethod
def _distance_to_edge(
coordinates: tuple[float, float], edge: tuple[Point, Point]
) -> float:
"""Calculate distance between coordinates and provided edge."""
perpendicular_point = GeoJsonDistanceHelper._perpendicular_point(
edge, Point(coordinates[0], coordinates[1])
)
# If there is a perpendicular point on the edge -> calculate distance.
# If there isn't, then the distance to the end points of the edge will
# need to be considered separately.
if perpendicular_point:
distance = GeoJsonDistanceHelper._distance_to_point(
coordinates, perpendicular_point
)
_LOGGER.debug("Distance between %s and %s: %s", coordinates, edge, distance)
return distance
return float("inf")
@staticmethod
def _perpendicular_point(edge: tuple[Point, Point], point: Point) -> Point | None:
"""Find a perpendicular point on the edge to the provided point."""
a, b = edge
# Safety check: a and b can't be an edge if they are the same point.
if a == b:
return None
px = point.longitude
py = point.latitude
ax = a.longitude
ay = a.latitude
bx = b.longitude
by = b.latitude
# Alter longitude to cater for 180 degree crossings.
if px < 0:
px += 360.0
if ax < 0:
ax += 360.0
if bx < 0:
bx += 360.0
if ay > by or ax > bx:
ax, ay, bx, by = bx, by, ax, ay
dx = abs(bx - ax)
dy = abs(by - ay)
shortest_length = ((dx * (px - ax)) + (dy * (py - ay))) / (
(dx * dx) + (dy * dy)
)
rx = ax + dx * shortest_length
ry = ay + dy * shortest_length
if bx >= rx >= ax and by >= ry >= ay:
if rx > 180:
# Correct longitude.
rx -= 360.0
return Point(ry, rx)
return None
|