import math import random import copy from typing import List, Tuple, Union def point_from_heading(_x, _y, heading, distance): """Calculates a point from a given point, heading and distance. :param _x: source point x :param _y: source point y :param heading: heading in degrees from source point :param distance: distance from source point :return: returns a tuple (x, y) of the calculated point """ while heading < 0: heading += 360 heading %= 360 rad_heading = math.radians(heading) x = _x + math.cos(rad_heading) * distance y = _y + math.sin(rad_heading) * distance return x, y def distance(x1, y1, x2, y2): """Returns the distance between 2 points :param x1: x coordinate of point 1 :param y1: y coordinate of point 1 :param x2: x coordinate of point 2 :param y2: y coordinate of point 2 :return: distance in point units(m) """ return math.hypot(x2 - x1, y2 - y1) def heading_between_points(x1, y1, x2, y2): """Returns the angle between 2 points in degrees. :param x1: x coordinate of point 1 :param y1: y coordinate of point 1 :param x2: x coordinate of point 2 :param y2: y coordinate of point 2 :return: angle in degrees """ def angle_trunc(a): while a < 0.0: a += math.pi * 2 return a deltax = x2 - x1 deltay = y2 - y1 return math.degrees(angle_trunc(math.atan2(deltay, deltax))) class Point: def __init__(self, x, y): self.x = x self.y = y def point_from_heading(self, heading, distance): x, y = point_from_heading(self.x, self.y, heading, distance) return Point(x, y) def heading_between_point(self, point): return heading_between_points(self.x, self.y, point.x, point.y) def distance_to_point(self, point): return distance(self.x, self.y, point.x, point.y) def random_point_within(self, distance, min_distance=0): """Returns a random point within the given distance. This is a shortcut for Rectangle.from_point().random_point(). Args: distance: max distance for the random point. min_distance: minimum distance the random point should have from origin Returns: Point: a new random point within the given distance from the point. """ return self.point_from_heading(random.randrange(0, 360), random.random() * (distance - min_distance) + min_distance) def __add__(self, other): return Point(self.x + other.x, self.y + other.y) def __radd__(self, other): if isinstance(other, Point): return self + other return Point(self.x + other, self.y + other) def __sub__(self, other): if isinstance(other, Point): return Point(self.x - other.x, self.y - other.y) return Point(self.x - other, self.y - other) def __mul__(self, other): return Point(self.x * other, self.y * other) def __rmul__(self, other): return self * other def __eq__(self, other): return self.x == other.x and self.y == other.y def __ne__(self, other): return not self == other def __repr__(self): return "Point({x}, {y})".format(x=self.x, y=self.y) class Triangle: def __init__(self, points: Union[Tuple[Point, Point, Point], List[Point]]): if len(points) != 3: raise RuntimeError("Triangle needs 3 points.") self.points = copy.copy(points) # type: List[Point] def area(self): a = (self.points[0].x * self.points[1].y + self.points[1].x * self.points[2].y + self.points[2].x * self.points[0].y - self.points[0].y * self.points[1].x - self.points[1].y * self.points[2].x - self.points[2].y * self.points[0].x) a /= 2 return a def random_point(self) -> Point: r = random.random() s = random.random() if r + s >= 1: r = 1 - r s = 1 - s return self.points[0] + (r * (self.points[1] - self.points[0]) + s * (self.points[2] - self.points[0])) def __repr__(self): return "Triangle({points})".format(points=", ".join(map(repr, self.points))) class Rectangle: def __init__(self, top, left, bottom, right): self.top = top self.left = left self.bottom = bottom self.right = right @staticmethod def from_point(point: Point, side_length): top = point.x + side_length / 2 left = point.y - side_length / 2 bottom = point.x - side_length / 2 right = point.y + side_length / 2 return Rectangle(top, left, bottom, right) def point_in_rect(self, point: Point): return self.bottom <= point.x <= self.top and self.left <= point.y <= self.right def height(self): return self.top - self.bottom def width(self): return self.right - self.left def center(self) -> Point: return Point(self.bottom + (self.height() / 2), self.left + (self.width() / 2)) def resize(self, percentage: float): w = self.width() h = self.height() w *= percentage / 2 h *= percentage / 2 c = self.center() return Rectangle(c.x + h, c.y - w, c.x - h, c.y + w) def random_point(self) -> Point: x = self.bottom + random.random() * (self.top - self.bottom) y = self.left + random.random() * (self.right - self.left) return Point(x, y) def random_distant_points(self, distance) -> Tuple[Point, Point]: # determine vertical/horizontal if self.width() > self.height(): axis_y = self.width() axis_x = self.height() sy = self.left sx = self.bottom hdg_start = 60 else: axis_y = self.height() axis_x = self.width() sy = self.bottom sx = self.left hdg_start = 330 d = distance if distance < axis_y else axis_y * 0.2 sy += random.random() * (axis_y - d) sx += random.random() * axis_x p1 = Point(sx, sy) while True: hdg = random.random() * 60 p2 = p1.point_from_heading(hdg_start + hdg, d) if self.point_in_rect(p2): return p1, p2 def __eq__(self, other): return self.top == other.top and self.bottom == other.bottom \ and self.left == other.left and self.right == other.right def __ne__(self, other): return not self == other def __repr__(self): return "Rectangle({t}, {l}, {b}, {r})".format(t=self.top, l=self.left, b=self.bottom, r=self.right) class Polygon: def __init__(self, points: List[Point]=None): if points is None: points = [] self.points = copy.copy(points) def point_in_poly(self, point: Point): """Checks if the given point is within the polygon. :param point: Point to test :return: True if point is within the polygon else False """ n = len(self.points) inside = False p1x = self.points[0].x p1y = self.points[0].y for i in range(n+1): p = self.points[i % n] p2x = p.x p2y = p.y if point.y > min(p1y, p2y): if point.y <= max(p1y, p2y): if point.x <= max(p1x, p2x): xints = 0 if p1y != p2y: xints = (point.y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x if p1x == p2x or point.x <= xints: inside = not inside p1x, p1y = p2x, p2y return inside def random_point(self) -> Point: """Returns a random point within this polygon object :return: a random point """ # split polygon into triangles using ear clipping tris = self.triangulate() # calculate areas of the triangles areas = [(x, x.area()) for x in tris] full_area = sum([x[1] for x in areas]) # calculate full area rtri = random.random() * full_area # get a random value from the area # iterate through areas until we found "ours" s = areas[0][1] i = 1 while s <= rtri: s += areas[i][1] i += 1 # return a random point from this triangle return areas[i-1][0].random_point() @staticmethod def is_convex(a: Point, b: Point, c: Point): # only convex if traversing anti-clockwise! crossp = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) if crossp >= 0: return True return False @staticmethod def in_triangle(a, b, c, p): l = [0, 0, 0] eps = 0.0000001 # calculate barycentric coefficients for point p # eps is needed as error correction since for very small distances denom->0 l[0] = ((b.y - c.y) * (p.x - c.x) + (c.x - b.x) * (p.y - c.y)) /\ (((b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y)) + eps) l[1] = ((c.y - a.y) * (p.x - c.x) + (a.x - c.x) * (p.y - c.y)) /\ (((b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y)) + eps) l[2] = 1 - l[0] - l[1] # check if p lies in triangle (a, b, c) for x in l: if x >= 1 or x <= 0: return False return True def is_clockwise(self): poly_length = len(self.points) # initialize sum with last element sum_ = (self.points[0].x - self.points[poly_length-1].x) * (self.points[0].y + self.points[poly_length-1].y) # iterate over all other elements (0 to n-1) for i in range(poly_length-1): sum_ += (self.points[i+1].x - self.points[i].x) * (self.points[i+1].y + self.points[i].y) return sum_ > 0 @staticmethod def get_ear(poly): size = len(poly) if size < 3: return [] if size == 3: tri = (poly[0], poly[1], poly[2]) del poly[:] return tri for i in range(size): tritest = False p1 = poly[(i-1) % size] p2 = poly[i % size] p3 = poly[(i+1) % size] if Polygon.is_convex(p1, p2, p3): for x in poly: if not (x in (p1, p2, p3)) and Polygon.in_triangle(p1, p2, p3, x): tritest = True if not tritest: del poly[i % size] return p1, p2, p3 print('GetEar(): no ear found') return [] def triangulate(self): tri = [] plist = self.points[::-1] if self.is_clockwise() else self.points[:] while len(plist) >= 3: a = self.get_ear(plist) if not a: break tri.append(Triangle(a)) return tri def outbound_rectangle(self) -> Rectangle: top = max([x.x for x in self.points]) bot = min([x.x for x in self.points]) right = max([x.y for x in self.points]) left = min([x.y for x in self.points]) return Rectangle(top, left, bot, right) def __repr__(self): return "Polygon([{points}])".format(points=", ".join(map(repr, self.points)))