Skip to content

Commit

Permalink
fix(polygon): Address the edge cases of polygon_relationship
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey authored and Chris Mackey committed Mar 12, 2023
1 parent aef471e commit a2c5338
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 13 deletions.
42 changes: 41 additions & 1 deletion ladybug_geometry/geometry2d/_1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from __future__ import division

from .pointvector import Vector2D, Point2D
from ..intersection2d import intersect_line2d, closest_point2d_on_line2d
from ..intersection2d import intersect_line2d, closest_point2d_on_line2d, \
closest_point2d_on_line2d_infinite


class Base1DIn2D(object):
Expand Down Expand Up @@ -102,6 +103,45 @@ def intersect_line_ray(self, line_ray):
"""
return intersect_line2d(self, line_ray)

def is_parallel(self, line_ray, angle_tolerance):
"""Test whether this object is parallel to another LineSegment2D or Ray2D.
Args:
line_ray: Another LineSegment2D or Ray2D for which parallelization
with this objects will be tested.
angle_tolerance: The max angle in radians that the direction between
this object and another can vary for them to be considered
parallel.
"""
if self.v.angle(line_ray.v) <= angle_tolerance:
return True
elif self.v.angle(line_ray.v.reverse()) <= angle_tolerance:
return True
return False

def is_colinear(self, line_ray, tolerance, angle_tolerance=None):
"""Test whether this object is colinear to another LineSegment2D or Ray2D.
Args:
line_ray: Another LineSegment2D or Ray2D for which co-linearity
with this object will be tested.
tolerance: The maximum distance between the line_ray and the infinite
extension of this object for them to be considered colinear.
angle_tolerance: The max angle in radians that the direction between
this object and another can vary for them to be considered
parallel. If None, the angle tolerance will not be used to
evaluate co-linearity and the lines will only be considered
colinear if the endpoints of one line are within the tolerance
distance of the other line. (Default: None).
"""
if angle_tolerance is not None and \
not self.is_parallel(line_ray, angle_tolerance):
return False
_close_pt = closest_point2d_on_line2d_infinite(self.p, line_ray)
if self.p.distance_to_point(_close_pt) >= tolerance:
return False
return True

def duplicate(self):
"""Get a copy of this object."""
return self.__copy__()
Expand Down
37 changes: 28 additions & 9 deletions ladybug_geometry/geometry2d/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,16 +916,35 @@ def polygon_relationship(self, polygon, tolerance):
This will be one of the following:
* -1 = Outside polygon
* 0 = Intersects the polygon
* +1 = Inside polygon
* -1 = Outside this polygon
* 0 = Overlaps (intersects or contains) this polygon
* +1 = Inside this polygon
"""
pt_rels = [self.point_relationship(pt, tolerance) for pt in polygon]
if all(rel >= 0 for rel in pt_rels):
return 1
if all(rel <= 0 for rel in pt_rels):
return -1
return 0
# first evaluate the point relationships to rule out the inside case
pt_rels1 = [self.point_relationship(pt, tolerance) for pt in polygon]
pt_rels2 = [polygon.point_relationship(pt, tolerance) for pt in self]
if all(r1 >= 0 for r1 in pt_rels1) and all(r2 <= 0 for r2 in pt_rels2):
return 1 # definitely inside the polygon
if 1 in pt_rels1 or 1 in pt_rels2 or all(r2 == 0 for r2 in pt_rels2):
return 0 # definitely overlap in the polygons

# if two non-colinear edges intersect, we know there must be overlap
all_pts = self.vertices + polygon.vertices
for seg in self.segments:
for _s in polygon.segments:
if seg.is_colinear(_s, tolerance):
continue
int_pt = intersect_line2d(seg, _s)
if int_pt is None:
continue
for pt in all_pts:
if int_pt.is_equivalent(pt, tolerance):
break
else: # unique intersection point found; there is overlap
return 0

# we can reliably say that the polygons have nothing to do with one another
return -1

def distance_to_point(self, point):
"""Get the minimum distance between this shape and the input point.
Expand Down
10 changes: 7 additions & 3 deletions ladybug_geometry/geometry3d/_1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def is_parallel(self, line_ray, angle_tolerance):
return True
return False

def is_colinear(self, line_ray, tolerance, angle_tolerance):
def is_colinear(self, line_ray, tolerance, angle_tolerance=None):
"""Test whether this object is colinear to another LineSegment3D or Ray3D.
Args:
Expand All @@ -96,9 +96,13 @@ def is_colinear(self, line_ray, tolerance, angle_tolerance):
extension of this object for them to be considered colinear.
angle_tolerance: The max angle in radians that the direction between
this object and another can vary for them to be considered
parallel.
parallel. If None, the angle tolerance will not be used to
evaluate co-linearity and the lines will only be considered
colinear if the endpoints of one line are within the tolerance
distance of the other line. (Default: None).
"""
if not self.is_parallel(line_ray, angle_tolerance):
if angle_tolerance is not None and \
not self.is_parallel(line_ray, angle_tolerance):
return False
_close_pt = closest_point3d_on_line3d_infinite(self.p, line_ray)
if self.p.distance_to_point(_close_pt) >= tolerance:
Expand Down
51 changes: 51 additions & 0 deletions tests/polygon2d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,57 @@ def test_is_polygon_inside_outside():
assert polygon.is_polygon_outside(hole_4)


def test_polygon_relationship():
"""Test the polygon_relationship method."""
# check polygon with itself
bound_pts = [Point2D(0, 0), Point2D(4, 0), Point2D(4, 4), Point2D(0, 4)]
polygon = Polygon2D(bound_pts)
assert polygon.polygon_relationship(polygon, 0.01) == 1

# check polygon with various types holes with clearly-defined relationships
hole_pts_1 = [Point2D(1, 1), Point2D(1.5, 1), Point2D(1.5, 1.5), Point2D(1, 1.5)]
hole_pts_2 = [Point2D(2, 2), Point2D(3, 2), Point2D(3, 3), Point2D(2, 3)]
hole_pts_3 = [Point2D(2, 2), Point2D(6, 2), Point2D(6, 6), Point2D(2, 6)]
hole_pts_4 = [Point2D(5, 5), Point2D(6, 5), Point2D(6, 6), Point2D(5, 6)]
hole_1 = Polygon2D(hole_pts_1)
hole_2 = Polygon2D(hole_pts_2)
hole_3 = Polygon2D(hole_pts_3)
hole_4 = Polygon2D(hole_pts_4)
assert polygon.polygon_relationship(hole_1, 0.01) == 1
assert polygon.polygon_relationship(hole_2, 0.01) == 1
assert polygon.polygon_relationship(hole_3, 0.01) == 0
assert polygon.polygon_relationship(hole_4, 0.01) == -1

# check the polygon with an adjacent one within tolerance
adj_pts = [Point2D(3.999, 0), Point2D(5, 0), Point2D(5, 4), Point2D(4, 4)]
adj_p = Polygon2D(adj_pts)
assert polygon.polygon_relationship(adj_p, 0.01) == -1

# check the polygon colinear with the other
in_pts = [Point2D(0, 0), Point2D(4, 0), Point2D(4, 2), Point2D(0, 2)]
in_p = Polygon2D(in_pts)
assert polygon.polygon_relationship(in_p, 0.01) == 1
assert in_p.polygon_relationship(polygon, 0.01) == 0

# check the polygon with an overlapping intersection
int_pts = [Point2D(-1, 1), Point2D(5, 1), Point2D(5, 3), Point2D(-1, 3)]
int_p = Polygon2D(int_pts)
assert polygon.polygon_relationship(int_p, 0.01) == 0

# check the polygon that contains the other
cont_pts = [Point2D(-1, -1), Point2D(5, -1), Point2D(5, 5), Point2D(-1, 5)]
cont_p = Polygon2D(cont_pts)
assert polygon.polygon_relationship(cont_p, 0.01) == 0
assert cont_p.polygon_relationship(polygon, 0.01) == 1

# check the polygon with a concave overlap
conc_pts = [Point2D(-1, -1), Point2D(5, -1), Point2D(5, 5), Point2D(3, 5),
Point2D(3, 3), Point2D(2, 3), Point2D(2, 5), Point2D(-1, 5)]
conc_p = Polygon2D(conc_pts)
assert polygon.polygon_relationship(conc_p, 0.01) == 0
assert conc_p.polygon_relationship(polygon, 0.01) == 0


def test_distance_to_point():
"""Test the distance_to_point method."""
pts = (Point2D(0, 0), Point2D(4, 0), Point2D(4, 2), Point2D(2, 2),
Expand Down

0 comments on commit a2c5338

Please sign in to comment.