Source code for ladybug_geometry.geometry2d.polyline

# coding=utf-8
"""2D Polyline"""
from __future__ import division
import math

from ._2d import Base2DIn2D
from .pointvector import Point2D
from .line import LineSegment2D
from ..intersection2d import intersect_line2d, intersect_line2d_infinite
from .._polyline import _group_vertices


[docs]class Polyline2D(Base2DIn2D): """2D polyline object. Args: vertices: A list of Point2D objects representing the vertices of the polyline. interpolated: Boolean to note whether the polyline should be interpolated between the input vertices when it is translated to other interfaces. Note that this property has no bearing on the geometric calculations performed by this library and is only present in order to assist with display/translation. Properties: * vertices * segments * min * max * center * p1 * p2 * length * is_self_intersecting * interpolated """ __slots__ = ('_interpolated', '_segments', '_length', '_is_self_intersecting') def __init__(self, vertices, interpolated=False): """Initialize Polyline2D.""" Base2DIn2D.__init__(self, vertices) self._interpolated = interpolated self._segments = None self._length = None self._is_self_intersecting = None
[docs] @classmethod def from_dict(cls, data): """Create a Polyline2D from a dictionary. Args: data: A python dictionary in the following format. .. code-block:: python { "type": "Polyline2D", "vertices": [(0, 0), (10, 0), (0, 10)] } """ interp = data['interpolated'] if 'interpolated' in data else False return cls(tuple(Point2D.from_array(pt) for pt in data['vertices']), interp)
[docs] @classmethod def from_array(cls, point_array): """Create a Polyline2D from a nested array of vertex coordinates. Args: point_array: Nested array of point arrays. """ return Polyline2D(Point2D(*point) for point in point_array)
[docs] @classmethod def from_polygon(cls, polygon): """Create a closed Polyline2D from a Polygon2D. Args: polygon: A Polygon2D object to be converted to a Polyline2D. """ return Polyline2D(polygon.vertices + (polygon.vertices[0],))
@property def segments(self): """Tuple of all line segments in the polyline.""" if self._segments is None: self._segments = \ tuple(LineSegment2D.from_end_points(vert, self._vertices[i + 1]) for i, vert in enumerate(self._vertices[:-1])) return self._segments @property def length(self): """The length of the polyline.""" if self._length is None: self._length = sum([seg.length for seg in self.segments]) return self._length @property def p1(self): """Starting point of the Polyline2D.""" return self._vertices[0] @property def p2(self): """End point of the Polyline2D.""" return self._vertices[-1] @property def is_self_intersecting(self): """Boolean noting whether the polyline has self-intersecting segments.""" if self._is_self_intersecting is None: self._is_self_intersecting = False _segs = self.segments for i, _s in enumerate(_segs[1: len(_segs) - 1]): _skip = (i, i + 1, i + 2) _other_segs = [x for j, x in enumerate(_segs) if j not in _skip] for _oth_s in _other_segs: if _s.intersect_line_ray(_oth_s) is not None: # intersection! self._is_self_intersecting = True break if self._is_self_intersecting is True: break return self._is_self_intersecting @property def interpolated(self): """Boolean noting whether the polyline should be interpolated upon translation. Note that this property has no bearing on the geometric calculations performed by this library and is only present in order to assist with display/translation. """ return self._interpolated
[docs] def is_closed(self, tolerance): """Test whether this polyline is closed to within the tolerance. Args: tolerance: The minimum difference between vertices below which vertices are considered the same. """ return self._vertices[0].is_equivalent(self._vertices[-1], tolerance)
[docs] def remove_colinear_vertices(self, tolerance): """Get a version of this polyline without colinear or duplicate vertices. Args: tolerance: The minimum distance that a vertex can be from a line before it is considered colinear. """ if len(self.vertices) == 3: return self # Polyline2D cannot have fewer than 3 vertices new_vertices = [self.vertices[0]] # first vertex is always ok skip = 0 # track the number of vertices being skipped/removed # loop through vertices and remove all cases of colinear verts for i, _v in enumerate(self.vertices[1:-1]): _a = self[i - skip].determinant(_v) + _v.determinant(self[i + 2]) + \ self[i + 2].determinant(self[i - skip]) if abs(_a) >= tolerance: new_vertices.append(_v) skip = 0 else: skip += 1 new_vertices.append(self[-1]) # last vertex is always ok _new_poly = Polyline2D(new_vertices) self._transfer_properties(_new_poly) return _new_poly
[docs] def reverse(self): """Get a copy of this polyline where the vertices are reversed.""" _new_poly = Polyline2D(tuple(pt for pt in reversed(self.vertices))) self._transfer_properties(_new_poly) return _new_poly
[docs] def move(self, moving_vec): """Get a polyline that has been moved along a vector. Args: moving_vec: A Vector2D with the direction and distance to move the polyline. """ _new_poly = Polyline2D(tuple(pt.move(moving_vec) for pt in self.vertices)) self._transfer_properties(_new_poly) return _new_poly
[docs] def rotate(self, angle, origin): """Get a polyline that is rotated counterclockwise by a certain angle. Args: angle: An angle for rotation in radians. origin: A Point2D for the origin around which the point will be rotated. """ _new_poly = Polyline2D(tuple(pt.rotate(angle, origin) for pt in self.vertices)) self._transfer_properties(_new_poly) return _new_poly
[docs] def reflect(self, normal, origin): """Get a polyline reflected across a plane with the input normal and origin. Args: normal: A Vector2D representing the normal vector for the plane across which the polyline will be reflected. THIS VECTOR MUST BE NORMALIZED. origin: A Point2D representing the origin from which to reflect. """ _new_poly = Polyline2D(tuple(pt.reflect(normal, origin) for pt in self.vertices)) self._transfer_properties(_new_poly) return _new_poly
[docs] def scale(self, factor, origin=None): """Scale a polyline by a factor from an origin point. Args: factor: A number representing how much the polyline should be scaled. origin: A Point2D representing the origin from which to scale. If None, it will be scaled from the World origin (0, 0). """ if origin is None: _new_poly = Polyline2D(tuple( Point2D(pt.x * factor, pt.y * factor) for pt in self.vertices)) else: _new_poly = Polyline2D(tuple( pt.scale(factor, origin) for pt in self.vertices)) _new_poly._interpolated = self._interpolated return _new_poly
[docs] def offset(self, distance, check_intersection=False): """Offset the polyline by a given distance. Note that the resulting shape may be self-intersecting if the distance is large enough and the is_self_intersecting property may be used to identify these shapes. Args: distance: The distance that the polyline will be offset. Both positive and negative values are accepted with positive values being offset to the left of the polyline line and negative values being offset to the right of the polyline (starting from the first polyline point and continuing down the polyline). check_intersection: A boolean to note whether the resulting operation should be checked for self intersection and, if so, None will be returned instead of the self-intersecting polyline. """ # make sure the offset is not zero if distance == 0: return self # loop through the vertices and get the new offset vectors middle_verts = list(self._vertices[1:-1]) if len(middle_verts) != 1: middle_verts = [pt for i, pt in enumerate(middle_verts) if pt != middle_verts[i - 1]] all_verts = [self._vertices[0]] + middle_verts + [self._vertices[-1]] move_vec_st = self.segments[0].v.rotate(math.pi / 2).normalize() * distance move_vecs = [move_vec_st] for i, pt in enumerate(middle_verts): v1 = all_verts[i] - pt v2 = all_verts[i + 2] - pt ang = v1.angle_counterclockwise(v2) / 2 if ang == 0: ang = math.pi / 2 m_vec = v1.rotate(ang).normalize() m_dist = -distance / math.sin(ang) m_vec = m_vec * m_dist move_vecs.append(m_vec) move_vec_end = self.segments[-1].v.rotate(math.pi / 2).normalize() * distance move_vecs.append(move_vec_end) # move the vertices by the offset to create the new Polygon2D new_pts = tuple(pt.move(m_vec) for pt, m_vec in zip(all_verts, move_vecs)) new_poly = Polyline2D(new_pts, self.interpolated) # check for self intersection between the moving vectors if requested if check_intersection: poly_segs = new_poly.segments _segs = [LineSegment2D(p, v) for p, v in zip(all_verts, move_vecs)] _skip = (0, len(_segs) - 1) _other_segs = [x for j, x in enumerate(poly_segs) if j not in _skip] for _oth_s in _other_segs: if _segs[0].intersect_line_ray(_oth_s) is not None: # intersection! return None for i, _s in enumerate(_segs[1: len(_segs)]): _skip = (i, i + 1) _other_segs = [x for j, x in enumerate(poly_segs) if j not in _skip] for _oth_s in _other_segs: if _s.intersect_line_ray(_oth_s) is not None: # intersection! return None return new_poly
[docs] def intersect_line_ray(self, line_ray): """Get the intersections between this polyline and a Ray2D or LineSegment2D. Args: line_ray: A LineSegment2D or Ray2D or to intersect. Returns: A list with Point2D objects for the intersections. List will be empty if no intersection exists. """ intersections = [] for _s in self.segments: inters = intersect_line2d(_s, line_ray) if inters is not None: intersections.append(inters) return intersections
[docs] def intersect_line_infinite(self, ray): """Get the intersections between this polyline and a Ray2D extended infinitely. Args: ray: A Ray2D or to intersect. This will be extended in both directions infinitely for the intersection. Returns: A list with Point2D objects for the intersections. List will be empty if no intersection exists. """ intersections = [] for _s in self.segments: inters = intersect_line2d_infinite(_s, ray) if inters is not None: intersections.append(inters) return intersections
[docs] def to_dict(self): """Get Polyline2D as a dictionary.""" base = {'type': 'Polyline2D', 'vertices': [pt.to_array() for pt in self.vertices]} if self.interpolated: base['interpolated'] = self.interpolated return base
[docs] def to_array(self): """Get a list of lists where each sub-list represents a Point2D vertex.""" return tuple(pt.to_array() for pt in self.vertices)
[docs] def to_polygon(self, tolerance): """Get a Polygon2D derived from this object. If the polyline is closed to within the tolerance, the segments of this polyline and the resulting polygon will match. Otherwise, an extra LineSegment2D will be added to connect the start and end of the polyline. Args: tolerance: The minimum difference between vertices below which vertices are considered the same. """ from .polygon import Polygon2D # must be imported here to avoid circular import if self.is_closed(tolerance): return Polygon2D(self._vertices[:-1]) return Polygon2D(self._vertices)
[docs] @staticmethod def join_segments(segments, tolerance): """Get an array of Polyline2Ds from a list of LineSegment2Ds. Args: segments: An array of LineSegment2D objects. tolerance: The minimum difference in X, Y, and Z values at which Point2Ds are considered equivalent. Segments with points that match within the tolerance will be joined. Returns: An array of Polyline2D and LineSegment2D objects assembled from the joined segments. """ # group the vertices that make up polylines grouped_verts = _group_vertices(segments, tolerance) # create the Polyline2D and LineSegment2D objects joined_lines = [] for v_list in grouped_verts: if len(v_list) == 2: joined_lines.append(LineSegment2D.from_end_points(v_list[0], v_list[1])) else: joined_lines.append(Polyline2D(v_list)) return joined_lines
def _transfer_properties(self, new_polyline): """Transfer properties from this polyline to a new polyline.""" new_polyline._interpolated = self._interpolated new_polyline._length = self._length new_polyline._is_self_intersecting = self._is_self_intersecting def __copy__(self): return Polyline2D(self._vertices, self._interpolated) def __key(self): """A tuple based on the object properties, useful for hashing.""" return tuple(hash(pt) for pt in self._vertices) + (self._interpolated,) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, Polyline2D) and self.__key() == other.__key() def __repr__(self): return 'Polyline2D ({} vertices)'.format(len(self))