Source code for dragonfly.skylightparameter

# coding: utf-8
"""Skylight Parameters with instructions for generating skylights."""
from __future__ import division
import math
import sys
if (sys.version_info < (3, 0)):  # python 2
    from itertools import izip as zip  # python 2

from ladybug_geometry.geometry2d import Point2D, Vector2D, Polygon2D
from ladybug_geometry.geometry3d import Vector3D, Point3D, Face3D
from ladybug_geometry.bounding import bounding_rectangle

from honeybee.typing import float_in_range, float_positive
from honeybee.altnumber import autocalculate
from honeybee.aperture import Aperture
from honeybee.door import Door


class _SkylightParameterBase(object):
    """Base object for all Skylight parameters.

    This object records the minimum number of the methods that must be overwritten
    on a skylight parameter object for it to be successfully be applied in
    dragonfly workflows.
    """
    __slots__ = ()

    def __init__(self):
        pass

    def area_from_face(self, face):
        """Get the skylight area generated by these parameters from a Room2D Face3D."""
        return 0

    def add_skylight_to_face(self, face, tolerance=0.01):
        """Add Apertures to a Honeybee Roof Face using these Skylight Parameters."""
        pass

    def scale(self, factor):
        """Get a scaled version of these SkylightParameters.

        This method is called within the scale methods of the Room2D.

        Args:
            factor: A number representing how much the object should be scaled.
        """
        return self

    def split(self, face_3ds, tolerance=0.01):
        """Split these skylight parameters across several Face3D.

        This is useful when the Room2D to which the skylight is assigned has been
        split into multiple Room2Ds.

        Args:
            face_3ds: A list of horizontal Face3D representing the floor geometries
                of a Room2D to which these skylight parameters are split across.
            tolerance: The maximum difference between point values for them to be
                considered distinct. (Default: 0.01, suitable for objects in meters).
        """
        return self  # assume even distribution by default

    @classmethod
    def from_dict(cls, data):
        """Create SkylightParameterBase from a dictionary.

        .. code-block:: python

            {
            "type": "SkylightParameterBase"
            }
        """
        assert data['type'] == 'SkylightParameterBase', \
            'Expected SkylightParameterBase dictionary. Got {}.'.format(data['type'])
        return cls()

    def to_dict(self):
        """Get SkylightParameterBase as a dictionary."""
        return {'type': 'SkylightParameterBase'}

    def duplicate(self):
        """Get a copy of this object."""
        return self.__copy__()

    def ToString(self):
        return self.__repr__()

    def __copy__(self):
        return _SkylightParameterBase()

    def __repr__(self):
        return 'SkylightParameterBase'


[docs] class GriddedSkylightArea(_SkylightParameterBase): """Instructions for gridded skylights defined by an absolute area. Args: skylight_area: A number for the skylight area in current model units. If this area is larger than the area of the roof that it is applied to, the skylight will fill the parent roof at a 99% ratio. spacing: A number for the spacing between the centers of each grid cell. This should be less than a third of the dimension of the Roof geometry if multiple, evenly-spaced skylights are desired. If None or Autocalculate, a spacing of one third the smaller dimension of the parent Roof will be automatically assumed. (Default: Autocalculate). Properties: * skylight_area * spacing """ __slots__ = ('_skylight_area', '_spacing') def __init__(self, skylight_area, spacing=autocalculate): """Initialize GriddedSkylightArea.""" self._skylight_area = float_positive(skylight_area, 'skylight area') if spacing == autocalculate: spacing = None elif spacing is not None: spacing = float_positive(spacing, 'skylight spacing') assert spacing > 0, 'GriddedSkylightArea spacing must be greater than zero.' self._spacing = spacing @property def skylight_area(self): """Get a number for the skylight area in current model units.""" return self._skylight_area @property def spacing(self): """Get a number or the spacing between the skylights. None indicates that the spacing will always be one third of the smaller dimension of the parent Roof. """ return self._spacing
[docs] def area_from_face(self, face): """Get the skylight area generated by these parameters from a Room2D Face3D. Args: face: A Roof Face3D to which these parameters are applied. """ return self._skylight_area
[docs] def add_skylight_to_face(self, face, tolerance=0.01): """Add Apertures to a Honeybee Roof Face using these Skylight Parameters. Args: face: A honeybee-core Face object. tolerance: The maximum difference between point values for them to be considered distinct. (Default: 0.01, suitable for objects in meters). """ if self._skylight_area == 0: return None spacing = self.spacing if self.spacing is None: min_pt, max_pt = face.min, face.max min_dim = min(max_pt.x - min_pt.x, max_pt.y - min_pt.y) spacing = (min_dim / 3) - tolerance skylight_ratio = self.skylight_area / face.area skylight_ratio = 0.99 if skylight_ratio > 0.99 else skylight_ratio face.apertures_by_ratio_gridded( skylight_ratio, spacing, tolerance=tolerance)
[docs] def scale(self, factor): """Get a scaled version of these SkylightParameters. This method is called within the scale methods of the Room2D. Args: factor: A number representing how much the object should be scaled. """ spc = self.spacing * factor if self.spacing is not None else None return GriddedSkylightArea(self.skylight_area * factor, spc)
[docs] def split(self, face_3ds, tolerance=0.01): """Split these skylight parameters across several Face3D. This is useful when the Room2D to which the skylight is assigned has been split into multiple Room2Ds. Args: face_3ds: A list of horizontal Face3D representing the floor geometries of a Room2D to which these skylight parameters are split across. tolerance: The maximum difference between point values for them to be considered distinct. (Default: 0.01, suitable for objects in meters). """ total_area = sum(face.area for face in face_3ds) return [GriddedSkylightArea(self.area * (face.area / total_area), self.spacing) for face in face_3ds]
[docs] @classmethod def from_dict(cls, data): """Create GriddedSkylightArea from a dictionary. .. code-block:: python { "type": "GriddedSkylightArea", "skylight_area": 2.5, "spacing": 2 } """ assert data['type'] == 'GriddedSkylightArea', \ 'Expected GriddedSkylightArea dictionary. Got {}.'.format(data['type']) spc = data['spacing'] if 'spacing' in data and \ data['spacing'] != autocalculate.to_dict() else None return cls(data['skylight_area'], spc)
[docs] def to_dict(self): """Get GriddedSkylightArea as a dictionary.""" base = {'type': 'GriddedSkylightArea', 'skylight_area': self.skylight_area} if self.spacing is not None: base['spacing'] = self.spacing return base
def __copy__(self): return GriddedSkylightArea( self.skylight_area, self.spacing) def __key(self): """A tuple based on the object properties, useful for hashing.""" return (self.skylight_area, self.spacing) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, GriddedSkylightArea) and self.__key() == other.__key() def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return 'GriddedSkylightArea: [area: {}]'.format(self.skylight_area)
[docs] class GriddedSkylightRatio(_SkylightParameterBase): """Instructions for gridded skylights derived from an area ratio with the roof. Args: skylight_ratio: A number between 0 and 1 for the ratio between the skylight area and the total Roof face area. spacing: A number for the spacing between the centers of each grid cell. This should be less than a third of the dimension of the Roof geometry if multiple, evenly-spaced skylights are desired. If None or Autocalculate, a spacing of one third the smaller dimension of the parent Roof will be automatically assumed. (Default: Autocalculate). Properties: * skylight_ratio * spacing """ __slots__ = ('_skylight_ratio', '_spacing') def __init__(self, skylight_ratio, spacing=autocalculate): """Initialize GriddedSkylightRatio.""" self._skylight_ratio = float_in_range(skylight_ratio, 0, 1.0, 'skylight ratio') if spacing == autocalculate: spacing = None elif spacing is not None: spacing = float_positive(spacing, 'skylight spacing') assert spacing > 0, 'GriddedSkylightRatio spacing must be greater than zero.' self._spacing = spacing @property def skylight_ratio(self): """Get a number between 0 and 1 for the skylight ratio.""" return self._skylight_ratio @property def spacing(self): """Get a number or the spacing between the skylights. None indicates that the spacing will always be one third of the smaller dimension of the parent Roof. """ return self._spacing
[docs] def area_from_face(self, face): """Get the skylight area generated by these parameters from a Room2D Face3D. Args: face: A Roof Face3D to which these parameters are applied. """ return face.area * self._skylight_ratio
[docs] def add_skylight_to_face(self, face, tolerance=0.01): """Add Apertures to a Honeybee Roof Face using these Skylight Parameters. Args: face: A honeybee-core Face object. tolerance: The maximum difference between point values for them to be considered distinct. (Default: 0.01, suitable for objects in meters). """ if self._skylight_ratio == 0: return None spacing = self.spacing if self.spacing is None: min_pt, max_pt = face.min, face.max min_dim = min(max_pt.x - min_pt.x, max_pt.y - min_pt.y) spacing = (min_dim / 3) - tolerance face.apertures_by_ratio_gridded( self.skylight_ratio, spacing, tolerance=tolerance)
[docs] def scale(self, factor): """Get a scaled version of these SkylightParameters. This method is called within the scale methods of the Room2D. Args: factor: A number representing how much the object should be scaled. """ spc = self.spacing * factor if self.spacing is not None else None return GriddedSkylightRatio(self.skylight_ratio, spc)
[docs] @classmethod def from_dict(cls, data): """Create GriddedSkylightRatio from a dictionary. .. code-block:: python { "type": "GriddedSkylightRatio", "skylight_ratio": 0.05, "spacing": 2 } """ assert data['type'] == 'GriddedSkylightRatio', \ 'Expected GriddedSkylightRatio dictionary. Got {}.'.format(data['type']) spc = data['spacing'] if 'spacing' in data and \ data['spacing'] != autocalculate.to_dict() else None return cls(data['skylight_ratio'], spc)
[docs] def to_dict(self): """Get GriddedSkylightRatio as a dictionary.""" base = {'type': 'GriddedSkylightRatio', 'skylight_ratio': self.skylight_ratio} if self.spacing is not None: base['spacing'] = self.spacing return base
def __copy__(self): return GriddedSkylightRatio( self.skylight_ratio, self.spacing) def __key(self): """A tuple based on the object properties, useful for hashing.""" return (self.skylight_ratio, self.spacing) def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, GriddedSkylightRatio) and self.__key() == other.__key() def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return 'GriddedSkylightRatio: [ratio: {}]'.format(self.skylight_ratio)
[docs] class DetailedSkylights(_SkylightParameterBase): """Instructions for detailed skylights, defined by 2D Polygons (lists of 2D vertices). Note that these parameters are intended to represent skylights that are specific to a particular Room2D and, unlike the other SkylightsParameters, this class performs no automatic checks to ensure that the skylights lie within the boundary of the Roof that they are assigned to. Args: polygons: An array of ladybug_geometry Polygon2D objects in world XY coordinates with one polygon for each skylight. These coordinate values should lie within the Room2D's Polygon2D. are_doors: An array of booleans that align with the polygons and note whether each of the polygons represents an overhead door (True) or a skylight (False). If None, it will be assumed that all polygons represent skylights and they will be translated to Apertures in any resulting Honeybee model. (Default: None). Properties: * polygons * are_doors """ __slots__ = ('_polygons', '_are_doors') def __init__(self, polygons, are_doors=None): """Initialize DetailedSkylights.""" if not isinstance(polygons, tuple): polygons = tuple(polygons) for polygon in polygons: assert isinstance(polygon, Polygon2D), \ 'Expected Polygon2D for skylight polygon. Got {}'.format(type(polygon)) assert len(polygons) != 0, \ 'There must be at least one polygon to use DetailedSkylights.' self._polygons = polygons if are_doors is None: self._are_doors = (False,) * len(polygons) else: if not isinstance(are_doors, tuple): are_doors = tuple(are_doors) for is_dr in are_doors: assert isinstance(is_dr, bool), 'Expected booleans for ' \ 'DetailedSkylights.are_doors. Got {}'.format(type(is_dr)) assert len(are_doors) == len(polygons), \ 'Length of DetailedSkylights.are_doors ({}) does not match the length ' \ 'of DetailedSkylights.polygons ({}).'.format( len(are_doors), len(polygons)) self._are_doors = are_doors @property def polygons(self): """Get an array of Polygon2Ds with one polygon for each skylight.""" return self._polygons @property def are_doors(self): """Get an array of booleans that note whether each polygon is a door.""" return self._are_doors
[docs] def area_from_face(self, face): """Get the skylight area generated by these parameters from a Room2D Face3D. Args: face: A Roof Face3D to which these parameters are applied. """ return sum(polygon.area for polygon in self._polygons)
[docs] def check_overlaps(self, tolerance=0.01): """Check whether any polygons overlap with one another. Args: tolerance: The minimum distance that two polygons must overlap in order for them to be considered overlapping and invalid. (Default: 0.01, suitable for objects in meters). Returns: A string with the message. Will be an empty string if valid. """ # group the polygons according to their overlaps grouped_polys = Polygon2D.group_by_overlap(self.polygons, tolerance) # report any polygons that overlap if not all(len(g) == 1 for g in grouped_polys): base_msg = '({} skylights overlap one another)' all_msg = [] for p_group in grouped_polys: if len(p_group) != 1: all_msg.append(base_msg.format(len(p_group))) return ' '.join(all_msg) return ''
[docs] def check_self_intersecting(self, tolerance=0.01): """Check whether any polygons in these skylight parameters are self intersecting. Args: tolerance: The minimum distance between a vertex coordinates where they are considered equivalent. (Default: 0.01, suitable for objects in meters). Returns: A string with the message. Will be an empty string if valid. """ self_int_i = [] for i, polygon in enumerate(self.polygons): if polygon.is_self_intersecting: try: new_geo = polygon.remove_colinear_vertices(tolerance) if new_geo.is_self_intersecting: self_int_i.append(str(i)) except AssertionError: self_int_i.append(str(i)) if len(self_int_i) != 0: return 'Skylight polygons with the following indices are ' \ 'self-intersecting: ({})'.format(' '.join(self_int_i)) return ''
[docs] def check_valid_for_face(self, face): """Check that these skylight parameters are valid for a given Face3D. Args: face: A Roof Face3D to which these parameters are applied. Returns: A string with the message. Will be an empty string if valid. """ # first check that the total skylight area isn't larger than the roof total_area = face.area win_area = self.area_from_face(face) if win_area >= total_area: return 'Total area of skylights [{}] is greater than the area of the ' \ 'parent roof [{}].'.format(win_area, total_area) # next, check to be sure that no skylight is out of the roof boundary msg_template = 'Skylight polygon {} is outside the range allowed ' \ 'by the parent roof.' verts2d = tuple(Point2D(pt.x, pt.y) for pt in face.boundary) parent_poly, parent_holes = Polygon2D(verts2d), None if face.has_holes: parent_holes = tuple( Polygon2D(Point2D(pt.x, pt.y) for pt in hole) for hole in face.holes ) for i, p_gon in enumerate(self.polygons): if not self._is_sub_polygon(p_gon, parent_poly, parent_holes): return msg_template.format(i) return ''
[docs] def add_skylight_to_face(self, face, tolerance=0.01): """Add Apertures to a Honeybee Roof Face using these Skylight Parameters. Args: face: A honeybee-core Face object. tolerance: The maximum difference between point values for them to be considered distinct. (Default: 0.01, suitable for objects in meters). """ # get the polygons that represent the roof face verts2d = tuple(Point2D(pt.x, pt.y) for pt in face.geometry.boundary) parent_poly, parent_holes = Polygon2D(verts2d), None if face.geometry.has_holes: parent_holes = tuple( Polygon2D(Point2D(pt.x, pt.y) for pt in hole) for hole in face.geometry.holes ) # loop through each polygon and create its geometry sub_count = 1 p_dir = Vector3D(0, 0, 1) for polygon, isd in zip(self.polygons, self.are_doors): poly_relation = self._sub_poly_relation( polygon, parent_poly, parent_holes, tolerance) if poly_relation == -1: continue pt3d = tuple( face.geometry.plane.project_point(Point3D(p.x, p.y, 0), p_dir) for p in polygon) if None in pt3d: continue s_geos = [Face3D(pt3d)] if poly_relation == 0: # find the boolean difference par_geo = face.geometry bool_int = Face3D.coplanar_intersection( par_geo, s_geos[0], tolerance, math.radians(1)) if bool_int is None or len(bool_int) == 0: # boolean intersection failed continue # offset the result of the boolean intersection from the edge s_geos = [] for res in bool_int: new_pts = [res.plane.xy_to_xyz(pt2) for pt2 in res.boundary_polygon2d.offset(tolerance * 2)] s_geos.append(Face3D(new_pts)) for s_geo in s_geos: if isd: sub_f = Door('{}_Door{}'.format(face.identifier, sub_count), s_geo) face.add_door(sub_f) else: sub_f = Aperture('{}_Glz{}'.format(face.identifier, sub_count), s_geo) face.add_aperture(sub_f) sub_count += 1
[docs] def move(self, moving_vec): """Get this DetailedSkylights moved along a vector. Args: moving_vec: A Vector3D with the direction and distance to move the polygon. """ vec2 = Vector2D(moving_vec.x, moving_vec.y) return DetailedSkylights( tuple(polygon.move(vec2) for polygon in self.polygons), self.are_doors)
[docs] def scale(self, factor, origin=None): """Get a scaled version of these DetailedSkylights. This method is called within the scale methods of the Room2D. Args: factor: A number representing how much the object should be scaled. origin: A ladybug_geometry Point3D representing the origin from which to scale. If None, it will be scaled from the World origin (0, 0, 0). """ ori = Point2D(origin.x, origin.y) if origin is not None else None return DetailedSkylights( tuple(polygon.scale(factor, ori) for polygon in self.polygons), self.are_doors)
[docs] def rotate(self, angle, origin): """Get these DetailedSkylights rotated counterclockwise in the XY plane. Args: angle: An angle in degrees. origin: A ladybug_geometry Point3D for the origin around which the object will be rotated. """ ori, ang = Point2D(origin.x, origin.y), math.radians(angle) return DetailedSkylights( tuple(polygon.rotate(ang, ori) for polygon in self.polygons), self.are_doors)
[docs] def reflect(self, plane): """Get a reflected version of these DetailedSkylights across a plane. Args: plane: A ladybug_geometry Plane across which the object will be reflected. """ # get the plane normal and origin in 2D space normal = Vector2D(plane.n.x, plane.n.y) origin = Point2D(plane.o.x, plane.o.y) # loop through the polygons and reflect them across the plane new_polygons = [] for polygon in self.polygons: new_verts = tuple(pt.reflect(normal, origin) for pt in polygon.vertices) new_polygons.append(Polygon2D(tuple(reversed(new_verts)))) return DetailedSkylights(new_polygons, self.are_doors)
[docs] def split(self, face_3ds, tolerance=0.01): """Split these skylight parameters across several Face3D. This is useful when the Room2D to which the skylight is assigned has been split into multiple Room2Ds. Args: face_3ds: A list of horizontal Face3D representing the floor geometries of a Room2D to which these skylight parameters are split across. tolerance: The maximum difference between point values for them to be considered distinct. (Default: 0.01, suitable for objects in meters). """ # loop through the polygons and build up new skylight parameters new_sky_par = [] for face_3d in face_3ds: verts2d = tuple(Point2D(pt.x, pt.y) for pt in face_3d.boundary) room_poly = Polygon2D(verts2d) rel_poly, rel_doors, offset_needed = [], [], False for sky_poly, isd in zip(self.polygons, self.are_doors): poly_rel = room_poly.polygon_relationship(sky_poly, tolerance) if poly_rel >= 0: rel_poly.append(sky_poly) rel_doors.append(isd) if poly_rel == 0: offset_needed = True if len(rel_poly) == 0: new_sky_par.append(None) continue new_sky = DetailedSkylights(rel_poly, rel_doors) if offset_needed: new_sky.offset_polygons_for_face(face_3d, tolerance, tolerance) new_sky_par.append(new_sky) return new_sky_par
[docs] def offset(self, offset_distance, tolerance=0.01): """Offset the edges of all skylight polygons by a certain distance. This is useful for translating between interfaces that expect the window frame to be included within or excluded from the geometry. Note that this operation can often create polygons that collide with one another or extend past the parent Face. So it may be desirable to run the union_overlaps method after using this one. Args: offset_distance: Distance with which the edges of each polygon will be offset from the original geometry. Positive values will offset the geometry outwards and negative values will offset the geometries inwards. tolerance: The minimum difference between point values for them to be considered the distinct. (Default: 0.01, suitable for objects in meters). """ offset_polys, offset_are_doors = [], [] for polygon, isd in zip(self.polygons, self.are_doors): try: poly = polygon.remove_colinear_vertices(tolerance) off_poly = poly.offset(-offset_distance, False) if not off_poly.is_self_intersecting: offset_polys.append(off_poly) else: # polygon became self-intersecting after offset offset_polys.append(poly) offset_are_doors.append(isd) except AssertionError: # degenerate window to ignore pass self._polygons = tuple(offset_polys) self._are_doors = tuple(offset_are_doors)
[docs] def offset_polygons_for_face(self, face_3d, offset_distance=0.05, tolerance=0.01): """Offset the polygons until all vertices are inside the boundaries of a Face3D. Args: face_3d: A horizontal Face3D representing the floor geometry of a Room2D to which these skylight parameters are assigned. offset_distance: Distance from the edge of the face_3d that the polygons will be offset to. (Default: 0.05, suitable for objects in meters). tolerance: The maximum difference between point values for them to be considered distinct. (Default: 0.01, suitable for objects in meters). """ # get the polygons that represent the roof face face_z = face_3d[0].z verts2d = tuple(Point2D(pt.x, pt.y) for pt in face_3d.boundary) parent_poly, parent_holes = Polygon2D(verts2d), None if face_3d.has_holes: parent_holes = tuple( Polygon2D(Point2D(pt.x, pt.y) for pt in hole) for hole in face_3d.holes ) # loop through the polygons and offset them if they are not correctly bounded new_polygons, new_are_doors = [], [] for polygon, isd in zip(self.polygons, self.are_doors): if not self._is_sub_polygon(polygon, parent_poly, parent_holes): # find the boolean intersection of the polygon with the room sub_face = Face3D([Point3D.from_point2d(pt, face_z) for pt in polygon]) bool_int = Face3D.coplanar_intersection( face_3d, sub_face, tolerance, math.radians(1)) if bool_int is None: # skylight completely outside parent continue # offset the result of the boolean intersection from the edge parent_edges = face_3d.boundary_segments if face_3d.holes is None \ else face_3d.boundary_segments + \ tuple(seg for hole in face_3d.hole_segments for seg in hole) for new_f in bool_int: new_pts_2d = [] for pt in new_f.boundary: for edge in parent_edges: close_pt = edge.closest_point(pt) if pt.distance_to_point(close_pt) < offset_distance: move_vec = edge.v.rotate_xy(math.pi / 2).normalize() move_vec = move_vec * offset_distance pt = pt.move(move_vec) new_pts_2d.append(Point2D(pt.x, pt.y)) new_polygons.append(Polygon2D(new_pts_2d)) else: new_polygons.append(polygon) new_are_doors.append(isd) # assign the offset polygons to this object self._polygons = tuple(new_polygons) self._are_doors = tuple(new_are_doors)
[docs] def remove_self_intersecting(self, tolerance=0.01): """Remove any skylight polygons that are self intersecting. Args: tolerance: The minimum distance between a vertex coordinates where they are considered equivalent. (Default: 0.01, suitable for objects in meters). Returns: A string with the message. Will be an empty string if valid. """ new_polygons, new_are_doors = [], [] for polygon, isd in zip(self.polygons, self.are_doors): if not polygon.is_self_intersecting: new_polygons.append(polygon) new_are_doors.append(isd) else: try: new_geo = polygon.remove_colinear_vertices(tolerance) if not new_geo.is_self_intersecting: new_polygons.append(new_geo) new_are_doors.append(isd) except AssertionError: pass # assign the clean polygons to this object self._polygons = tuple(new_polygons) self._are_doors = tuple(new_are_doors)
[docs] def union_overlaps(self, tolerance=0.01): """Union any skylight polygons that overlap with one another. Args: tolerance: The minimum distance that two polygons must overlap in order for them to be considered overlapping. (Default: 0.01, suitable for objects in meters). """ # group the polygons by their overlap grouped_polys = Polygon2D.group_by_overlap(self.polygons, tolerance) # union any of the polygons that overlap if not all(len(g) == 1 for g in grouped_polys): new_polys = [] for p_group in grouped_polys: if len(p_group) == 1: new_polys.append(p_group[0]) else: union_poly = Polygon2D.boolean_union_all(p_group, tolerance) for new_poly in union_poly: new_polys.append(new_poly.remove_colinear_vertices(tolerance)) self._reassign_are_doors(new_polys) self._polygons = tuple(new_polys)
[docs] def merge_and_simplify(self, max_separation, tolerance=0.01): """Merge skylight polygons that are close to one another into a single polygon. This can be used to create a simpler set of skylights that is easier to edit and is in the same location as the original skylights. Args: max_separation: A number for the maximum distance between skylight polygons at which point they will be merged into a single geometry. Typically, this max_separation should be set to a value that is slightly larger than the window frame. Setting this equal to the tolerance will simply join neighboring skylights together. tolerance: The maximum difference between point values for them to be considered distinct. (Default: 0.01, suitable for objects in meters). """ # gather a clean version of the polygons with colinear vertices removed clean_polys = [] for poly in self.polygons: try: clean_polys.append(poly.remove_colinear_vertices(tolerance)) except AssertionError: # degenerate geometry to ignore pass # join the polygons together if max_separation <= tolerance: new_polys = Polygon2D.joined_intersected_boundary( clean_polys, tolerance) else: new_polys = Polygon2D.gap_crossing_boundary( clean_polys, max_separation, tolerance) self._reassign_are_doors(new_polys) self._polygons = tuple(new_polys)
[docs] def merge_to_bounding_rectangle(self, tolerance=0.01): """Merge skylight polygons that touch or overlap with one another to a rectangle. Args: tolerance: The minimum distance from the edge of a neighboring polygon at which a point is considered to touch that polygon. (Default: 0.01, suitable for objects in meters). """ # group the polygons by their overlap grouped_polys = Polygon2D.group_by_touching(self.polygons, tolerance) # union any of the polygons that overlap if not all(len(g) == 1 for g in grouped_polys): new_polys = [] for p_group in grouped_polys: if len(p_group) == 1: new_polys.append(p_group[0]) else: min_pt, max_pt = bounding_rectangle(p_group) rect_verts = ( min_pt, Point2D(max_pt.x, min_pt.y), max_pt, Point2D(min_pt.x, max_pt.y)) rect_poly = Polygon2D(rect_verts) new_polys.append(rect_poly) self._reassign_are_doors(new_polys) self._polygons = tuple(new_polys)
[docs] def remove_small_skylights(self, area_threshold): """Remove any small skylight polygons that are below a certain area threshold. Args: area_threshold: A number for the area below which a skylight polygon will be removed. """ new_polygons, new_are_doors = [], [] for poly, is_dr in zip(self.polygons, self.are_doors): if poly.area > area_threshold: new_polygons.append(poly) new_are_doors.append(is_dr) self._polygons = tuple(new_polygons) self._are_doors = tuple(new_are_doors)
[docs] @classmethod def from_dict(cls, data): """Create DetailedSkylights from a dictionary. Args: data: A dictionary in the format below. .. code-block:: python { "type": "DetailedSkylights", "polygons": [((0.5, 0.5), (2, 0.5), (2, 2), (0.5, 2)), ((3, 1), (4, 1), (4, 2))], "are_doors": [False] } """ assert data['type'] == 'DetailedSkylights', \ 'Expected DetailedSkylights dictionary. Got {}.'.format(data['type']) are_doors = data['are_doors'] if 'are_doors' in data else None return cls( tuple(Polygon2D(tuple(Point2D.from_array(pt) for pt in poly)) for poly in data['polygons']), are_doors )
[docs] def to_dict(self): """Get DetailedSkylights as a dictionary.""" return { 'type': 'DetailedSkylights', 'polygons': [[pt.to_array() for pt in poly] for poly in self.polygons], 'are_doors': self.are_doors }
def _reassign_are_doors(self, new_polys): """Reset the are_doors property using a set of new polygons.""" if len(new_polys) != len(self._polygons): if all(not dr for dr in self._are_doors): # common case of no overhead doors self._are_doors = (False,) * len(new_polys) else: new_are_doors = [] for n_poly in new_polys: for o_poly, is_door in zip(self.polygons, self.are_doors): if n_poly.is_point_inside_bound_rect(o_poly.center): new_are_doors.append(is_door) break else: new_are_doors.append(False) self._are_doors = tuple(new_are_doors) @staticmethod def _is_sub_polygon(sub_poly, parent_poly, parent_holes=None): """Check if a sub-polygon is valid for a given assumed parent polygon. Args: sub_poly: A sub-Polygon2D for which sub-face equivalency will be tested. parent_poly: A parent Polygon2D. parent_holes: An optional list of Polygon2D for any holes that may exist in the parent polygon. (Default: None). """ if parent_holes is None: return parent_poly.is_polygon_inside(sub_poly) else: if not parent_poly.is_polygon_inside(sub_poly): return False for hole_poly in parent_holes: if not hole_poly.is_polygon_outside(sub_poly): return False return True @staticmethod def _sub_poly_relation(sub_polygon, parent_poly, parent_holes, tolerance): """Check the relationship between a sub_polygon and a parent polygon. Args: sub_polygon: A Polygon2D which will be checked if it lies inside the parent. parent_poly: A parent Polygon2D. parent_holes: An optional list of Polygon2D for any holes that may exist in the parent polygon. (Default: None). """ base_relation = parent_poly.polygon_relationship(sub_polygon, tolerance) if parent_holes is None: return base_relation else: if base_relation == -1: return -1 for hole_poly in parent_holes: hole_relation = hole_poly.polygon_relationship(sub_polygon, tolerance) if hole_relation == 1: return -1 return base_relation @staticmethod def _is_sub_point(sub_point, parent_poly, parent_holes=None): """Check if a point lies inside a parent polygon. Args: sub_point: A Point2D which will be checked if it lies inside the parent. parent_poly: A parent Polygon2D. parent_holes: An optional list of Polygon2D for any holes that may exist in the parent polygon. (Default: None). """ if parent_holes is None: return parent_poly.is_point_inside(sub_point) else: if not parent_poly.is_point_inside(sub_point): return False for hole_poly in parent_holes: if hole_poly.is_point_inside(sub_point): return False return True def __len__(self): return len(self._polygons) def __getitem__(self, key): return self._polygons[key] def __iter__(self): return iter(self._polygons) def __copy__(self): return DetailedSkylights(self._polygons, self._are_doors) def __key(self): """A tuple based on the object properties, useful for hashing.""" return tuple(hash(polygon) for polygon in self._polygons) + self.are_doors def __hash__(self): return hash(self.__key()) def __eq__(self, other): return isinstance(other, DetailedSkylights) and \ len(self._polygons) == len(other._polygons) def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return 'DetailedSkylights: [{} windows]'.format(len(self._polygons))