File: geojson_distance_helper.py

package info (click to toggle)
python-aio-geojson-client 0.20-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 268 kB
  • sloc: python: 1,368; makefile: 4
file content (142 lines) | stat: -rw-r--r-- 5,468 bytes parent folder | download
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