def is_valid_polygon(polygon: Polygon) -> bool: """Check that no pair of edges in the given polygon properly intersects. Additionally check for general position.""" for i, edge_i in enumerate(polygon.edges()): for j, edge_j in enumerate(polygon.edges()): if i != j and edge_i.properly_intersects(edge_j): return False if j not in ((i + 1) % len(polygon), (i - 1) % len(polygon)) and edge_i.intersects(edge_j): return False # Make sure there are no three points on a line for p in polygon.points: for q in polygon.points: if p == q: continue for r in polygon.points: if p == r or q == r: continue if Point.turn(p, q, r) == Point.NO_TURN: return False return True
def in_subpolygon(polygon: Polygon, q1: Point, q2: Point, t: Point, t_trapezoid: Trapezoid) -> bool: """O(1): Return whether `t` lies inside the subpolygon of `polygon` to the right of `q1`,`q2`.""" assert isinstance(q1, (PolygonPoint, EdgePoint)) assert isinstance(q2, (PolygonPoint, EdgePoint)) assert isinstance(t, Point) assert isinstance(t_trapezoid, Trapezoid) assert isinstance(polygon, Polygon) # Because our line (q1,q2) can start and/or end on edges we need to be careful in taking decisions. # Find the vertex indices for q1,q2. If q1 lies on an edge we need to increase the index by one to have the smaller # subpolygon. ix1 = q1.index if isinstance(q1, EdgePoint): ix1 = polygon.next(ix1) # The second index does not need special treatment since the index already shrinks the polygon. ix2 = q2.index if ix1 == ix2: assert isinstance(q1, EdgePoint) return Point.turn(q1, q2, t) != Point.CCW_TURN # First we check in which part of the (possibly) smaller subpolygon our trapezoid lies. small_polygon_position = trapezoid_subpolygon_position( polygon, ix1, ix2, t_trapezoid) # If the trapezoid lies right to the line it is safe. if small_polygon_position == 1: return True # In the other cases we should have a closer look. # Now we widen the subpolygon s.t. we can check whether t lies completely outside our range. if isinstance(q1, EdgePoint): ix1 = polygon.prev(ix1) if isinstance(q2, EdgePoint): ix2 = polygon.next(ix2) # If the point does not lie inside this bigger subpolygon we can safely say it does not lie in the actual # subpolygon if ix1 != ix2: big_polygon_position = trapezoid_subpolygon_position( polygon, ix1, ix2, t_trapezoid) if big_polygon_position == small_polygon_position == -1: return False # Now the trapezoid lies somewhere between the small and the big subpolygon. # First we can decide with respect to the x-coordinates -- just checking where t lies with respect to line(q1,q2) # does not work! if t.is_right_of(q1) and t.is_right_of(q2): if ((isinstance(q1, EdgePoint) and q1.index == t_trapezoid.bot_edge_ix) or (isinstance(q2, EdgePoint) and q2.index == t_trapezoid.top_edge_ix)): return True if ((isinstance(q1, EdgePoint) and q1.index == t_trapezoid.top_edge_ix) or (isinstance(q2, EdgePoint) and q2.index == t_trapezoid.bot_edge_ix)): return False if t.left_of(q1) and t.left_of(q2): if ((isinstance(q1, EdgePoint) and q1.index == t_trapezoid.top_edge_ix) or (isinstance(q2, EdgePoint) and q2.index == t_trapezoid.bot_edge_ix)): return True if ((isinstance(q1, EdgePoint) and q1.index == t_trapezoid.bot_edge_ix) or (isinstance(q2, EdgePoint) and q2.index == t_trapezoid.top_edge_ix)): return False return Point.turn(q1, q2, t) != Point.CCW_TURN
def shortest_path(polygon: Polygon, s: Point, t: Point) -> Iterable[Point]: """O(n^2): Return the shortest path from s to t in the given polygon. This function uses only constant additional space and takes time O(n^2). Args: polygon: A polygon in counter-clockwise order. s: The start point (inside the polygon). t: The end point (inside the polygon). Returns: An iterator over the list of all vertex points of the polygonal chain representing the shortest geodesic path from s to t inside the polygon. Raises: AssertionError: A type check fails. """ # ========================================================================== # Reset properties which can be accessed later on. # ========================================================================== shortest_path.properties = dict(iterations=0) # ========================================================================== # Type checking. # ========================================================================== assert isinstance(polygon, Polygon) assert isinstance(s, Point) assert isinstance(t, Point) # ========================================================================== # Trivial case: s == t. # ========================================================================== # In the very trivial case the start and end point are identical thus we can # just return without any calculation. if s == t: yield s return # Save s and t so we can output the original values even though we modify the # original ones original_s = s original_t = t # ========================================================================== # Locate s and t. Trivial case: both in same trapezoid. # ========================================================================== # Locate start and end point inside our polygon. s_trapezoid = polygon.trapezoid(s) t_trapezoid = polygon.trapezoid(t) # If s lies directly on trapezoid boundary shift it by some small value if s.x in (s_trapezoid.x_left, s_trapezoid.x_right): shift = min(s_trapezoid.x_right - s_trapezoid.x_left, 0.00002) / 2 if s.x == s_trapezoid.x_left: s = Point(s.x + shift, s.y) else: s = Point(s.x - shift, s.y) s_trapezoid = polygon.trapezoid(s) # If t lies directly on trapezoid boundary shift it by some small value if t.x in (t_trapezoid.x_left, t_trapezoid.x_right): shift = min(t_trapezoid.x_right - t_trapezoid.x_left, 0.00002) / 2 if t.x == t_trapezoid.x_left: t = Point(t.x + shift, t.y) else: t = Point(t.x - shift, t.y) t_trapezoid = polygon.trapezoid(t) # If both points are located inside the same trapezoid just return both in # order if s_trapezoid == t_trapezoid: yield original_s yield original_t return # ========================================================================== # Find next trapezoid when going from s to t. This is needed for # initialisation. # ========================================================================== # Find out whether we have to go left or right to find t go_left = t_trapezoid.is_left_of(s_trapezoid) # Get the neighbouring trapezoids only on the side we are looking at if go_left: neighbours = polygon.neighbour_trapezoids(s_trapezoid, 0b10) else: neighbours = polygon.neighbour_trapezoids(s_trapezoid, 0b01) # Each trapezoid side has at most 2 neighbours (due to not two # x-coordinates being the same). Furthermore if we need to go to one # side there has to be at least one neighbour. assert len(neighbours) in (1, 2) # Choose the first neighbour if we only have one or it lies in the right # direction if (len(neighbours) == 1 or (go_left and t_trapezoid.is_left_of(neighbours[0])) or (not go_left and t_trapezoid.is_right_of(neighbours[0]))): next_trapezoid = neighbours[0] else: next_trapezoid = neighbours[1] # Get the boundary between the old and the new current trapezoid boundary = next_trapezoid.intersection(s_trapezoid) # Edges need to be oriented in counter-clockwise direction if Point.turn(original_s, boundary.a, boundary.b) == Point.CW_TURN: boundary.reverse() elif Point.turn(original_s, boundary.a, boundary.b) == Point.NO_TURN: # Edge is always oriented from top to bottom -- so if we go right it # should be reversed so to have it correct if not go_left: boundary.reverse() # We can now define our triple (p, q1, q2) as in the algorithm p = PolygonPoint(original_s) q1 = boundary.a if q1.edge is not None: q1 = EdgePoint(q1, q1.edge) else: q1 = PolygonPoint(q1, q1.index) q2 = boundary.b if q2.edge is not None: q2 = EdgePoint(q2, q2.edge) else: q2 = PolygonPoint(q2, q2.index) # ========================================================================== # Call make_step until we can see t. # ========================================================================== while not polygon.point_sees_other_point(p, original_t): shortest_path.properties['iterations'] += 1 point, p, q1, q2 = make_step(p, q1, q2, original_t, polygon, t_trapezoid) if point: if point.tuple() == original_s.tuple(): yield original_s else: yield point # ========================================================================== # Finish # ========================================================================== if p.tuple() == original_s.tuple(): yield original_s else: yield p yield original_t
def make_step(p: PolygonPoint, q1: Point, q2: Point, t: Point, polygon: Polygon, t_trapezoid: Trapezoid) -> MakeStepResult: """O(n): Advance the given triple (p, q1, q2) towards t. Args: p: q1: q2: t: polygon: Returns: """ # ========================================================================== # Type checking. # ========================================================================== assert isinstance(p, PolygonPoint) assert isinstance(q1, (PolygonPoint, EdgePoint)) assert isinstance(q2, (PolygonPoint, EdgePoint)) assert isinstance(t, Point) assert isinstance(t_trapezoid, Trapezoid) assert isinstance(polygon, Polygon) if isinstance(q1, PolygonPoint) and Point.turn( p, q1, polygon.succ(q1)) == Point.CW_TURN: q_prime = hit_polygon_boundary(p, q1, polygon) if in_subpolygon(polygon, q1, q_prime, t, t_trapezoid): return MakeStepResult(old_cusp=p, cusp=q1, right=polygon.succ(q1), left=q_prime) else: return MakeStepResult(old_cusp=None, cusp=p, right=q_prime, left=q2) elif isinstance(q2, PolygonPoint) and Point.turn( p, q2, polygon.pred(q2)) == Point.CCW_TURN: q_prime = hit_polygon_boundary(p, q2, polygon) if in_subpolygon(polygon, q_prime, q2, t, t_trapezoid): return MakeStepResult(old_cusp=p, cusp=q2, right=q_prime, left=polygon.pred(q2)) else: return MakeStepResult(old_cusp=None, cusp=p, right=q1, left=q_prime) else: succ_q1 = polygon.succ(q1) if Point.turn(p, q1, succ_q1) != Point.CW_TURN and Point.turn( p, q2, succ_q1) != Point.CCW_TURN: # succ(q1) lies in wedge q1,p,q2 q_prime = hit_polygon_boundary(p, succ_q1, polygon) if q_prime != q2: if p.squared_distance_to(q_prime) >= p.squared_distance_to( succ_q1): q_prime = succ_q1 if p.index is None: p_ = hit_polygon_boundary(q_prime, p, polygon) else: p_ = p if in_subpolygon(polygon, p_, q_prime, t, t_trapezoid): return MakeStepResult(old_cusp=None, cusp=p, right=q1, left=q_prime) else: return MakeStepResult(old_cusp=None, cusp=p, right=q_prime, left=q2) pred_q2 = polygon.pred(q2) q_prime = hit_polygon_boundary(p, pred_q2, polygon) if p.squared_distance_to(q_prime) >= p.squared_distance_to(pred_q2): q_prime = pred_q2 if p.index is None: p_ = hit_polygon_boundary(q_prime, p, polygon) else: p_ = p if in_subpolygon(polygon, q_prime, p_, t, t_trapezoid): return MakeStepResult(old_cusp=None, cusp=p, right=q_prime, left=q2) else: return MakeStepResult(old_cusp=None, cusp=p, right=q1, left=q_prime)
def jarvis_march( polygon: Polygon, start_index: int, end_index: int, direction: int, good_turn: int, predicate: Callable[[Point], T], ignore: Callable[[Point, Point], bool] = lambda x: False) -> Iterable[Point]: """Do a Jarvis march on the given polygon. We start at start_index going into direction stopping at end_index. For every vertex we consider appropriate predicate is applied. If it yields something which not evaluates to False we immediately stop the march and return the predicate result together with the vertex and a list of all points visited beforehand. Args: ignore: A function which is called for every vertex and should return True iff this vertex is to be ignored during the jarvis march. polygon: A polygon. start_index: The index of the starting vertex. end_index: The index of the last vertex to consider. direction: The direction in which we walk along the polygon edge. Needs to be either 1 or -1. good_turn: If the current, the next and a third vertex form a turn that is the same as good_turn, the third will be chosen over the next. predicate: A function that takes a vertex as an argument and decides whether to continue the march or stop. Returns: A 3-tuple (result, point, visited) in which result is the result of the predicate function, point is the point which fulfils the predicate and visited is a list of vertices visited in between. Raises: AssertionError: a) A type check fails. b) None of the vertices in the specified range fulfilled the predicate. """ # ========================================================================== # Type checking. # ========================================================================== assert isinstance(polygon, Polygon) assert isinstance(start_index, int) assert isinstance(end_index, int) assert isinstance(direction, int) assert isinstance(good_turn, int) first = polygon.point(start_index) while True: point_loc.properties['predicates'] += 1 result = predicate(first) # If the result does not evaluate to False return if result: return result, first # If this assertion fails none of the vertices fulfilled the predicate assert first.index != end_index second = polygon.point(first.index + direction) if second.index != end_index: for index in polygon.indices(second.index + direction, end_index, direction): point = polygon.point(index) point_loc.properties['ignores_theo'] += 1 if Point.turn(first, second, point) == good_turn: point_loc.properties['ignores'] += 1 if not ignore(first, point): second = point yield first first = second
def point_loc(polygon: Polygon, s: Point, t: Point) -> Iterable[Point]: """Return the shortest path from s to t in the given polygon. This function uses only constant additional space and takes time O(n^2). Args: polygon: A polygon in counter-clockwise order. s: The start point (inside the polygon). t: The end point (inside the polygon). Returns: An iterator over the list of all vertex points of the polygonal chain representing the shortest geodesic path from s to t inside the polygon. Raises: AssertionError: a) A type check fails. b) The number of neighbours found on one side of a trapezoid is not 1 or 2. c) The Jarvis march throws an AssertionError. """ # ========================================================================== # Reset properties which can be accessed later on. # ========================================================================== point_loc.properties = dict(iterations=0, jarvis_marches=0, predicates=0, ignores=0, ignores_theo=0) # ========================================================================== # Type checking. # ========================================================================== assert isinstance(polygon, Polygon) assert isinstance(s, Point) assert isinstance(t, Point) # ========================================================================== # Imports. # ========================================================================== from geometry import Funnel # ========================================================================== # Trivial case: s == t. # ========================================================================== # In the very trivial case the start and end point are identical thus we can # just return without any calculation. if s == t: yield s return # Save s and t so we can output the original values even though we modify the # original ones original_s = s original_t = t # ========================================================================== # Locate s and t. Trivial case: both in same trapezoid. # ========================================================================== # Locate start and end point inside our polygon. s_trapezoid = polygon.trapezoid(s) t_trapezoid = polygon.trapezoid(t) # If s lies directly on trapezoid boundary shift it by some small value if s.x in (s_trapezoid.x_left, s_trapezoid.x_right): shift = min(s_trapezoid.x_right - s_trapezoid.x_left, 0.00002) / 2 if s.x == s_trapezoid.x_left: s = Point(s.x + shift, s.y) else: s = Point(s.x - shift, s.y) s_trapezoid = polygon.trapezoid(s) # If t lies directly on trapezoid boundary shift it by some small value if t.x in (t_trapezoid.x_left, t_trapezoid.x_right): shift = min(t_trapezoid.x_right - t_trapezoid.x_left, 0.00002) / 2 if t.x == t_trapezoid.x_left: t = Point(t.x + shift, t.y) else: t = Point(t.x - shift, t.y) t_trapezoid = polygon.trapezoid(t) # If both points are located inside the same trapezoid just return both in # order if s_trapezoid == t_trapezoid: yield original_s yield original_t return # ========================================================================== # Preparation. # ========================================================================== # The cusp is the point we are always standing at and from which we can see # the trapezoid boundaries we are visiting. It gets updates in case we lose # visibility. We obviously start at the starting point. cusp = s # The funnel is our visibility angle. funnel = None # We also need to save the trapezoid we are currently in and the one we are # coming from current_trapezoid = s_trapezoid previous_trapezoid = None boundary = None previous_boundary = None # ========================================================================== # Walking the trapezoids. # ========================================================================== while current_trapezoid != t_trapezoid: point_loc.properties['iterations'] += 1 # ---------------------------------------------------------------------- # Finding the next trapezoid. # ---------------------------------------------------------------------- # Find out whether we have to go left or right to find t go_left = t_trapezoid.is_left_of(current_trapezoid) # Get the neighbouring trapezoids only on the side we are looking at if go_left: neighbours = polygon.neighbour_trapezoids(current_trapezoid, 0b10) else: neighbours = polygon.neighbour_trapezoids(current_trapezoid, 0b01) # Each trapezoid side has at most 2 neighbours (due to not two # x-coordinates being the same). Furthermore if we need to go to one # side there has to be at least one neighbour. assert len(neighbours) in (1, 2) # Since we are going to select a new current trapezoid save it already previous_trapezoid = current_trapezoid # Choose the first neighbour if we only have one or it lies in the right # direction if (len(neighbours) == 1 or (go_left and t_trapezoid.is_left_of(neighbours[0])) or (not go_left and t_trapezoid.is_right_of(neighbours[0]))): current_trapezoid = neighbours[0] else: current_trapezoid = neighbours[1] # Get the boundary between the old and the new current trapezoid previous_boundary = boundary boundary = current_trapezoid.intersection(previous_trapezoid) # Edges need to be oriented in counter-clockwise direction if Point.turn(cusp, boundary.a, boundary.b) == Point.CW_TURN: boundary.reverse() elif Point.turn(cusp, boundary.a, boundary.b) == Point.NO_TURN: # Edge is always oriented from top to bottom -- so if we go right it # should be reversed so to have it correct if not go_left: boundary.reverse() # On encountering the first boundary we do not have a funnel yet. We # then create it and start looking for the next trapezoid if funnel is None: funnel = Funnel(cusp, boundary.a, boundary.b) continue # ---------------------------------------------------------------------- # Checking (and possibly updating) the visibility. # ---------------------------------------------------------------------- # Check where both boundary end points are in respect to the funnel position_of_a = funnel.position_of(boundary.a) position_of_b = funnel.position_of(boundary.b) # Save whether both end points are on the same side of the funnel both_right_of = position_of_a == position_of_b == Funnel.RIGHT_OF both_left_of = position_of_a == position_of_b == Funnel.LEFT_OF # ---------------------------------------------------------------------- # CASE 1: We do not see the boundary any more. # ---------------------------------------------------------------------- if both_left_of or both_right_of: # The current view point will definitely change now. # We have to take care of the special case in which the cusp is the # starting point, because it might have been shifted by a small bit. if cusp == s: yield original_s else: yield cusp # ------------------------------------------------------------------ # Prepare the Jarvis march # ------------------------------------------------------------------ point_loc.properties['jarvis_marches'] += 1 params = prepare_jarvis_march(polygon, funnel, current_trapezoid, both_right_of, go_left, boundary) # ------------------------------------------------------------------ # Actually perform the Jarvis march # ------------------------------------------------------------------ if previous_boundary is None: x_bound_point = boundary.a else: x_bound_point = previous_boundary.a if both_right_of: ignore_func = ignore_function((cusp, x_bound_point), funnel, Funnel.RIGHT_OF) else: ignore_func = ignore_function((cusp, x_bound_point), funnel, Funnel.LEFT_OF) # Since polygon.point_sees_edge2 returns a tuple of the two funnel # points we directly extract them. Additionally we get the new cusp # and yield all vertices visited until finding cusp. (v1, v2), cusp = yield from jarvis_march( polygon=polygon, predicate=partial(polygon.point_sees_edge2, edge=boundary), # ignore=lambda first, second: not polygon.point_sees_other_point(first, second), ignore=ignore_func, # ignore=lambda u: u.x > max(s.x, cusp.x, boundary.a.x) or # u.x < min(s.x, cusp.x, boundary.a.x), **params) # ------------------------------------------------------------------ # Update the cusp and the funnel # ------------------------------------------------------------------ # In the special case in which the cusp falls together with an end # point of our edge we advance the funnel point on the next edge # into the right direction. # (We only compare cusp with v1 since polygon.point_sees_edge # guarantees to return the funnel point which falls together first.) if v1 == cusp: # If the cusp falls together with the top right or bottom left # edge we choose the next counter-clockwise point if v1.index in (current_trapezoid.top_right_ix, current_trapezoid.bot_left_ix): v1 = polygon.point(v1.index + 1) # If the cusp falls together with the top left or bottom right # edge we choose the next clockwise point elif v1.index in (current_trapezoid.bot_right_ix, current_trapezoid.top_left_ix): v1 = polygon.point(v1.index - 1) # Since v1 and v2 will be the funnel boundary points they # shall be in the right (counter-clockwise) order if Point.turn(cusp, v1, v2) == Point.CW_TURN: v1, v2 = v2, v1 # In some cases the funnel points returned by # polygon.point_sees_edge are not vertices but lie on polygon edges. # We do not want to have those points as funnel points since they # can suffer from floating point inaccuracies. # This should only be a problem iff the point lies on an edge # incident to the cusp for then the other endpoint may or may not be # found inside the funnel. if isinstance( v1, IntersectionPoint) and v1.index is None and isinstance( cusp, PolygonPoint): if v1.edge in (cusp.index, (cusp.index - 1) % polygon.len): # For v1 we can safely choose the endpoint of the edge which is # "more counter-clockwise" v1 = polygon.point(v1.edge + 1) if isinstance( v2, IntersectionPoint) and v2.index is None and isinstance( cusp, PolygonPoint): if v2.edge in (cusp.index, (cusp.index - 1) % polygon.len): # For v2 we can safely choose the endpoint of the edge which is # "more clockwise" v2 = polygon.point(v2.edge) funnel.cusp = cusp funnel.first = v1 funnel.second = v2 # ---------------------------------------------------------------------- # CASE 2: We see the boundary but we need to shrink the visibility. # ---------------------------------------------------------------------- else: # If needed (i.e. if the edge reduces the funnel) update the # second and first funnel point if funnel.contains(boundary.a) and boundary.a.index is not None: funnel.first = boundary.a if funnel.contains(boundary.b) and boundary.b.index is not None: funnel.second = boundary.b # ========================================================================== # Do the final Jarvis march # ========================================================================== # We have to take care of the special case in which the cusp is the # starting point, because it might have been shifted by a small bit. if cusp == s: yield original_s else: yield cusp if not polygon.point_sees_other_point(cusp, t): # Save whether the previous polygon is left or right of the # current one go_left = previous_trapezoid.is_right_of(current_trapezoid) point_loc.properties['jarvis_marches'] += 1 params = prepare_jarvis_march(polygon, funnel, current_trapezoid, funnel.position_of(t) == Funnel.RIGHT_OF, go_left) # ------------------------------------------------------------------ # Actually perform the Jarvis march # ------------------------------------------------------------------ if funnel.position_of(t) == Funnel.RIGHT_OF: ignore_func = ignore_function((cusp, boundary.a), funnel, Funnel.RIGHT_OF) else: ignore_func = ignore_function((cusp, boundary.a), funnel, Funnel.LEFT_OF) # Since we are nearly finished we only care about the list of visited # nodes and the new cusp (which is not contained in the list) _, cusp = yield from jarvis_march( polygon=polygon, predicate=partial(polygon.point_sees_other_point, other_point=t), # ignore=lambda first, second: not polygon.point_sees_other_point(first, second), ignore=ignore_func, # ignore=lambda u: u.x > max(s.x, cusp.x, t.x, boundary.a.x) or # u.x < min(s.x, cusp.x, t.x, boundary.a.x), **params) yield cusp yield original_t
def test_can_create_circle(p1, p2, p3): """Check that we can always create a circle if the points do not lie on one line.""" assume(Point.turn(p1, p2, p3) != Point.NO_TURN) Circle(p1, p2, p3)