File: geometry.py

package info (click to toggle)
python-aio-georss-client 0.12-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 376 kB
  • sloc: python: 2,656; xml: 513; makefile: 4
file content (203 lines) | stat: -rw-r--r-- 7,073 bytes parent folder | download | duplicates (2)
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"""Geometry models."""
from __future__ import annotations


class Geometry:
    """Represents a geometry."""


class Point(Geometry):
    """Represents a point."""

    def __init__(self, latitude: float, longitude: float):
        """Initialise point."""
        self._latitude: float = latitude
        self._longitude: float = longitude

    def __repr__(self):
        """Return string representation of this point."""
        return "<{}(latitude={}, longitude={})>".format(
            self.__class__.__name__, self.latitude, self.longitude
        )

    def __hash__(self) -> int:
        """Return unique hash of this geometry."""
        return hash((self.latitude, self.longitude))

    def __eq__(self, other: object) -> bool:
        """Return if this object is equal to other object."""
        return (
            self.__class__ == other.__class__
            and self.latitude == other.latitude
            and self.longitude == other.longitude
        )

    @property
    def latitude(self) -> float | None:
        """Return the latitude of this point."""
        return self._latitude

    @property
    def longitude(self) -> float | None:
        """Return the longitude of this point."""
        return self._longitude


class Polygon(Geometry):
    """Represents a polygon."""

    def __init__(self, points: list[Point]):
        """Initialise polygon."""
        self._points: list[Point] = points

    def __repr__(self):
        """Return string representation of this polygon."""
        return f"<{self.__class__.__name__}(centroid={self.centroid})>"

    def __hash__(self) -> int:
        """Return unique hash of this geometry."""
        return hash(self.points)

    def __eq__(self, other: object) -> bool:
        """Return if this object is equal to other object."""
        return self.__class__ == other.__class__ and self.points == other.points

    @property
    def points(self) -> list[Point] | None:
        """Return the points of this polygon."""
        return self._points

    @property
    def edges(self) -> list[tuple[Point, Point]]:
        """Return all edges of this polygon."""
        edges: list[tuple[Point, Point]] = []
        for i in range(1, len(self.points)):
            previous = self.points[i - 1]
            current = self.points[i]
            edges.append((previous, current))
        return edges

    @property
    def centroid(self) -> Point:
        """Find the polygon's centroid as a best approximation."""
        longitudes_list: list[float] = [point.longitude for point in self.points]
        latitudes_list: list[float] = [point.latitude for point in self.points]
        number_of_points: int = len(self.points)
        longitude: float = sum(longitudes_list) / number_of_points
        latitude: float = sum(latitudes_list) / number_of_points
        return Point(latitude, longitude)

    def is_inside(self, point: Point | None) -> bool:
        """Check if the provided point is inside this polygon."""
        if point:
            crossings: int = 0
            for edge in self.edges:
                if Polygon._ray_crosses_segment(point, edge):
                    crossings += 1
            return crossings % 2 == 1
        return False

    @staticmethod
    def _ray_crosses_segment(point: Point, edge: tuple[Point, Point]):
        """Use ray-casting algorithm to check provided point and edge."""
        a, b = edge
        px = point.longitude
        py = point.latitude
        ax = a.longitude
        ay = a.latitude
        bx = b.longitude
        by = b.latitude
        if ay > by:
            ax = b.longitude
            ay = b.latitude
            bx = a.longitude
            by = a.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 py == ay or py == by:
            py += 0.00000001
        if (py > by or py < ay) or (px > max(ax, bx)):
            return False
        if px < min(ax, bx):
            return True

        red = ((by - ay) / (bx - ax)) if (ax != bx) else float("inf")
        blue = ((py - ay) / (px - ax)) if (ax != px) else float("inf")
        return blue >= red


class BoundingBox(Geometry):
    """Represents a bounding box (bbox)."""

    # <!--gdacs: bbox format = lonmin lonmax latmin latmax -->
    # <gdacs:bbox> 164.5652 172.5652 -24.9041 -16.9041 </gdacs:bbox>

    def __init__(self, bottom_left: Point, top_right: Point):
        """Initialise bounding box."""
        self._bottom_left: Point = bottom_left
        self._top_right: Point = top_right

    def __repr__(self):
        """Return string representation of this bounding box."""
        return "<{}(bottom_left={}, top_right={})>".format(
            self.__class__.__name__, self._bottom_left, self._top_right
        )

    def __hash__(self) -> int:
        """Return unique hash of this geometry."""
        return hash((self.bottom_left, self.top_right))

    def __eq__(self, other: object) -> bool:
        """Return if this object is equal to other object."""
        return (
            self.__class__ == other.__class__
            and self.bottom_left == other.bottom_left
            and self.top_right == other.top_right
        )

    @property
    def bottom_left(self) -> Point:
        """Return bottom left point."""
        return self._bottom_left

    @property
    def top_right(self) -> Point:
        """Return top right point."""
        return self._top_right

    @property
    def centroid(self) -> Point:
        """Find the bounding box's centroid as a best approximation."""
        transposed_top_right_longitude: float = self._top_right.longitude
        if self._bottom_left.longitude > self._top_right.longitude:
            # bounding box spans across 180 degree longitude
            transposed_top_right_longitude = self._top_right.longitude + 360
        longitude: float = (
            self._bottom_left.longitude + transposed_top_right_longitude
        ) / 2
        latitude: float = (self._bottom_left.latitude + self._top_right.latitude) / 2
        return Point(latitude, longitude)

    def is_inside(self, point: Point) -> bool:
        """Check if the provided point is inside this bounding box."""
        if point:
            transposed_point_longitude: float = point.longitude
            transposed_top_right_longitude = self._top_right.longitude
            if self._bottom_left.longitude > self._top_right.longitude:
                # bounding box spans across 180 degree longitude
                transposed_top_right_longitude = self._top_right.longitude + 360
                if point.longitude < 0:
                    transposed_point_longitude += 360
            return (
                self._bottom_left.latitude <= point.latitude <= self._top_right.latitude
            ) and (
                self._bottom_left.longitude
                <= transposed_point_longitude
                <= transposed_top_right_longitude
            )