xmet/lib/xmet/geo.py

157 lines
4 KiB
Python

import re
import enum
import math
import shapely
from typing import Self
class PointDirection(enum.Enum):
EQUAL = 0
LEFT = enum.auto()
RIGHT = enum.auto()
class PointSequence(list):
linestring: shapely.LineString
polygon: shapely.Polygon
@staticmethod
def from_file(path: str) -> Self:
ret = PointSequence()
with open(path, 'r') as fh:
data = fh.read()
for line in data.split('\n'):
if line == '':
continue
lat, lon = re.split(r'\s*,\s*', line)
ret.add(float(lon), float(lat))
return ret
def __init__(self, points: list=None):
super().__init__()
self.linestring = None
self.polygon = None
if points is not None:
for point in points:
typeof = type(point)
if typeof is tuple:
self.add(*point)
elif typeof is shapely.Point:
self.append(point)
def add(self, lon: float, lat: float):
self.append(shapely.Point(lon, lat))
def is_closed(self) -> bool:
return self[-1] == self[0]
def close(self):
if not self.is_closed():
self.append(self[0])
self.linestring = shapely.LineString(self)
self.polygon = shapely.Polygon(self)
def nearest_index(self, point: shapely.Point) -> int:
indices = list()
for i in range(0, len(self)):
indices.append((i, self[i].distance(point)))
indices.sort(key=lambda i: i[1])
return indices[0][0]
def index_distance(self, i1: int, i2: int) -> int:
"""
Returns the index distance of i1 relative to i2, and whether i1 is
considered left of, equal to, or right of i2.
"""
count = len(self)
value = count - ((i1 - i2) % count)
if value == 0:
direction = PointDirection.EQUAL
elif value > count / 2:
direction = PointDirection.RIGHT
else:
direction = PointDirection.LEFT
return value, direction
class PolygonBuilder():
point_first: shapely.Point
point_last: shapely.Point
def __init__(self, sequence: PointSequence, bounds: PointSequence):
self.sequence = sequence
self.bounds = bounds
self.point_first = None
self.point_last = None
self.total = 0
def yield_point(self, point: shapely.Point):
"""
Yield the single point to the caller, while maintaining state of
number of points yielded to the caller.
"""
self.point_last = point
yield point
self.total += 1
def each_intermediate_point(self, p1: shapely.Point, p2: shapely.Point):
count = len(self.bounds)
i1 = self.bounds.nearest_index(p1)
i2 = self.bounds.nearest_index(p2)
dist, direction = self.bounds.index_distance(i1, i2)
if direction is not PointDirection.LEFT:
return
for i in range(i1, i2):
yield from self.yield_point(self.bounds[i % count])
def each_point_within(self):
for point in self.sequence:
if self.point_first is None:
self.point_first = point
self.point_last = point
self.total += 1
yield point
#
# If the first point is to the left of the last point, fill in the
# intermediates.
#
if self.total == 2:
i1 = self.bounds.nearest_index(self.point_first)
i2 = self.bounds.nearest_index(self.point_last)
dist, direction = self.bounds.index_distance(i1, i2)
if direction is PointDirection.RIGHT:
yield from self.each_intermediate_point(self.point_last,
self.point_first)
def process(self) -> shapely.Polygon:
pass
def heading(p1: shapely.Point, p2: shapely.Point) -> float:
dx = p2.x - p1.x
dy = p2.y - p1.y
return math.atan2(dy, dx)