def bisectors(self): """The angle bisectors of the triangle. An angle bisector of a triangle is a straight line through a vertex which cuts the corresponding angle in half. Returns ------- bisectors : dict Each key is a vertex (Point) and each value is the corresponding bisector (Segment). See Also -------- Point Segment Examples -------- >>> from sympy.geometry import Point, Triangle, Segment >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) >>> t = Triangle(p1, p2, p3) >>> from sympy import sqrt >>> t.bisectors()[p2] == Segment(Point(0, sqrt(2) - 1), Point(1, 0)) True """ s = self.sides v = self.vertices c = self.incenter l1 = Segment(v[0], Line(v[0], c).intersection(s[1])[0]) l2 = Segment(v[1], Line(v[1], c).intersection(s[2])[0]) l3 = Segment(v[2], Line(v[2], c).intersection(s[0])[0]) return {v[0]: l1, v[1]: l2, v[2]: l3}
def medians(self): """The medians of the triangle. A median of a triangle is a straight line through a vertex and the midpoint of the opposite side, and divides the triangle into two equal areas. Returns ------- medians : dict Each key is a vertex (Point) and each value is the median (Segment) at that point. See Also -------- Point Segment Examples -------- >>> from sympy.geometry import Point, Triangle >>> p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1) >>> t = Triangle(p1, p2, p3) >>> t.medians[p1] Segment(Point(0, 0), Point(1/2, 1/2)) """ s = self.sides v = self.vertices return { v[0]: Segment(s[1].midpoint, v[0]), v[1]: Segment(s[2].midpoint, v[1]), v[2]: Segment(s[0].midpoint, v[2]) }
def sides(self): """A list of the segments that form the sides of the polygon.""" res = [] for ind in xrange(0, len(self.vertices)-1): res.append( Segment(self.vertices[ind], self.vertices[ind+1]) ) res.append( Segment(self.vertices[-1], self.vertices[0]) ) return res
def sides(self): """The line segments that form the sides of the polygon. Returns ------- sides : list of sides Each side is a Segment. See Also -------- Point Segment Examples -------- >>> from sympy import Point, Polygon >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) >>> poly = Polygon(p1, p2, p3, p4) >>> poly.sides [Segment(Point(0, 0), Point(1, 0)), Segment(Point(1, 0), Point(5, 1)), Segment(Point(0, 1), Point(5, 1)), Segment(Point(0, 0), Point(0, 1))] """ res = [] for ind in xrange(0, len(self.vertices) - 1): res.append(Segment(self.vertices[ind], self.vertices[ind + 1])) res.append(Segment(self.vertices[-1], self.vertices[0])) return res
def bisectors(self): """ The angle bisectors of the triangle in a dictionary where the key is the vertex and the value is the bisector at that point. Example: ======== >>> from sympy.geometry import Point, Triangle, Segment >>> p1,p2,p3 = Point(0,0), Point(1,0), Point(0,1) >>> t = Triangle(p1, p2, p3) >>> from sympy import sqrt >>> t.bisectors[p2] == Segment(Point(0, sqrt(2)-1), Point(1, 0)) True """ s = self.sides v = self.vertices c = self.incenter l1 = Segment(v[0], GeometryEntity.do_intersection(Line(v[0], c), s[1])[0]) l2 = Segment(v[1], GeometryEntity.do_intersection(Line(v[1], c), s[2])[0]) l3 = Segment(v[2], GeometryEntity.do_intersection(Line(v[2], c), s[0])[0]) return {v[0]: l1, v[1]: l2, v[2]: l3}
def is_right(self): """Returns True if the triangle is right-angled, False otherwise.""" s = self.sides return ( Segment.is_perpendicular(s[0], s[1]) or Segment.is_perpendicular(s[1], s[2]) or Segment.is_perpendicular(s[0], s[2]) )
def convex_hull(*args): """ Returns a Polygon representing the convex hull of a set of 2D points. Notes: ====== This can only be performed on a set of non-symbolic points. Example: ======== >>> from sympy.geometry import Point, convex_hull >>> points = [ Point(x) for x in [(1,1), (1,2), (3,1), (-5,2), (15,4)] ] >>> convex_hull(points) Polygon(Point(-5, 2), Point(1, 1), Point(3, 1), Point(15, 4)) Description of method used: =========================== See http://en.wikipedia.org/wiki/Graham_scan. """ from point import Point from line import Segment from polygon import Polygon def uniquify(a): # not order preserving return list(set(a)) p = args[0] if isinstance(p, Point): p = uniquify(args) if len(p) == 1: return p[0] elif len(p) == 2: return Segment(p[0], p[1]) def orientation(p, q, r): '''Return positive if p-q-r are clockwise, neg if ccw, zero if colinear.''' return (q[1]-p[1])*(r[0]-p[0]) - (q[0]-p[0])*(r[1]-p[1]) '''scan to find upper and lower convex hulls of a set of 2d points.''' U = [] L = [] p.sort() for p_i in p: while len(U) > 1 and orientation(U[-2], U[-1], p_i) <= 0: U.pop() while len(L) > 1 and orientation(L[-2], L[-1], p_i) >= 0: L.pop() U.append(p_i) L.append(p_i) U.reverse() convexHull = tuple(L + U[1:-1]) if len(convexHull) == 2: return Segment(convexHull[0], convexHull[1]) return Polygon(convexHull)
def sides(self): """The line segments that form the sides of the polygon. Returns ------- sides : list of sides Each side is a Segment. Note ---- The Segments that represent the sides are an undirected line segment so cannot be used to tell the orientation of the polygon. See Also -------- Point Segment Examples -------- >>> from sympy import Point, Polygon >>> p1, p2, p3, p4 = map(Point, [(0, 0), (1, 0), (5, 1), (0, 1)]) >>> poly = Polygon(p1, p2, p3, p4) >>> poly.sides [Segment(Point(0, 0), Point(1, 0)), Segment(Point(1, 0), Point(5, 1)), Segment(Point(0, 1), Point(5, 1)), Segment(Point(0, 0), Point(0, 1))] """ res = [] for i in xrange(-len(self), 0): res.append(Segment(self[i], self[i + 1])) return res
def __new__(cls, *args, **kwargs): if len(args) != 3: raise GeometryError("Triangle.__new__ requires three points") vertices = [Point(a) for a in args] # remove consecutive duplicates nodup = [] for p in vertices: if nodup and p == nodup[-1]: continue nodup.append(p) if len(nodup) > 1 and nodup[-1] == nodup[0]: nodup.pop() # last point was same as first # remove collinear points i = -3 while i < len(nodup) - 3 and len(nodup) > 2: a, b, c = sorted([nodup[i], nodup[i + 1], nodup[i + 2]]) if Point.is_collinear(a, b, c): nodup[i] = a nodup[i + 1] = None nodup.pop(i + 1) i += 1 vertices = filter(lambda x: x is not None, nodup) if len(vertices) == 3: return GeometryEntity.__new__(cls, *vertices, **kwargs) elif len(vertices) == 2: return Segment(*vertices, **kwargs) else: return Point(*vertices, **kwargs)
def medians(self): """ The medians of the triangle in a dictionary where the key is the vertex and the value is the median at that point. Example: ======== >>> p1,p2,p3 = Point(0,0), Point(1,0), Point(0,1) >>> t = Triangle(p1, p2, p3) >>> t.medians[p1] Segment(Point(0, 0), Point(1/2, 1/2)) """ s = self.sides v = self.vertices return {v[0]: Segment(s[1].midpoint, v[0]), v[1]: Segment(s[2].midpoint, v[1]), v[2]: Segment(s[0].midpoint, v[2])}
def is_right(self): """Is the triangle right-angled. Returns ------- is_right : boolean Examples -------- >>> from sympy.geometry import Triangle, Point >>> t1 = Triangle(Point(0, 0), Point(4, 0), Point(4, 3)) >>> t1.is_right() True """ s = self.sides return Segment.is_perpendicular(s[0], s[1]) or \ Segment.is_perpendicular(s[1], s[2]) or \ Segment.is_perpendicular(s[0], s[2])
def encloses_point(self, p): """ Return True if p is enclosed by (is inside of) self. Notes ----- Being on the border of self is considered False. The general Polygon.encloses_point method is called only if a point is not within or beyond the incircle or circumcircle, respectively. Parameters ---------- p : Point Returns ------- encloses_point : True, False or None Examples -------- >>> from sympy import RegularPolygon, S, Point, Symbol >>> p = RegularPolygon((0, 0), 3, 4) >>> p.encloses_point(Point(0, 0)) True >>> r, R = p.inradius, p.circumradius >>> p.encloses_point(Point((r + R)/2, 0)) True >>> p.encloses_point(Point(R/2, R/2 + (R - r)/10)) False >>> t = Symbol('t', real=True) >>> p.encloses_point(p.arbitrary_point().subs(t, S.Half)) False >>> p.encloses_point(Point(5, 5)) False """ c = self.center d = Segment(c, p).length if d >= self.radius: return False elif d < self.inradius: return True else: # now enumerate the RegularPolygon like a general polygon. return Polygon.encloses_point(self, p)
def is_right(self): """Returns True if the triangle is right-angled, False otherwise.""" s = self.sides return Segment.is_perpendicular(s[0], s[1]) or \ Segment.is_perpendicular(s[1], s[2]) or \ Segment.is_perpendicular(s[0], s[2])
def convex_hull(*args): """The convex hull of a collection of 2-dimensional points. Parameters ---------- args : a collection of Points Returns ------- convex_hull : Polygon Notes ----- This can only be performed on a set of non-symbolic points. See Also -------- Point References ---------- http://en.wikipedia.org/wiki/Graham_scan Examples -------- >>> from sympy.geometry import Point, convex_hull >>> points = [Point(x) for x in [(1, 1), (1, 2), (3, 1), (-5, 2), (15, 4)]] >>> convex_hull(points) Polygon(Point(-5, 2), Point(1, 1), Point(3, 1), Point(15, 4)) """ from point import Point from line import Segment from polygon import Polygon def uniquify(a): # not order preserving return list(set(a)) p = args[0] if isinstance(p, Point): p = uniquify(args) if len(p) == 1: return p[0] elif len(p) == 2: return Segment(p[0], p[1]) def orientation(p, q, r): '''Return positive if p-q-r are clockwise, neg if ccw, zero if collinear.''' return (q[1] - p[1]) * (r[0] - p[0]) - (q[0] - p[0]) * (r[1] - p[1]) # scan to find upper and lower convex hulls of a set of 2d points. U = [] L = [] p.sort() for p_i in p: while len(U) > 1 and orientation(U[-2], U[-1], p_i) <= 0: U.pop() while len(L) > 1 and orientation(L[-2], L[-1], p_i) >= 0: L.pop() U.append(p_i) L.append(p_i) U.reverse() convexHull = tuple(L + U[1:-1]) if len(convexHull) == 2: return Segment(convexHull[0], convexHull[1]) return Polygon(convexHull)
def _do_poly_distance(self, e2): """ Calculates the least distance between the exteriors of two convex polygons e1 and e2. Does not check for the convexity of the polygons as it is assumed only called by Polygon.distance which does such checks. Notes ----- - Prints a warning if the two polygons possibly intersect as the return value will not be valid in such a case. For a more through test of intersection use intersection(). Example ------- >>> from sympy.geometry import Point, Polygon >>> square = Polygon(Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)) >>> triangle = Polygon(Point(1, 2), Point(2, 2), Point(2, 1)) >>> square._do_poly_distance(triangle) sqrt(2)/2 Description of method used -------------------------- Method: [1] http://cgm.cs.mcgill.ca/~orm/mind2p.html Uses rotating calipers: [2] http://en.wikipedia.org/wiki/Rotating_calipers and antipodal points: [3] http://en.wikipedia.org/wiki/Antipodal_point """ e1 = self '''Tests for a possible intersection between the polygons and outputs a warning''' e1_center = e1.centroid e2_center = e2.centroid e1_max_radius = S(0) e2_max_radius = S(0) for vertex in e1.vertices: r = Point.distance(e1_center, vertex) if e1_max_radius < r: e1_max_radius = r for vertex in e2.vertices: r = Point.distance(e2_center, vertex) if e2_max_radius < r: e2_max_radius = r center_dist = Point.distance(e1_center, e2_center) if center_dist <= e1_max_radius + e2_max_radius: warnings.warn("Polygons may intersect producing erroneous output") ''' Find the upper rightmost vertex of e1 and the lowest leftmost vertex of e2 ''' e1_ymax = (S(0), -oo) e2_ymin = (S(0), oo) for vertex in e1.vertices: if vertex[1] > e1_ymax[1] or (vertex[1] == e1_ymax[1] and vertex[0] > e1_ymax[0]): e1_ymax = vertex for vertex in e2.vertices: if vertex[1] < e2_ymin[1] or (vertex[1] == e2_ymin[1] and vertex[0] < e2_ymin[0]): e2_ymin = vertex min_dist = Point.distance(e1_ymax, e2_ymin) ''' Produce a dictionary with vertices of e1 as the keys and, for each vertex, the points to which the vertex is connected as its value. The same is then done for e2. ''' e1_connections = {} e2_connections = {} for side in e1.sides: if side.p1 in e1_connections: e1_connections[side.p1].append(side.p2) else: e1_connections[side.p1] = [side.p2] if side.p2 in e1_connections: e1_connections[side.p2].append(side.p1) else: e1_connections[side.p2] = [side.p1] for side in e2.sides: if side.p1 in e2_connections: e2_connections[side.p1].append(side.p2) else: e2_connections[side.p1] = [side.p2] if side.p2 in e2_connections: e2_connections[side.p2].append(side.p1) else: e2_connections[side.p2] = [side.p1] e1_current = e1_ymax e2_current = e2_ymin support_line = Line(Point(S(0), S(0)), Point(S(1), S(0))) ''' Determine which point in e1 and e2 will be selected after e2_ymin and e1_ymax, this information combined with the above produced dictionaries determines the path that will be taken around the polygons ''' point1 = e1_connections[e1_ymax][0] point2 = e1_connections[e1_ymax][1] angle1 = support_line.angle_between(Line(e1_ymax, point1)) angle2 = support_line.angle_between(Line(e1_ymax, point2)) if angle1 < angle2: e1_next = point1 elif angle2 < angle1: e1_next = point2 elif Point.distance(e1_ymax, point1) > Point.distance(e1_ymax, point2): e1_next = point2 else: e1_next = point1 point1 = e2_connections[e2_ymin][0] point2 = e2_connections[e2_ymin][1] angle1 = support_line.angle_between(Line(e2_ymin, point1)) angle2 = support_line.angle_between(Line(e2_ymin, point2)) if angle1 > angle2: e2_next = point1 elif angle2 > angle1: e2_next = point2 elif Point.distance(e2_ymin, point1) > Point.distance(e2_ymin, point2): e2_next = point2 else: e2_next = point1 ''' Loop which determins the distance between anti-podal pairs and updates the minimum distance accordingly. It repeats until it reaches the starting position. ''' while True: e1_angle = support_line.angle_between(Line(e1_current, e1_next)) e2_angle = pi - support_line.angle_between( Line(e2_current, e2_next)) if e1_angle < e2_angle: support_line = Line(e1_current, e1_next) e1_segment = Segment(e1_current, e1_next) min_dist_current = e1_segment.distance(e2_current) if min_dist_current.evalf() < min_dist.evalf(): min_dist = min_dist_current if e1_connections[e1_next][0] != e1_current: e1_current = e1_next e1_next = e1_connections[e1_next][0] else: e1_current = e1_next e1_next = e1_connections[e1_next][1] elif e1_angle > e2_angle: support_line = Line(e2_next, e2_current) e2_segment = Segment(e2_current, e2_next) min_dist_current = e2_segment.distance(e1_current) if min_dist_current.evalf() < min_dist.evalf(): min_dist = min_dist_current if e2_connections[e2_next][0] != e2_current: e2_current = e2_next e2_next = e2_connections[e2_next][0] else: e2_current = e2_next e2_next = e2_connections[e2_next][1] else: support_line = Line(e1_current, e1_next) e1_segment = Segment(e1_current, e1_next) e2_segment = Segment(e2_current, e2_next) min1 = e1_segment.distance(e2_next) min2 = e2_segment.distance(e1_next) min_dist_current = min(min1, min2) if min_dist_current.evalf() < min_dist.evalf(): min_dist = min_dist_current if e1_connections[e1_next][0] != e1_current: e1_current = e1_next e1_next = e1_connections[e1_next][0] else: e1_current = e1_next e1_next = e1_connections[e1_next][1] if e2_connections[e2_next][0] != e2_current: e2_current = e2_next e2_next = e2_connections[e2_next][0] else: e2_current = e2_next e2_next = e2_connections[e2_next][1] if e1_current == e1_ymax and e2_current == e2_ymin: break return min_dist
def convex_hull(*args): """The convex hull of a collection of 2-dimensional points. Parameters ---------- args : a collection of Points Returns ------- convex_hull : Polygon Notes ----- This can only be performed on a set of non-symbolic points. See Also -------- Point References ---------- [1] http://en.wikipedia.org/wiki/Graham_scan [2] Andrew's Monotone Chain Algorithm ( A.M. Andrew, "Another Efficient Algorithm for Convex Hulls in Two Dimensions", 1979) http://softsurfer.com/Archive/algorithm_0109/algorithm_0109.htm Examples -------- >>> from sympy.geometry import Point, convex_hull >>> points = [(1,1), (1,2), (3,1), (-5,2), (15,4)] >>> convex_hull(*points) Polygon(Point(-5, 2), Point(1, 1), Point(3, 1), Point(15, 4)) """ from entity import GeometryEntity from point import Point from line import Segment from polygon import Polygon p = set() for e in args: if not isinstance(e, GeometryEntity): try: e = Point(e) except NotImplementedError: raise ValueError('%s is not a GeometryEntity and cannot be made into Point' % str(e)) if isinstance(e, Point): p.add(e) elif isinstance(e, Segment): p.update(e.points) elif isinstance(e, Polygon): p.update(e.vertices) else: raise NotImplementedError('Convex hull for %s not implemented.' % type(e)) p = list(p) if len(p) == 1: return p[0] elif len(p) == 2: return Segment(p[0], p[1]) def orientation(p, q, r): '''Return positive if p-q-r are clockwise, neg if ccw, zero if collinear.''' return (q[1] - p[1])*(r[0] - p[0]) - (q[0] - p[0])*(r[1] - p[1]) # scan to find upper and lower convex hulls of a set of 2d points. U = [] L = [] p.sort() for p_i in p: while len(U) > 1 and orientation(U[-2], U[-1], p_i) <= 0: U.pop() while len(L) > 1 and orientation(L[-2], L[-1], p_i) >= 0: L.pop() U.append(p_i) L.append(p_i) U.reverse() convexHull = tuple(L + U[1:-1]) if len(convexHull) == 2: return Segment(convexHull[0], convexHull[1]) return Polygon(*convexHull)
def __new__(cls, *args, **kwargs): if kwargs.get('n', 0): n = kwargs.pop('n') args = list(args) # return a virtual polygon with n sides if len(args) == 2: # center, radius args.append(n) elif len(args) == 3: # center, radius, rotation args.insert(2, n) return RegularPolygon(*args, **kwargs) vertices = [Point(a) for a in args] # remove consecutive duplicates nodup = [] for p in vertices: if nodup and p == nodup[-1]: continue nodup.append(p) if len(nodup) > 1 and nodup[-1] == nodup[0]: nodup.pop() # last point was same as first # remove collinear points unless they are shared points got = set() shared = set() for p in nodup: if p in got: shared.add(p) else: got.add(p) i = -3 while i < len(nodup) - 3 and len(nodup) > 2: a, b, c = sorted([nodup[i], nodup[i + 1], nodup[i + 2]]) if b not in shared and Point.is_collinear(a, b, c): nodup[i] = a nodup[i + 1] = None nodup.pop(i + 1) i += 1 vertices = filter(lambda x: x is not None, nodup) if len(vertices) > 3: rv = GeometryEntity.__new__(cls, *vertices, **kwargs) elif len(vertices) == 3: return Triangle(*vertices, **kwargs) elif len(vertices) == 2: return Segment(*vertices, **kwargs) else: return Point(*vertices, **kwargs) # reject polygons that have intersecting sides unless the # intersection is a shared point or a generalized intersection. # A self-intersecting polygon is easier to detect than a # random set of segments since only those sides that are not # part of the convex hull can possibly intersect with other # sides of the polygon...but for now we use the n**2 algorithm # and check all sides with intersection with any preceding sides hit = _symbol('hit') if not rv.is_convex: sides = rv.sides for i, si in enumerate(sides): pts = si[0], si[1] ai = si.arbitrary_point(hit) for j in xrange(i): sj = sides[j] if sj[0] not in pts and sj[1] not in pts: aj = si.arbitrary_point(hit) tx = (solve(ai[0] - aj[0]) or [S.Zero])[0] if tx.is_number and 0 <= tx <= 1: ty = (solve(ai[1] - aj[1]) or [S.Zero])[0] if (tx or ty) and ty.is_number and 0 <= ty <= 1: print ai, aj raise GeometryError( "Polygon has intersecting sides.") return rv
def _do_poly_distance(self, e2): """ Calculates the least distance between the exteriors of two convex polygons e1 and e2. Does not check for the convexity of the polygons as it is assumed only called by Polygon.distance which does such checks. Notes ----- - Prints a warning if the two polygons possibly intersect as the return value will not be valid in such a case. For a more through test of intersection use intersection(). Examples ------- >>> from sympy.geometry import Point, Polygon >>> square = Polygon(Point(0, 0), Point(0, 1), Point(1, 1), Point(1, 0)) >>> triangle = Polygon(Point(1, 2), Point(2, 2), Point(2, 1)) >>> square._do_poly_distance(triangle) sqrt(2)/2 Description of method used -------------------------- Method: [1] http://cgm.cs.mcgill.ca/~orm/mind2p.html Uses rotating calipers: [2] http://en.wikipedia.org/wiki/Rotating_calipers and antipodal points: [3] http://en.wikipedia.org/wiki/Antipodal_point """ e1 = self '''Tests for a possible intersection between the polygons and outputs a warning''' e1_center = e1.centroid e2_center = e2.centroid e1_max_radius = S(0) e2_max_radius = S(0) for vertex in e1.vertices: r = Point.distance(e1_center, vertex) if e1_max_radius < r: e1_max_radius = r for vertex in e2.vertices: r = Point.distance(e2_center, vertex) if e2_max_radius < r: e2_max_radius = r center_dist = Point.distance(e1_center, e2_center) if center_dist <= e1_max_radius + e2_max_radius: warnings.warn("Polygons may intersect producing erroneous output") ''' Find the upper rightmost vertex of e1 and the lowest leftmost vertex of e2 ''' e1_ymax = (S(0), -oo) e2_ymin = (S(0), oo) for vertex in e1.vertices: if vertex[1] > e1_ymax[1] or (vertex[1] == e1_ymax[1] and vertex[0] > e1_ymax[0]): e1_ymax = vertex for vertex in e2.vertices: if vertex[1] < e2_ymin[1] or (vertex[1] == e2_ymin[1] and vertex[0] < e2_ymin[0]): e2_ymin = vertex min_dist = Point.distance(e1_ymax, e2_ymin) ''' Produce a dictionary with vertices of e1 as the keys and, for each vertex, the points to which the vertex is connected as its value. The same is then done for e2. ''' e1_connections = {} e2_connections = {} for side in e1.sides: if side.p1 in e1_connections: e1_connections[side.p1].append(side.p2) else: e1_connections[side.p1] = [side.p2] if side.p2 in e1_connections: e1_connections[side.p2].append(side.p1) else: e1_connections[side.p2] = [side.p1] for side in e2.sides: if side.p1 in e2_connections: e2_connections[side.p1].append(side.p2) else: e2_connections[side.p1] = [side.p2] if side.p2 in e2_connections: e2_connections[side.p2].append(side.p1) else: e2_connections[side.p2] = [side.p1] e1_current = e1_ymax e2_current = e2_ymin support_line = Line(Point(S(0), S(0)), Point(S(1), S(0))) ''' Determine which point in e1 and e2 will be selected after e2_ymin and e1_ymax, this information combined with the above produced dictionaries determines the path that will be taken around the polygons ''' point1 = e1_connections[e1_ymax][0] point2 = e1_connections[e1_ymax][1] angle1 = support_line.angle_between(Line(e1_ymax, point1)) angle2 = support_line.angle_between(Line(e1_ymax, point2)) if angle1 < angle2: e1_next = point1 elif angle2 < angle1: e1_next = point2 elif Point.distance(e1_ymax, point1) > Point.distance(e1_ymax, point2): e1_next = point2 else: e1_next = point1 point1 = e2_connections[e2_ymin][0] point2 = e2_connections[e2_ymin][1] angle1 = support_line.angle_between(Line(e2_ymin, point1)) angle2 = support_line.angle_between(Line(e2_ymin, point2)) if angle1 > angle2: e2_next = point1 elif angle2 > angle1: e2_next = point2 elif Point.distance(e2_ymin, point1) > Point.distance(e2_ymin, point2): e2_next = point2 else: e2_next = point1 ''' Loop which determins the distance between anti-podal pairs and updates the minimum distance accordingly. It repeats until it reaches the starting position. ''' while True: e1_angle = support_line.angle_between(Line(e1_current, e1_next)) e2_angle = pi - support_line.angle_between(Line(e2_current, e2_next)) if e1_angle < e2_angle: support_line = Line(e1_current, e1_next) e1_segment = Segment(e1_current, e1_next) min_dist_current = e1_segment.distance(e2_current) if min_dist_current.evalf() < min_dist.evalf(): min_dist = min_dist_current if e1_connections[e1_next][0] != e1_current: e1_current = e1_next e1_next = e1_connections[e1_next][0] else: e1_current = e1_next e1_next = e1_connections[e1_next][1] elif e1_angle > e2_angle: support_line = Line(e2_next, e2_current) e2_segment = Segment(e2_current, e2_next) min_dist_current = e2_segment.distance(e1_current) if min_dist_current.evalf() < min_dist.evalf(): min_dist = min_dist_current if e2_connections[e2_next][0] != e2_current: e2_current = e2_next e2_next = e2_connections[e2_next][0] else: e2_current = e2_next e2_next = e2_connections[e2_next][1] else: support_line = Line(e1_current, e1_next) e1_segment = Segment(e1_current, e1_next) e2_segment = Segment(e2_current, e2_next) min1 = e1_segment.distance(e2_next) min2 = e2_segment.distance(e1_next) min_dist_current = min(min1, min2) if min_dist_current.evalf() < min_dist.evalf(): min_dist = min_dist_current if e1_connections[e1_next][0] != e1_current: e1_current = e1_next e1_next = e1_connections[e1_next][0] else: e1_current = e1_next e1_next = e1_connections[e1_next][1] if e2_connections[e2_next][0] != e2_current: e2_current = e2_next e2_next = e2_connections[e2_next][0] else: e2_current = e2_next e2_next = e2_connections[e2_next][1] if e1_current == e1_ymax and e2_current == e2_ymin: break return min_dist
def convex_hull(*args): """ Returns a Polygon representing the convex hull of a set of 2D points. Notes: ====== This can only be performed on a set of non-symbolic points. Example: ======== >>> from sympy.geometry import Point >>> points = [ Point(x) for x in [(1,1), (1,2), (3,1), (-5,2), (15,4)] ] >>> convex_hull(points) Polygon(Point(3, 1), Point(15, 4), Point(-5, 2), Point(1, 1)) Description of method used: =========================== See http://en.wikipedia.org/wiki/Graham_scan. """ from point import Point from line import Segment from polygon import Polygon p = args[0] if isinstance(p, Point): p = args # Basic checks if len(p) == 1: return p[0] elif len(p) == 2: return Segment(p[0], p[1]) # Find lowest+rightmost point m = 0 for i in xrange(1, len(p)): if (p[i][1] < p[m][1]) or ((p[i][1] == p[m][1]) and (p[i][0] > p[m][0])): m = i p[0], p[m] = p[m], p[0] def tarea(a, b, c): return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]) # Radial sort of points with respect to p[0] (our pivot) destroy = {} p0 = p[0] def pcompare(p1, p2): a = tarea(p0, p1, p2) if a > 0: return -1 elif a < 0: return 1 else: x = abs(p1[0] - p0[0]) - abs(p2[0] - p0[0]) y = abs(p1[1] - p0[1]) - abs(p2[1] - p0[1]) if (x < 0) or (y < 0): destroy[p1] = True return -1 elif (x > 0) or (y > 0): destroy[p2] = True return 1 else: destroy[p1] = True return 0 p = p[1:] p.sort(pcompare) p.insert(0, p0) # Destroy points as found by sorting for i in xrange(len(p) - 1, -1, -1): if p[i] in destroy: del p[i] # Graham scan def isleft(a, b, c): return (tarea(a, b, c) > 0) top = [p[0], p[1]] i = 2 while i < len(p): p1 = top[-2] p2 = top[-1] if isleft(p1, p2, p[i]): top.append(p[i]) i += 1 else: top.pop() return Polygon(top)