diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/.nojekyll @@ -0,0 +1 @@ + diff --git a/README.md b/README.md new file mode 100644 index 00000000..ebfb3665 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# documentation diff --git a/docs/.buildinfo b/docs/.buildinfo new file mode 100644 index 00000000..ae14840f --- /dev/null +++ b/docs/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 89d4175636d7a8278efe82e967435b14 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/.doctrees/cli/create.doctree b/docs/.doctrees/cli/create.doctree new file mode 100644 index 00000000..fa00a8ef Binary files /dev/null and b/docs/.doctrees/cli/create.doctree differ diff --git a/docs/.doctrees/cli/edit.doctree b/docs/.doctrees/cli/edit.doctree new file mode 100644 index 00000000..9428d167 Binary files /dev/null and b/docs/.doctrees/cli/edit.doctree differ diff --git a/docs/.doctrees/cli/index.doctree b/docs/.doctrees/cli/index.doctree new file mode 100644 index 00000000..ace97894 Binary files /dev/null and b/docs/.doctrees/cli/index.doctree differ diff --git a/docs/.doctrees/cli/translate.doctree b/docs/.doctrees/cli/translate.doctree new file mode 100644 index 00000000..bc2f0d78 Binary files /dev/null and b/docs/.doctrees/cli/translate.doctree differ diff --git a/docs/.doctrees/cli/validate.doctree b/docs/.doctrees/cli/validate.doctree new file mode 100644 index 00000000..3eafba11 Binary files /dev/null and b/docs/.doctrees/cli/validate.doctree differ diff --git a/docs/.doctrees/dragonfly.building.doctree b/docs/.doctrees/dragonfly.building.doctree new file mode 100644 index 00000000..5cb617ee Binary files /dev/null and b/docs/.doctrees/dragonfly.building.doctree differ diff --git a/docs/.doctrees/dragonfly.cli.create.doctree b/docs/.doctrees/dragonfly.cli.create.doctree new file mode 100644 index 00000000..1e522a61 Binary files /dev/null and b/docs/.doctrees/dragonfly.cli.create.doctree differ diff --git a/docs/.doctrees/dragonfly.cli.doctree b/docs/.doctrees/dragonfly.cli.doctree new file mode 100644 index 00000000..f3dfc05b Binary files /dev/null and b/docs/.doctrees/dragonfly.cli.doctree differ diff --git a/docs/.doctrees/dragonfly.cli.edit.doctree b/docs/.doctrees/dragonfly.cli.edit.doctree new file mode 100644 index 00000000..e9e8e13a Binary files /dev/null and b/docs/.doctrees/dragonfly.cli.edit.doctree differ diff --git a/docs/.doctrees/dragonfly.cli.translate.doctree b/docs/.doctrees/dragonfly.cli.translate.doctree new file mode 100644 index 00000000..659124fd Binary files /dev/null and b/docs/.doctrees/dragonfly.cli.translate.doctree differ diff --git a/docs/.doctrees/dragonfly.cli.validate.doctree b/docs/.doctrees/dragonfly.cli.validate.doctree new file mode 100644 index 00000000..351281bb Binary files /dev/null and b/docs/.doctrees/dragonfly.cli.validate.doctree differ diff --git a/docs/.doctrees/dragonfly.colorobj.doctree b/docs/.doctrees/dragonfly.colorobj.doctree new file mode 100644 index 00000000..b91528e6 Binary files /dev/null and b/docs/.doctrees/dragonfly.colorobj.doctree differ diff --git a/docs/.doctrees/dragonfly.config.doctree b/docs/.doctrees/dragonfly.config.doctree new file mode 100644 index 00000000..89287ab6 Binary files /dev/null and b/docs/.doctrees/dragonfly.config.doctree differ diff --git a/docs/.doctrees/dragonfly.context.doctree b/docs/.doctrees/dragonfly.context.doctree new file mode 100644 index 00000000..469bc8ca Binary files /dev/null and b/docs/.doctrees/dragonfly.context.doctree differ diff --git a/docs/.doctrees/dragonfly.dictutil.doctree b/docs/.doctrees/dragonfly.dictutil.doctree new file mode 100644 index 00000000..b6583f74 Binary files /dev/null and b/docs/.doctrees/dragonfly.dictutil.doctree differ diff --git a/docs/.doctrees/dragonfly.doctree b/docs/.doctrees/dragonfly.doctree new file mode 100644 index 00000000..b0798cb1 Binary files /dev/null and b/docs/.doctrees/dragonfly.doctree differ diff --git a/docs/.doctrees/dragonfly.extensionutil.doctree b/docs/.doctrees/dragonfly.extensionutil.doctree new file mode 100644 index 00000000..8939f617 Binary files /dev/null and b/docs/.doctrees/dragonfly.extensionutil.doctree differ diff --git a/docs/.doctrees/dragonfly.model.doctree b/docs/.doctrees/dragonfly.model.doctree new file mode 100644 index 00000000..89735151 Binary files /dev/null and b/docs/.doctrees/dragonfly.model.doctree differ diff --git a/docs/.doctrees/dragonfly.projection.doctree b/docs/.doctrees/dragonfly.projection.doctree new file mode 100644 index 00000000..1b9e2512 Binary files /dev/null and b/docs/.doctrees/dragonfly.projection.doctree differ diff --git a/docs/.doctrees/dragonfly.properties.doctree b/docs/.doctrees/dragonfly.properties.doctree new file mode 100644 index 00000000..2a4badf4 Binary files /dev/null and b/docs/.doctrees/dragonfly.properties.doctree differ diff --git a/docs/.doctrees/dragonfly.roof.doctree b/docs/.doctrees/dragonfly.roof.doctree new file mode 100644 index 00000000..b82021ed Binary files /dev/null and b/docs/.doctrees/dragonfly.roof.doctree differ diff --git a/docs/.doctrees/dragonfly.room2d.doctree b/docs/.doctrees/dragonfly.room2d.doctree new file mode 100644 index 00000000..1bbb9b03 Binary files /dev/null and b/docs/.doctrees/dragonfly.room2d.doctree differ diff --git a/docs/.doctrees/dragonfly.shadingparameter.doctree b/docs/.doctrees/dragonfly.shadingparameter.doctree new file mode 100644 index 00000000..078bf56c Binary files /dev/null and b/docs/.doctrees/dragonfly.shadingparameter.doctree differ diff --git a/docs/.doctrees/dragonfly.skylightparameter.doctree b/docs/.doctrees/dragonfly.skylightparameter.doctree new file mode 100644 index 00000000..4b1bc825 Binary files /dev/null and b/docs/.doctrees/dragonfly.skylightparameter.doctree differ diff --git a/docs/.doctrees/dragonfly.story.doctree b/docs/.doctrees/dragonfly.story.doctree new file mode 100644 index 00000000..91e6e82b Binary files /dev/null and b/docs/.doctrees/dragonfly.story.doctree differ diff --git a/docs/.doctrees/dragonfly.subdivide.doctree b/docs/.doctrees/dragonfly.subdivide.doctree new file mode 100644 index 00000000..c6ce93b6 Binary files /dev/null and b/docs/.doctrees/dragonfly.subdivide.doctree differ diff --git a/docs/.doctrees/dragonfly.windowparameter.doctree b/docs/.doctrees/dragonfly.windowparameter.doctree new file mode 100644 index 00000000..36cf7571 Binary files /dev/null and b/docs/.doctrees/dragonfly.windowparameter.doctree differ diff --git a/docs/.doctrees/dragonfly.writer.building.doctree b/docs/.doctrees/dragonfly.writer.building.doctree new file mode 100644 index 00000000..0bc3e715 Binary files /dev/null and b/docs/.doctrees/dragonfly.writer.building.doctree differ diff --git a/docs/.doctrees/dragonfly.writer.context.doctree b/docs/.doctrees/dragonfly.writer.context.doctree new file mode 100644 index 00000000..5c45ab00 Binary files /dev/null and b/docs/.doctrees/dragonfly.writer.context.doctree differ diff --git a/docs/.doctrees/dragonfly.writer.doctree b/docs/.doctrees/dragonfly.writer.doctree new file mode 100644 index 00000000..db38e8c3 Binary files /dev/null and b/docs/.doctrees/dragonfly.writer.doctree differ diff --git a/docs/.doctrees/dragonfly.writer.model.doctree b/docs/.doctrees/dragonfly.writer.model.doctree new file mode 100644 index 00000000..fee49258 Binary files /dev/null and b/docs/.doctrees/dragonfly.writer.model.doctree differ diff --git a/docs/.doctrees/dragonfly.writer.room2d.doctree b/docs/.doctrees/dragonfly.writer.room2d.doctree new file mode 100644 index 00000000..815d0c2c Binary files /dev/null and b/docs/.doctrees/dragonfly.writer.room2d.doctree differ diff --git a/docs/.doctrees/dragonfly.writer.story.doctree b/docs/.doctrees/dragonfly.writer.story.doctree new file mode 100644 index 00000000..ee5555b8 Binary files /dev/null and b/docs/.doctrees/dragonfly.writer.story.doctree differ diff --git a/docs/.doctrees/environment.pickle b/docs/.doctrees/environment.pickle new file mode 100644 index 00000000..b5c22499 Binary files /dev/null and b/docs/.doctrees/environment.pickle differ diff --git a/docs/.doctrees/index.doctree b/docs/.doctrees/index.doctree new file mode 100644 index 00000000..a7580b2d Binary files /dev/null and b/docs/.doctrees/index.doctree differ diff --git a/docs/.doctrees/modules.doctree b/docs/.doctrees/modules.doctree new file mode 100644 index 00000000..274b50ba Binary files /dev/null and b/docs/.doctrees/modules.doctree differ diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..ebfb3665 --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +# documentation diff --git a/docs/_modules/dragonfly/building.html b/docs/_modules/dragonfly/building.html new file mode 100644 index 00000000..27b1260e --- /dev/null +++ b/docs/_modules/dragonfly/building.html @@ -0,0 +1,2885 @@ + + + + + + + dragonfly.building — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.building

+# coding: utf-8
+"""Dragonfly Building."""
+from __future__ import division
+
+import math
+try:
+    from itertools import izip as zip  # python 2
+except ImportError:
+    xrange = range  # python 3
+
+from ladybug_geometry.geometry2d import Vector2D, Point2D, LineSegment2D, Polygon2D
+from ladybug_geometry.geometry3d import Vector3D
+from ladybug_geometry_polyskel.polysplit import perimeter_core_subfaces
+
+from honeybee.model import Model
+from honeybee.room import Room
+from honeybee.shade import Shade
+from honeybee.boundarycondition import Outdoors
+from honeybee.boundarycondition import boundary_conditions as bcs
+from honeybee.typing import clean_string, invalid_dict_error
+from honeybee.units import parse_distance_string
+
+from ._base import _BaseGeometry
+from .properties import BuildingProperties
+from .story import Story
+from .room2d import Room2D
+import dragonfly.writer.building as writer
+
+
+
[docs]class Building(_BaseGeometry): + """A complete Building defined by Stories (and optional extra 3D rooms). + + Buildings must have at least one dragonfly Story or one Honeybee Room under + the room_3ds property. + + Args: + identifier: Text string for a unique Building ID. Must be < 100 characters + and not contain any spaces or special characters. + unique_stories: An array of unique Dragonfly Story objects that + together form the entire building. Stories input here can be in any + order but they will be automatically sorted from lowest floor to + highest floor when they are assigned to the Building. Note that, + if a given Story is repeated several times over the height of the + Building, the unique Story included in this list should be the + first (lowest) Story of the repeated floors. (Default: None). + room_3ds: An optional array of 3D Honeybee Room objects for additional + Rooms that are a part of the Building but are not represented within + the unique_stories. This is useful when there are parts of the Building + geometry that cannot easily be represented with the extruded floor + plate and sloped roof assumptions that underlie Dragonfly Room2Ds + and RoofSpecification. Cases where this input is most useful include + sloped walls and certain types of domed roofs that become tedious to + implement with RoofSpecification. Matching the Honeybee Room.story + property to the Dragonfly Story.display_name of an object within the + unique_stories will effectively place the Honeybee Room on that Story + for the purposes of floor_area, exterior_wall_area, etc. However, note + that the Honeybee Room.multiplier property takes precedence over + whatever multiplier is assigned to the Dragonfly Story that the + Room.story may reference. (Default: None). + sort_stories: A boolean to note whether the unique_stories should be sorted + from lowest to highest story upon initialization (True) or whether + the input order of unique_stories should be left as-is. (Default: True). + + Properties: + * identifier + * display_name + * full_id + * unique_stories + * unique_room_2ds + * room_3ds + * room_3d_faces + * room_3d_apertures + * room_3d_doors + * room_3d_shades + * has_room_2ds + * has_room_3ds + * room_2d_story_names + * room_3d_story_names + * story_count + * story_count_above_ground + * unique_stories_above_ground + * height + * height_above_ground + * height_from_first_floor + * footprint_area + * floor_area + * exterior_wall_area + * exterior_aperture_area + * volume + * min + * max + * user_data + """ + __slots__ = ('_unique_stories', '_room_3ds') + + def __init__(self, identifier, unique_stories=None, room_3ds=None, + sort_stories=True): + """A complete Building defined by Stories.""" + # initialize and perform a basic check that there's some geometry + _BaseGeometry.__init__(self, identifier) # process the identifier + if (unique_stories is None or len(unique_stories) == 0) and \ + (room_3ds is None or len(room_3ds) == 0): + raise ValueError( + 'Building must have at least one Story or one Room under room_3ds.') + + # process the story geometry + if unique_stories is not None: + for story in unique_stories: + assert isinstance(story, Story), \ + 'Expected dragonfly Story. Got {}'.format(type(story)) + story._parent = self + if sort_stories: + unique_stories = \ + tuple(sorted(unique_stories, key=lambda x: x.floor_height)) + else: + unique_stories = tuple(unique_stories) + self._unique_stories = unique_stories + else: + self._unique_stories = () + + # process the room_3d geometry + if room_3ds is not None: + for room in room_3ds: + assert isinstance(room, Room), \ + 'Expected honeybee Room. Got {}'.format(type(room)) + room._parent = self + # assign stories to any Rooms that lack them + if not all([r.story is not None for r in room_3ds]): + Room.stories_by_floor_height(room_3ds) + self._room_3ds = tuple(room_3ds) + else: + self._room_3ds = () + + self._properties = BuildingProperties(self) # properties for extensions + +
[docs] @classmethod + def from_footprint(cls, identifier, footprint, floor_to_floor_heights, + perimeter_offset=0, tolerance=0): + """Initialize a Building from an array of Face3Ds representing a footprint. + + All of the resulting Room2Ds will have a floor-to-ceiling height equal to the + Story floor-to-floor height. Also, none of the Room2Ds will have contact + with the ground or top exposure but the separate_top_bottom_floors method + can be used to automatically break these floors out from the multiplier + representation and assign these properties. + + Args: + identifier: Text string for a unique Building ID. Must be < 100 characters + and not contain any spaces or special characters. + footprint: An array of horizontal ladybug-geometry Face3Ds that together + represent the the footprint of the Building. + floor_to_floor_heights: An array of float values with a length equal + to the number of stories in the Building. Each value in the list + represents the floor_to_floor height of the Story starting from + the first floor and then moving to the top floor. Note that numbers + should be in the units system of the footprint geometry. + perimeter_offset: An optional positive number that will be used to + offset the perimeter of the footprint to create core/perimeter + zones. If this value is 0, no offset will occur and each story + will be represented with a single Room2D per polygon (Default: 0). + tolerance: The maximum difference between z values at which point vertices + are considered to be in the same horizontal plane. This is used to check + that all vertices of the input floor_geometry lie in the same horizontal + floor plane. Default is 0, which will not perform any check. + """ + # generate the unique Room2Ds from the footprint + room_2ds = cls._generate_room_2ds( + footprint, floor_to_floor_heights[0], perimeter_offset, + identifier, 1, tolerance) + + # generate the unique stories from the floor_to_floor_heights + stories = [] + total_height = 0 + prev_flr_to_flr = None + for i, flr_hgt in enumerate(floor_to_floor_heights): + if flr_hgt != prev_flr_to_flr: + if i != 0: + rooms = [room.duplicate() for room in room_2ds] + move_vec = Vector3D(0, 0, total_height) + for j, room in enumerate(rooms): + room.move(move_vec) + room.floor_to_ceiling_height = flr_hgt + room.identifier = \ + '{}_Floor{}_Room{}'.format(identifier, i + 1, j + 1) + if perimeter_offset != 0: # reset all boundary conditions + for room in rooms: + room.boundary_conditions = [bcs.outdoors] * len(room) + Room2D.solve_adjacency(rooms, tolerance) + else: + rooms = room_2ds + stories.append(Story( + '{}_Floor{}'.format(identifier, i + 1), rooms, flr_hgt)) + else: + stories[-1].multiplier += 1 + total_height += flr_hgt + prev_flr_to_flr = flr_hgt + + return cls(identifier, stories)
+ +
[docs] @classmethod + def from_all_story_geometry(cls, identifier, all_story_geometry, + floor_to_floor_heights, perimeter_offset=0, + tolerance=0.01): + """Initialize a Building from an array of Face3Ds arrays representing all floors. + + This method will test to see which of the stories are geometrically unique + (accoutring for both the floor plate geometry and the floor_to_floor_heights). + It will only include the unique floor geometries in the resulting Building. + + All of the resulting Room2Ds will have a floor-to-ceiling height equal to the + Story floor-to-floor height. + + Args: + identifier: Text string for a unique Building ID. Must be < 100 characters + and not contain any spaces or special characters. + all_story_geometry: An array of arrays with each sub-array possessing + horizontal ladybug-geometry Face3Ds that representing the floor + plates of the building. Together, these Face3Ds should represent + all Stories of a building and each array of Face3Ds should together + represent one Story. + floor_to_floor_heights: An array of float values with a length equal + to the number of stories in the Building. Each value in the list + represents the floor_to_floor height of the Story starting from + the first floor and then moving to the top floor. Note that numbers + should be in the units system of the footprint geometry. + perimeter_offset: An optional positive number that will be used to offset + the perimeter of the all_story_geometry to create core/perimeter + zones. If this value is 0, no offset will occur and each story + will be represented with a single Room2D per polygon (Default: 0). + tolerance: The maximum difference between x, y, and z values at which + point vertices are considered to be the same. This is also needed as + a means to determine which floor geometries are equivalent to one + another and should be a part the same Story. Default: 0.01, suitable + for objects in meters. + """ + # generate the first story of the building + room_2ds = cls._generate_room_2ds( + all_story_geometry[0], floor_to_floor_heights[0], perimeter_offset, + identifier, 1, tolerance) + stories = [Story('{}_Floor1'.format(identifier), room_2ds, + floor_to_floor_heights[0])] + + # generate the remaining unique stories from the floor_to_floor_heights + remaining_geo = all_story_geometry[1:] + remaining_flr_hgts = floor_to_floor_heights[1:] + prev_geo = all_story_geometry[0] + prev_flr_to_flr = floor_to_floor_heights[0] + for i, (room_geo, flr_hgt) in enumerate(zip(remaining_geo, remaining_flr_hgts)): + # test is anything is geometrically different + if flr_hgt != prev_flr_to_flr or len(room_geo) != len(prev_geo) or \ + not all(cls._is_story_equivalent(rm1, rm2, tolerance) + for rm1, rm2 in zip(room_geo, prev_geo)): + room_2ds = cls._generate_room_2ds( + room_geo, flr_hgt, perimeter_offset, identifier, i + 2, tolerance) + stories.append(Story( + '{}_Floor{}'.format(identifier, i + 2), room_2ds, flr_hgt)) + else: # geometry is the same as the floor below + stories[-1].multiplier += 1 + prev_geo = room_geo + prev_flr_to_flr = flr_hgt + + return cls(identifier, stories)
+ +
[docs] @classmethod + def from_dict(cls, data, tolerance=0, angle_tolerance=0, sort_stories=True): + """Initialize an Building from a dictionary. + + Args: + data: A dictionary representation of a Building object. + tolerance: The maximum difference between z values at which point vertices + are considered to be in the same horizontal plane. This is used to check + that all vertices of the input floor_geometry lie in the same horizontal + floor plane. Default is 0, which will not perform any check. + angle_tolerance: The max angle difference in degrees that vertices are + allowed to differ from one another in order to consider them colinear. + Default is 0, which makes no attempt to evaluate whether the Room + volume is closed. + sort_stories: A boolean to note whether the unique_stories should be sorted + from lowest to highest story upon initialization (True) or whether + the input order of unique_stories should be left as-is. (Default: True). + """ + # check the type of dictionary + assert data['type'] == 'Building', 'Expected Building dictionary. ' \ + 'Got {}.'.format(data['type']) + # extract the 2D Stories + stories = [] + if 'unique_stories' in data and data['unique_stories'] is not None: + for s_dict in data['unique_stories']: + try: + stories.append(Story.from_dict(s_dict, tolerance)) + except Exception as e: + invalid_dict_error(s_dict, e) + # extract any additional 3D Rooms + room_3ds = [] + if 'room_3ds' in data and data['room_3ds'] is not None: + for r_dict in data['room_3ds']: + try: + room_3ds.append(Room.from_dict(r_dict, tolerance, angle_tolerance)) + except Exception as e: + invalid_dict_error(r_dict, e) + # create the Building object + building = cls(data['identifier'], stories, room_3ds, sort_stories=sort_stories) + if 'display_name' in data and data['display_name'] is not None: + building.display_name = data['display_name'] + if 'user_data' in data and data['user_data'] is not None: + building.user_data = data['user_data'] + + if data['properties']['type'] == 'BuildingProperties': + building.properties._load_extension_attr_from_dict(data['properties']) + return building
+ +
[docs] @classmethod + def from_honeybee(cls, model, conversion_method='AllRoom2D'): + """Initialize a Building from a Honeybee Model. + + If each Room has a story, these will be used to determine the separation + into Dragonfly stories. Otherwise, stories will be auto-generated + based on the floor heights of rooms. + + Args: + model: A Honeybee Model to be converted to a Dragonfly Building. + conversion_method: Text to indicate how the Honeybee Rooms should be + converted to Dragonfly. Note that the AllRoom2D option may result + in some loss or simplification of the 3D Honeybee geometry but + ensures that all of Dragonfly's features for editing the rooms can + be used. The ExtrudedOnly method will convert only the 3D Rooms + that would have no loss or simplification of geometry when converted + to Room2D. AllRoom3D keeps all detailed 3D geometry on the + Building.room_3ds property, enabling you to convert the 3D Rooms + to Room2D using the Building.convert_room_3ds_to_2d() method as you + see fit. (Default: AllRoom2D). Choose from the following options. + + * AllRoom2D - All Honeybee Rooms converted to Dragonfly Room2D + * ExtrudedOnly - Only pure extrusions converted to Dragonfly Room2D + * AllRoom3D - All Honeybee Rooms left as-is on Building.room_3ds + + """ + # if the rooms are being left as they are, just create the Building + method = conversion_method.lower() + if method in ('allroom3d', 'extrudedonly'): + dup_rooms = [r.duplicate() for r in model.rooms] + bldg = cls(model.identifier, room_3ds=dup_rooms) + bldg._display_name = model._display_name + if method == 'extrudedonly': + bldg.convert_all_room_3ds_to_2d( + extrusion_rooms_only=True, tolerance=model.tolerance, + angle_tolerance=model.angle_tolerance) + for story in bldg.unique_stories: + story._reset_adjacencies_from_honeybee( + story.room_2ds, model.tolerance) + return bldg + elif method != 'allroom2d': + msg = 'Building.from_honeybee conversion_method "{}" is not recognized\n' \ + 'Choose from: AllRoom2D, ExtrudedOnly, AllRoom3D.'.format( + conversion_method) + raise ValueError(msg) + + # proceed to convert all to Room2D; assign stories if they don't already exist + min_diff = parse_distance_string('2m', model.units) + remove_stories = False + if not all([room.story is not None for room in model.rooms]): + model.assign_stories_by_floor_height(min_diff) + remove_stories = True + # group the rooms by story and create dragonfly Stories + story_dict = {} + for room in model.rooms: + try: + story_dict[room.story].append(room) + except KeyError: + story_dict[room.story] = [room] + # evaluate floor heights to see if floors should be split + removed_flrs, new_flrs = [], {} + for s_id, rms in story_dict.items(): + if not cls._room_story_geometry_valid(rms): + rm_grps, flr_hts = Room.group_by_floor_height(rms, min_diff) + for grp, ht in zip(rm_grps, flr_hts): + new_flrs['{}_{}'.format(s_id, ht)] = grp + removed_flrs.append(s_id) + for r_flr in removed_flrs: + story_dict.pop(r_flr) + story_dict.update(new_flrs) + # create the Story and Building objects + stories = [Story.from_honeybee(clean_string(str(s_id)), rms, model.tolerance) + for s_id, rms in story_dict.items()] + bldg = cls(model.identifier, stories) + bldg._display_name = model._display_name + # if stories were auto-generated, remove them to avoid editing the input + if remove_stories: + for rm in model.rooms: + rm.story = None + return bldg
+ + @staticmethod + def _room_story_geometry_valid(rooms): + """Check that a set of Honeybee Rooms have geometry that makes a valid Story. + + Args: + rooms: An array of Honeybee Rooms that will be checked to ensure their + geometry makes a valid Story. + + Returns: + True if the Room geometries make a valid Story. False if they do not. + """ + if len(rooms) == 1: + return True + flr_hts = sorted([rm.geometry.min.z for rm in rooms]) + min_flr_to_ceil = min([rm.geometry.max.z - rm.geometry.min.z for rm in rooms]) + return True if flr_hts[-1] - flr_hts[0] < min_flr_to_ceil else False + + @property + def unique_stories(self): + """Get a tuple of only unique Story objects that form the Building. + + Repeated stories are represented only once but will have a non-unity multiplier. + """ + return self._unique_stories + + @property + def unique_room_2ds(self): + """Get a list of the unique Room2D objects that form the Building.""" + rooms = [] + for story in self._unique_stories: + rooms.extend(story.room_2ds) + return rooms + + @property + def room_3ds(self): + """Get a tuple of additional 3D Honeybee Rooms assigned to the Building. + + These rooms are a part of the Building but are not represented within + the unique_stories or unique_room_2ds. Matching the Honeybee Room.story + property to the Dragonfly Story.display_name of an object within the + unique_stories will effectively place the Honeybee Room on that Story + for the purposes of floor_area, exterior_wall_area, etc. However, note + that the Honeybee Room.multiplier property takes precedence over + whatever multiplier is assigned to the Dragonfly Story that the + Room.story may reference. + """ + return self._room_3ds + + @property + def room_3d_faces(self): + """Get a list of all Face objects for the 3D Honeybee Rooms in the Building.""" + return [face for room in self._room_3ds for face in room._faces] + + @property + def room_3d_apertures(self): + """Get a list of all Aperture objects for the 3D Honeybee Rooms in the Building. + """ + child_apertures = [] + for room in self._room_3ds: + for face in room._faces: + child_apertures.extend(face._apertures) + return child_apertures + + @property + def room_3d_doors(self): + """Get a list of all Door objects for the 3D Honeybee Rooms in the Building.""" + child_doors = [] + for room in self._room_3ds: + for face in room._faces: + child_doors.extend(face._doors) + return child_doors + + @property + def room_3d_shades(self): + """Get a list of all Shade objects for the 3D Honeybee Rooms in the Building.""" + child_shades = [] + for room in self._room_3ds: + child_shades.extend(room.shades) + for face in room.faces: + child_shades.extend(face.shades) + for ap in face._apertures: + child_shades.extend(ap.shades) + for dr in face._doors: + child_shades.extend(dr.shades) + return child_shades + + @property + def has_room_2ds(self): + """Get a boolean noting whether this Building has Room2Ds assigned under stories. + """ + return len(self._unique_stories) != 0 + + @property + def has_room_3ds(self): + """Get a boolean noting whether this Building has 3D Honeybee Rooms. + """ + return len(self._room_3ds) != 0 + + @property + def room_2d_story_names(self): + """Get a tuple of all Story display_names that have Room2Ds on them.""" + return tuple(story.display_name for story in self._unique_stories) + + @property + def room_3d_story_names(self): + """Get a tuple of all story display_names that have 3D Honeybee Rooms on them.""" + return tuple(set(rm.story for rm in self._room_3ds)) + + @property + def story_count(self): + """Get an integer for the number of stories in the building. + + This includes both the Room2Ds within unique_stories (including the + Story.multiplier) as well as all stories defined by the room_3ds. + """ + r3d_stories = 0 + if self.has_room_3ds: + story_2ds = self.room_2d_story_names + for st in self.room_3d_story_names: + if st not in story_2ds: + r3d_stories += 1 + return sum((story.multiplier for story in self._unique_stories)) + r3d_stories + + @property + def story_count_above_ground(self): + """Get an integer for the number of stories above the ground. + + All stories defined by 3D Rooms are assumed to be above ground. + """ + r3d_stories = 0 + if self.has_room_3ds: + story_2ds = self.room_2d_story_names + for st in self.room_3d_story_names: + if st not in story_2ds: + r3d_stories += 1 + return sum((story.multiplier for story in self.unique_stories_above_ground)) + \ + r3d_stories + + @property + def unique_stories_above_ground(self): + """Get a tuple of unique Story objects that are above the ground. + + A story is considered above the ground if at least one of its Room2Ds + has an outdoor boundary condition. + """ + return [story for story in self._unique_stories if story.is_above_ground] + + @property + def height(self): + """Get a number for the roof height of the Building as an absolute Z-coordinate. + + This property will account for the fact that the tallest Room may be a 3D + Honeybee Room. + """ + r2_h, r3_h = None, None + if self.has_room_3ds: + r3_h = max(r.max.z for r in self.room_3ds) + if self.has_room_2ds: + last_flr = self._unique_stories[-1] + r2_h = last_flr.floor_height + \ + (last_flr.floor_to_floor_height * last_flr.multiplier) + if r2_h is not None and r3_h is not None: + return max(r2_h, r3_h) + elif r2_h is not None: + return r2_h + return r3_h + + @property + def height_above_ground(self): + """Get a the height difference between the roof and first floor above the ground. + + This property will account for any 3D Room if they exist. + """ + r2_h, r3_h, bldg_h = None, None, self.height + try: + r2_h = bldg_h - self.unique_stories_above_ground[0].floor_height + except IndexError: # building completely below ground or no Room2Ds + r2_h = 0 + if self.has_room_3ds: + r3_h = bldg_h - min(r.min.z for r in self.room_3ds) + if r2_h is not None and r3_h is not None: + return max(r2_h, r3_h) + elif r2_h is not None: + return r2_h + return r3_h + + @property + def height_from_first_floor(self): + """Get a the height difference between the roof and the bottom-most floor. + + This property will account for any 3D Room if they exist. + """ + r2_h, r3_h, bldg_h = None, None, self.height + try: + r2_h = bldg_h - self.unique_stories[0].floor_height + except IndexError: # building completely below ground or no Room2Ds + r2_h = 0 + if self.has_room_3ds: + r3_h = bldg_h - min(r.min.z for r in self.room_3ds) + if r2_h is not None and r3_h is not None: + return max(r2_h, r3_h) + elif r2_h is not None: + return r2_h + return r3_h + + @property + def footprint_area(self): + """Get a number for the total footprint area of the Building. + + The footprint is derived from the lowest dragonfly Story of the building + unless the Building is composed entirely of 3D Rooms, in which case it + is the combined floor area of the Rooms belonging to the lowest story. + """ + try: + return self._unique_stories[0].floor_area + except IndexError: # no Room2Ds + return sum(r.floor_area for r in self._lowest_story_room_3ds() + if not r.exclude_floor_area) + + @property + def floor_area(self): + """Get a number for the total floor area in the Building. + + This property uses both the 2D Story multipliers and the 3D Room multipliers + to determine the total floor area. + """ + fa_r2 = sum([story.floor_area * story.multiplier + for story in self._unique_stories]) + fa_r3 = sum([room.floor_area * room.multiplier for room in self._room_3ds + if not room.exclude_floor_area]) + return fa_r2 + fa_r3 + + @property + def exterior_wall_area(self): + """Get a number for the total exterior wall area in the Building. + + This property uses both the 2D Story multipliers and the 3D Room multipliers + to determine the total exterior wall area. + """ + ewa_r2 = sum([story.exterior_wall_area * story.multiplier + for story in self._unique_stories]) + ewa_r3 = sum([r.exterior_wall_area * r.multiplier for r in self._room_3ds]) + return ewa_r2 + ewa_r3 + + @property + def exterior_aperture_area(self): + """Get a number for the total exterior wall aperture area in the Building. + + This property uses both the 2D Story multipliers and the 3D Room multipliers + to determine the total exterior wall aperture area. All skylights apertures + are excluded. + """ + eaa_r2 = sum([story.exterior_aperture_area * story.multiplier + for story in self._unique_stories]) + eaa_r3 = sum([room.exterior_wall_aperture_area * room.multiplier + for room in self._room_3ds]) + return eaa_r2 + eaa_r3 + + @property + def volume(self): + """Get a number for the volume of all the Rooms in the Building. + + This property uses both the 2D Story multipliers and the 3D Room multipliers + to determine the total Building volume. + """ + v_2r = sum([story.volume * story.multiplier for story in self._unique_stories]) + v_3r = sum([room.volume * room.multiplier for room in self._room_3ds]) + return v_2r + v_3r + + @property + def min(self): + """Get a Point2D for the min bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this Building is in proximity + to other objects. + """ + r2_min_pt, r3_min_pt = None, None + if self.has_room_2ds: + r2_min_pt = self._calculate_min(self._unique_stories) + if self.has_room_3ds: + r3_min_pt = Model._calculate_min(self._room_3ds) + if r2_min_pt is not None and r3_min_pt is not None: + return Point2D(min(r2_min_pt.x, r3_min_pt.x), min(r2_min_pt.y, r3_min_pt.y)) + elif r2_min_pt is not None: + return r2_min_pt + return Point2D(r3_min_pt.x, r3_min_pt.y) + + @property + def max(self): + """Get a Point2D for the max bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this Building is in proximity + to other objects. + """ + r2_max_pt, r3_max_pt = None, None + if self.has_room_2ds: + r2_max_pt = self._calculate_max(self._unique_stories) + if self.has_room_3ds: + r3_max_pt = Model._calculate_max(self._room_3ds) + if r2_max_pt is not None and r3_max_pt is not None: + return Point2D(max(r2_max_pt.x, r3_max_pt.x), max(r2_max_pt.y, r3_max_pt.y)) + elif r2_max_pt is not None: + return r2_max_pt + return Point2D(r3_max_pt.x, r3_max_pt.y) + +
[docs] def all_stories(self): + """Get a list of all Story objects that form the Building. + + The Story objects returned here each have a multiplier of 1 and repeated + stories are represented will their own Story object. 3D Rooms are not included + in this output. + """ + all_stories = [] + for story in self._unique_stories: + new_story = story.duplicate() + new_story.add_prefix('Flr1') + new_story.multiplier = 1 + all_stories.append(new_story) + + if story.multiplier != 1: + for i in range(story.multiplier - 1): + new_story = story.duplicate() + new_story.add_prefix('Flr{}'.format(i + 2)) + new_story.multiplier = 1 + m_vec = Vector3D(0, 0, story.floor_to_floor_height * (i + 1)) + new_story.move(m_vec) + all_stories.append(new_story) + return all_stories
+ +
[docs] def all_room_2ds(self): + """Get a list of all Room2D objects that form the Building.""" + rooms = [] + for story in self.all_stories(): + rooms.extend(story.room_2ds) + return rooms
+ +
[docs] def room_3ds_by_story(self, story_name): + """Get all of the 3D Honeybee Room objects assigned to a particular story. + + Args: + story_name: Text for the display_name of the Story for which + Honeybee Room objects will be returned. + """ + rooms = [] + for room in self.room_3ds: + if room.story == story_name: + rooms.append(room) + return rooms
+ +
[docs] def footprint(self, tolerance=0.01): + """A list of Face3D objects representing the footprint of the building. + + The footprint is derived from the lowest story of the building and, if + all Room2Ds of this story can be joined into a single continuous polyface, + then only one Face3D will be contained in the list output from this method. + Otherwise, several Face3Ds may be output. + + Args: + tolerance: The minimum distance between points at which they are + not considered touching. Default: 0.01, suitable for objects + in meters. + """ + if self.has_room_2ds: + ground_story = self._unique_stories[0] + if len(ground_story.room_2ds) == 1: # no need to create any new geometry + return [ground_story.room_2ds[0].floor_geometry] + else: # need a single list of Face3Ds for the whole footprint + return ground_story.footprint(tolerance) + foot_rooms = self._lowest_story_room_3ds() + return Room.grouped_horizontal_boundary(foot_rooms, tolerance=tolerance)
+ +
[docs] def shade_representation( + self, exclude_index=None, cap=False, include_room3ds=False, tolerance=0.01): + """A list of honeybee Shade objects representing the building geometry. + + These can be used to account for this Building's shade in the simulation of + another nearby Building. + + Args: + exclude_index: An optional index for a unique_story to be excluded from + the shade representation. If None, all stories will be included + in the result. (Default: None). + cap: Boolean to note whether the shade representation should be capped + with a top face. Usually, this is not necessary to account for + blocked sun and is only needed when it's important to account for + reflected sun off of roofs. (Default: False). + include_room3ds: Boolean to note whether the 3D Rooms assigned to + this Building should be included in the shade representation. + Only exterior geometries are included. (Default: False). + tolerance: The minimum distance between points at which they are + not considered touching. Default: 0.01, suitable for objects + in meters. + """ + context_shades = [] + if exclude_index is None: + for story in self.unique_stories: + context_shades.extend(story.shade_representation(cap, tolerance)) + else: + for i, story in enumerate(self.unique_stories): + if i != exclude_index: + context_shades.extend(story.shade_representation(cap, tolerance)) + else: + mult_shd = story.shade_representation_multiplier( + cap=cap, tolerance=tolerance) + context_shades.extend(mult_shd) + if include_room3ds and self.has_room_3ds: + for room in self.room_3ds: + for face in room.faces: + if isinstance(face.boundary_condition, Outdoors): + context_shades.append(Shade(face.identifier, face.geometry)) + return context_shades
+ +
[docs] def suggested_alignment_axes( + self, distance, direction=Vector2D(0, 1), angle_tolerance=1.0): + """Get suggested LineSegment2Ds to be used for this Building. + + This method will return the most common axes across the Building's Room2D + geometry along with the number of Room2D segments that correspond to each axis. + The latter can be used to filter the suggested alignment axes to get + only the most common ones across the Building. + + Args: + distance: A number for the distance that will be used in the alignment + operation. This will be used to determine the resolution at which + alignment axes are generated and evaluated. Smaller alignment + distances will result in the generation of more common_axes since + a finer resolution can differentiate common that would typically be + grouped together. For typical building geometry, an alignment distance + of 0.3 meters or 1 foot is typically suitable for eliminating + unwanted details while not changing the geometry too much from + its original location. + direction: A Vector2D object to represent the direction in which the + common axes will be evaluated and generated. + angle_tolerance: The max angle difference in radians that the Room2D + segment direction can differ from the input direction before the + segments are not factored into this calculation of common axes. + + Returns: + A tuple with two elements. + + - common_axes: A list of LineSegment2D objects for the common + axes across the input Room2Ds. + + - axis_values: A list of integers that aligns with the common_axes + and denotes how many segments of the input Room2D each axis + relates to. Higher numbers indicate that that the axis is more + commonly aligned across the Room2Ds. + """ + return Room2D.generate_alignment_axes( + self.unique_room_2ds, distance, direction, angle_tolerance)
+ +
[docs] def add_stories(self, stories, add_duplicate_ids=False): + """Add additional Story objects to this Building. + + Using this method will ensure that Stories are ordered according to their + floor height as they are added. Also, in the case that Story identifiers + match an existing one in this Building, these Stories will be merged + together. If add_duplicate_ids is False, Room2Ds that have matching + identifiers within a merged Story will not be ignored in order to + avoid ID conflicts. + + Args: + stories: A list or tuple of Story objects to be added to this Building. + add_duplicate_ids: A boolean to note whether added Room2Ds that + have matching identifiers within each Story should be ignored (False) + or they should be added to the Story creating an ID collision + that can be resolved later (True). (Default: False). + """ + # check to be sure all of the input is correct + for story in stories: + assert isinstance(story, Story), \ + 'Expected dragonfly Story. Got {}'.format(type(story)) + # create the list of new stories, merging stories that have the same identifier + new_stories = list(self._unique_stories) + for o_story in stories: + for e_story in new_stories: + if o_story.identifier == e_story.identifier: + e_story.add_room_2ds(o_story.room_2ds, add_duplicate_ids) + break + else: + o_story._parent = self + new_stories.append(o_story) + # sort the stories by floor level and assign them to this Building + unique_stories = tuple(sorted(new_stories, key=lambda x: x.floor_height)) + self._unique_stories = unique_stories
+ +
[docs] def add_room_3ds(self, rooms, add_duplicate_ids=False): + """Add additional 3D Honeybee Room objects to this Building. + + Args: + stories: A list or tuple of Honeybee Room objects to be added to + this building. + add_duplicate_ids: A boolean to note whether added Rooms that + have matching identifiers within the current Building should be + ignored (False) or they should be added to the Building creating + an ID collision that can be resolved later (True). (Default: False). + """ + # check to be sure that the input is composed of Rooms + for room in rooms: + assert isinstance(room, Room), \ + 'Expected honeybee Room. Got {}'.format(type(room)) + # add the rooms and deal with duplicated IDs appropriately + new_room_3ds = list(self._room_3ds) + if add_duplicate_ids: + for room in rooms: + room._parent = self + if room.story is None: + room.story = 'Unknown' + new_room_3ds.append(room) + else: + exist_set = {rm.identifier for rm in self._room_3ds} + for room in rooms: + if room.identifier not in exist_set: + room._parent = self + if room.story is None: + room.story = 'Unknown' + new_room_3ds.append(room) + # assign the new Rooms to this Building + self._room_3ds = tuple(new_room_3ds)
+ +
[docs] def convert_room_3d_to_2d(self, room_3d_identifier, tolerance=0.01): + """Convert a single 3D Honeybee Room to a Dragonfly Room2D on this Building. + + This process will add the Room2D to an existing Dragonfly Story on the + Building if the Honeybee Room.story matches a Story.display_name on this + object. If not, a new Story on this Building will be initialized. + + Args: + room_3d_identifier: The identifier of the 3D honeybee Room on this + Building that will be converted to a dragonfly Room2D. + tolerance: The maximum difference between values at which point vertices + are considered to be the same. (Default: 0.01, suitable for + objects in Meters). + + Returns: + The newly-created Room2D object from the converted Room. Will be + None if the Honeybee Room is not a closed solid and cannot be + converted to a valid Room2D. + """ + # get the Honeybee Room object to be converted + hb_room_i = [i for i, r in enumerate(self.room_3ds) + if r.identifier == room_3d_identifier] + if len(hb_room_i) == 0: + raise ValueError( + 'No 3D Honeybee Room with an identifier of "{}" was found on ' + 'Building "{}"'.format(room_3d_identifier, self.display_name)) + elif len(hb_room_i) != 1: + raise ValueError( + 'Multiple 3D Honeybee Rooms with an identifier of "{}" were found on ' + 'Building "{}"'.format(room_3d_identifier, self.display_name)) + new_room_3ds = list(self._room_3ds) + hb_room = new_room_3ds.pop(hb_room_i[0]) + # create a Dragonfly Room2D from the Honeybee Room + try: + df_room = Room2D.from_honeybee(hb_room, tolerance) + except Exception: # room is not a closed solid + return None + self._room_3ds = tuple(new_room_3ds) + # assign the Room2D to an existing Story or create a new one + for story in self._unique_stories: + if story.display_name == hb_room.story: + story.add_room_2d(df_room) + break + else: # a new Story object has to be initialized + new_story = Story(clean_string(hb_room.story), (df_room,)) + new_story.display_name = hb_room.story + self.add_stories([new_story]) + return df_room
+ +
[docs] def convert_room_3ds_to_2d(self, room_3d_identifiers, tolerance=0.01): + """Convert several 3D Honeybee Rooms on this Building to a Dragonfly Room2Ds. + + This process will add the Room2Ds to an existing Dragonfly Story on the + Building if the Honeybee Room.story matches a Story.display_name on this + object. If not, a new Story on this Building will be initialized. + + Args: + room_3d_identifiers: A list of the identifiers for the 3D honeybee + Rooms on this Building that will be converted to dragonfly Room2Ds. + tolerance: The maximum difference between values at which point vertices + are considered to be the same. (Default: 0.01, suitable for + objects in Meters). + + Returns: + A list of the newly-created Room2D objects from the converted Rooms. + If a given 3D Room is not valid and cannot be converted to a Room2D, + it will not be included in this output. + """ + df_rooms = [] + for r3_id in room_3d_identifiers: + new_r2 = self.convert_room_3d_to_2d(r3_id, tolerance) + if new_r2 is not None: + df_rooms.append(new_r2) + return df_rooms
+ +
[docs] def convert_all_room_3ds_to_2d( + self, extrusion_rooms_only=True, tolerance=0.01, angle_tolerance=1): + """Convert all 3D Honeybee Rooms on this Building to a Dragonfly Room2Ds. + + This process will add the Room2Ds to an existing Dragonfly Story on the + Building if the Honeybee Room.story matches a Story.display_name on this + object. If not, a new Story on this Building will be initialized. + + Args: + extrusion_rooms_only: A boolean to note whether only the 3D Rooms that + can be represented as a Room2D without loss of geometry should be + converted to Room2Ds. When True, all 3D Rooms that are not pure + extrusions will be left as they are. If False, all 3D Rooms in + the model will be translated to Room2D regardless of whether they + are extrusions or not, meaning that there may be some loss of + geometry or simplification of it. + tolerance: The maximum difference between values at which point vertices + are considered to be the same. (Default: 0.01, suitable for + objects in Meters). + angle_tolerance: The max angle difference in degrees that Face3D normals + are allowed to differ from the vertical or horizontal before they + are no longer considered as such. (Default: 1 degree). + + Returns: + A list of the newly-created Room2D objects from the converted Rooms. + """ + # collect the relevant 3D Rooms if extrusion_rooms_only is selected + new_room_3ds = [] + if extrusion_rooms_only: + hb_rooms = [] + for hb_room in self.room_3ds: + if self._is_room_3d_extruded(hb_room, tolerance, angle_tolerance): + hb_rooms.append(hb_room) + else: + new_room_3ds.append(hb_room) + else: + hb_rooms = self.room_3ds + + # convert the relevant 3D Rooms to Room2D + df_rooms = [] + for hb_room in hb_rooms: + # create a Dragonfly Room2D from the Honeybee Room + try: + df_room = Room2D.from_honeybee(hb_room, tolerance) + except Exception: # invalid Honeybee Room that is not a closed solid + new_room_3ds.append(hb_room) + continue + # assign the Room2D to an existing Story or create a new one + for story in self._unique_stories: + if story.display_name == hb_room.story: + story.add_room_2d(df_room) + break + else: # a new Story object has to be initialized + new_story = Story(clean_string(hb_room.story), (df_room,)) + new_story.display_name = hb_room.story + self.add_stories([new_story]) + df_rooms.append(df_room) + + # reset the 3D Rooms on this object + self._room_3ds = tuple(new_room_3ds) + return df_rooms
+ +
[docs] def add_prefix(self, prefix): + """Change the object identifier and all child objects by inserting a prefix. + + This is particularly useful in workflows where you duplicate and edit + a starting object and then want to combine it with the original object + into one Model (like making a model of repeating buildings) since all objects + within a Model must have unique identifiers. + + Args: + prefix: Text that will be inserted at the start of this object's + (and child objects') identifier and display_name. It is recommended + that this prefix be short to avoid maxing out the 100 allowable + characters for dragonfly identifiers. + """ + self.identifier = clean_string('{}_{}'.format(prefix, self.identifier)) + if self._display_name is not None: + self.display_name = '{}_{}'.format(prefix, self.display_name) + self.properties.add_prefix(prefix) + for story in self.unique_stories: + story.add_prefix(prefix) + for room in self.room_3ds: + room.add_prefix(prefix)
+ +
[docs] def sort_stories(self): + """Sort the stories assigned to this Building by their floor heights""" + self._unique_stories = \ + tuple(sorted(self._unique_stories, key=lambda x: x.floor_height))
+ +
[docs] def separate_top_bottom_floors(self): + """Separate top/bottom Stories with non-unity multipliers into their own Stories. + + The resulting first and last Stories will each have a multiplier of 1 and + duplicated middle Stories will be added as needed. This method also + automatically assigns the first story Room2Ds to have a ground contact + floor and the top story Room2Ds to have an outdoor-exposed roof. + + This is particularly helpful when using to_honeybee workflows with + multipliers but one wants to account for the heat exchange of the top + or bottom floors with the ground or outdoors. + """ + # do not do anything if the Building has no 2D Stories + if not self.has_room_2ds: + return + + # empty tuples in case no floors are added + new_ground_floor, new_top_floor = (), () + + # ensure that the bottom floor is unique + if self._unique_stories[0].multiplier != 1: + story = self._unique_stories[0] + new_ground_floor = (self._separated_ground_floor(story),) + story.multiplier = story.multiplier - 1 + story.move(Vector3D(0, 0, story.floor_to_floor_height)) # 2nd floor + + # ensure that the top floor is unique + if self._unique_stories[-1].multiplier != 1: + story = self._unique_stories[-1] + new_top_floor = (self._separated_top_floor(story),) + story.multiplier = story.multiplier - 1 + + # set the unique stories to include any new top and bottom floors + self._unique_stories = new_ground_floor + self._unique_stories + new_top_floor + + # assign the is_ground_contact and is_top_exposed properties + self._unique_stories[0].set_ground_contact() + self._unique_stories[-1].set_top_exposed()
+ +
[docs] def separate_mid_floors(self, tolerance=0.01): + """Separate all Stories with non-unity multipliers into two or three Stories. + + This method automatically assigns the first story Room2Ds to have a ground + contact floor and will separate the top story of each unique story to + have outdoor-exposed roofs when no Room2Ds are sensed above a given room. + + This is particularly helpful when using to_honeybee workflows with + multipliers but one wants to account for the heat exchange of the top + or bottom floors with the ground or outdoors. + + Args: + tolerance: The tolerance that will be used to compute the point within + the floor boundary that is used to check whether there is geometry + above each Room2D. It is recommended that this number not be less + than 1 centimeter to avoid long computation times. Default: 0.01, + suitable for objects in meters. + """ + # do not do anything if the Building has no 2D Stories + if not self.has_room_2ds: + return + + # ensure that the bottom floor is unique + if self._unique_stories[0].multiplier != 1: + story = self._unique_stories[0] + new_ground_floor = self._separated_ground_floor(story) + story.multiplier = story.multiplier - 1 + story.move(Vector3D(0, 0, story.floor_to_floor_height)) # 2nd floor + else: + new_ground_floor = self._unique_stories[0] + if len(self._unique_stories) > 1: + new_ground_floor.set_top_exposed_by_story_above( + self._unique_stories[1], tolerance) + self._unique_stories = self._unique_stories[1:] + + # ensure that the top floor is unique + new_top_floors = [] + for i, story in enumerate(self._unique_stories): + if story.multiplier != 1: + new_top_floor = self._separated_top_floor(story) + story.multiplier = story.multiplier - 1 + try: + new_top_floor.set_top_exposed_by_story_above( + self._unique_stories[i + 1], tolerance) + except IndexError: # this is the last story + new_top_floor.set_top_exposed() + new_top_floors.extend((story, new_top_floor)) + else: + if i == len(self._unique_stories) - 1: + story.set_top_exposed() + else: + story.set_top_exposed_by_story_above( + self._unique_stories[i + 1], tolerance) + new_top_floors.append(story) + + # set the unique stories to include any new top and bottom floors + self._unique_stories = (new_ground_floor,) + tuple(new_top_floors) + + # assign the is_ground_contact and is_top_exposed properties + self._unique_stories[0].set_ground_contact()
+ +
[docs] def set_outdoor_window_parameters(self, window_parameter): + """Set all of the outdoor walls to have the same window parameters.""" + for story in self._unique_stories: + story.set_outdoor_window_parameters(window_parameter)
+ +
[docs] def set_outdoor_shading_parameters(self, shading_parameter): + """Set all of the outdoor walls to have the same shading parameters.""" + for story in self._unique_stories: + story.set_outdoor_shading_parameters(shading_parameter)
+ +
[docs] def to_rectangular_windows(self): + """Convert all of the windows of the Story to the RectangularWindows format.""" + for story in self._unique_stories: + story.to_rectangular_windows()
+ +
[docs] def move(self, moving_vec): + """Move this Building along a vector. + + Args: + moving_vec: A ladybug_geometry Vector3D with the direction and distance + to move the object. + """ + for story in self._unique_stories: + story.move(moving_vec) + for room in self._room_3ds: + room.move(moving_vec) + self.properties.move(moving_vec)
+ +
[docs] def rotate_xy(self, angle, origin): + """Rotate this Building counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle in degrees. + origin: A ladybug_geometry Point3D for the origin around which the + object will be rotated. + """ + for story in self._unique_stories: + story.rotate_xy(angle, origin) + for room in self._room_3ds: + room.rotate_xy(angle, origin) + self.properties.rotate_xy(angle, origin)
+ +
[docs] def reflect(self, plane): + """Reflect this Building across a plane. + + Args: + plane: A ladybug_geometry Plane across which the object will be reflected. + """ + for story in self._unique_stories: + story.reflect(plane) + for room in self._room_3ds: + room.reflect(plane) + self.properties.reflect(plane)
+ +
[docs] def scale(self, factor, origin=None): + """Scale this Building by a factor from an origin point. + + 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). + """ + for story in self._unique_stories: + story.scale(factor, origin) + for room in self._room_3ds: + room.scale(factor, origin) + self.properties.scale(factor, origin)
+ +
[docs] def to_honeybee(self, use_multiplier=True, add_plenum=False, tolerance=0.01, + enforce_adj=True, enforce_solid=True): + """Convert Dragonfly Building to a Honeybee Model. + + Args: + use_multiplier: If True, the multipliers on this Building's Stories will be + passed along to the generated Honeybee Room objects, indicating the + simulation will be run once for each unique room and then results + will be multiplied. If False, full geometry objects will be written + for each and every floor in the building that are represented through + multipliers and all resulting multipliers will be 1. (Default: True). + add_plenum: Boolean to indicate whether ceiling/floor plenums should + be auto-generated for the Rooms. (Default: False). + tolerance: The minimum distance in z values of floor_height and + floor_to_ceiling_height at which adjacent Faces will be split. + Default: 0.01, suitable for objects in meters. + enforce_adj: Boolean to note whether an exception should be raised if + an adjacency between two Room2Ds is invalid (True) or if the invalid + Surface boundary condition should be replaced with an Outdoor + boundary condition (False). If False, any Walls containing + WindowParameters and an illegal boundary condition will also + be replaced with an Outdoor boundary condition. (Default: True). + enforce_solid: Boolean to note whether rooms should be translated + as solid extrusions whenever translating them with custom + roof geometry produces a non-solid result (True) or the non-solid + room geometry should be allowed to remain in the result (False). + The latter is useful for understanding why a particular roof + geometry has produced a non-solid result. (Default: True). + + Returns: + A honeybee Model that represent the Building. + """ + hb_rooms = [] + if use_multiplier: + for story in self._unique_stories: + hb_rooms.extend(story.to_honeybee( + True, add_plenum=add_plenum, tolerance=tolerance, + enforce_adj=enforce_adj, enforce_solid=enforce_solid)) + else: + for story in self.all_stories(): + hb_rooms.extend(story.to_honeybee( + False, add_plenum=add_plenum, tolerance=tolerance, + enforce_adj=enforce_adj, enforce_solid=enforce_solid)) + for room in self.room_3ds: + hb_rooms.append(room) + hb_mod = Model(self.identifier, hb_rooms) + hb_mod._display_name = self._display_name + hb_mod._user_data = self._user_data + return hb_mod
+ +
[docs] def to_dict(self, abridged=False, included_prop=None): + """Return Building as a dictionary. + + Args: + abridged: Boolean to note whether the extension properties of the + object (ie. construction sets) should be included in detail + (False) or just referenced by identifier (True). Default: False. + included_prop: List of properties to filter keys that must be included in + output dictionary. For example ['energy'] will include 'energy' key if + available in properties to_dict. By default all the keys will be + included. To exclude all the keys from extensions use an empty list. + """ + base = {'type': 'Building'} + base['identifier'] = self.identifier + base['display_name'] = self.display_name + if len(self._unique_stories) != 0: + base['unique_stories'] = [s.to_dict(abridged, included_prop) + for s in self._unique_stories] + if len(self._room_3ds) != 0: + base['room_3ds'] = [r.to_dict(abridged, included_prop) + for r in self._room_3ds] + base['properties'] = self.properties.to_dict(abridged, included_prop) + if self.user_data is not None: + base['user_data'] = self.user_data + return base
+ + @property + def to(self): + """Building writer object. + + Use this method to access Writer class to write the building in other formats. + """ + return writer + +
[docs] @staticmethod + def process_alleys(buildings, distance=1.0, adiabatic=False, tolerance=0.01): + """Remove windows from any walls that within a distance of other buildings. + + This method can also optionally set the boundary conditions of these walls to + adiabatic. This is helpful when attempting to account for alleys or parti walls + that may exist between buildings of a denser urban district. + + Note that this staticmethod will edit the buildings in place so it may + be appropriate to duplicate the Buildings before running this method. + + Args: + buildings: Dragonfly Building objects which will have their windows removed + if their walls lie within the distance of another building. + distance: A number for the maximum distance of an alleyway in model + units. If a wall is closer to another Building than this distance, + the windows will be removed. (Default: 1.0; suitable for objects + in meters). + adiabatic: A boolean to note whether the walls that have their windows + removed should also receive an Adiabatic boundary condition. + This is useful when the alleyways are more like parti walls than + distinct pathways that someone could traverse. + """ + # get the adiabatic boundary condition in case we need it + try: + ad_bc = bcs.adiabatic + except AttributeError: # honeybee_energy is not loaded + ad_bc = bcs.outdoors if not adiabatic else bcs.ground + + # get the footprints, heights and bounding points of all of the buildings + story_heights, story_polys = [], [] + for bldg in buildings: + bldg_polys, bldg_s_hgts = [], [] + for story in bldg.unique_stories: + flr_hgt = story.floor_height + bldg_s_hgts.append((flr_hgt, flr_hgt + story.floor_to_floor_height)) + story_foot = story.footprint(tolerance) + st_poly = [Polygon2D((Point2D(p.x, p.y) for p in face.vertices)) + for face in story_foot] + bldg_polys.append(st_poly) + story_heights.append(bldg_s_hgts) + story_polys.append(bldg_polys) + bldg_heights = [b.height for b in buildings] + bldg_pts = [] + for bldg in buildings: + b_min, b_max = bldg.min, bldg.max + center = Point2D((b_min.x + b_max.x) / 2, (b_min.y + b_max.y) / 2) + bldg_pts.append((b_min, center, b_max)) + + # loop through the buildings and set the properties of the relevant walls + for i, bldg in enumerate(buildings): + # first determine the relevant buildings and building heights + rel_st_polys, rel_st_heights, rel_b_heights = [], [], [] + other_indices = list(range(i)) + list(range(i + 1, len(buildings))) + for j in other_indices: + if Building._bound_rect_in_dist(bldg_pts[i], bldg_pts[j], distance): + rel_st_polys.append(story_polys[j]) + rel_st_heights.append(story_heights[j]) + rel_b_heights.append(bldg_heights[j]) + + # then, loop through the story Room2Ds and set properties of relevant walls + for story in bldg.unique_stories: + st_hgt, st_f2f = story.floor_height, story.floor_to_floor_height + st_c_hgt = st_hgt + st_f2f + for rm in story.room_2ds: + zip_r_objs = zip(rm.boundary_conditions, rm.floor_segments_2d, + rm.segment_normals) + new_bcs = list(rm.boundary_conditions) + new_win_pars = list(rm.window_parameters) + for k, (bc, seg, normal) in enumerate(zip_r_objs): + if not isinstance(bc, Outdoors): # nothing to change + continue + seg_mid = seg.midpoint.move(normal * -tolerance) + seg_ray = LineSegment2D.from_sdl(seg_mid, normal, distance) + zip_b_objs = zip(rel_b_heights, rel_st_polys, rel_st_heights) + for bh, rel_poly, rel_hgt in zip_b_objs: + if st_hgt >= bh - tolerance: + continue # story above other bldg; we can ignore it + for o_story, o_h in zip(rel_poly, rel_hgt): + overlap = min((o_h[1], st_c_hgt)) - max((o_h[0], st_hgt)) + if overlap >= st_f2f * 0.33: # more than 1/3 overlap + for o_poly in o_story: + if len(o_poly.intersect_line_ray(seg_ray)) > 0: + # we have found an alleyway! + new_win_pars[k] = None + if adiabatic: + new_bcs[k] = ad_bc + break + # assign the new window parameters and boundary conditions + rm.window_parameters = new_win_pars + rm.boundary_conditions = new_bcs
+ +
[docs] @staticmethod + def district_to_honeybee( + buildings, use_multiplier=True, add_plenum=False, tolerance=0.01, + enforce_adj=True, enforce_solid=True): + """Convert an array of Building objects into a single district honeybee Model. + + Args: + buildings: An array of Building objects to be converted into a + honeybee Model. + use_multiplier: If True, the multipliers on this Building's Stories will be + passed along to the generated Honeybee Room objects, indicating the + simulation will be run once for each unique room and then results + will be multiplied. If False, full geometry objects will be written + for each and every floor in the building that are represented through + multipliers and all resulting multipliers will be 1. (Default: True). + add_plenum: Boolean to indicate whether ceiling/floor plenums should + be auto-generated for the Rooms. (Default: False). + tolerance: The minimum distance in z values of floor_height and + floor_to_ceiling_height at which adjacent Faces will be split. + Default: 0.01, suitable for objects in meters. + enforce_adj: Boolean to note whether an exception should be raised if + an adjacency between two Room2Ds is invalid (True) or if the invalid + Surface boundary condition should be replaced with an Outdoor + boundary condition (False). If False, any Walls containing + WindowParameters and an illegal boundary condition will also + be replaced with an Outdoor boundary condition. (Default: True). + enforce_solid: Boolean to note whether rooms should be translated + as solid extrusions whenever translating them with custom + roof geometry produces a non-solid result (True) or the non-solid + room geometry should be allowed to remain in the result (False). + The latter is useful for understanding why a particular roof + geometry has produced a non-solid result. (Default: True). + + Returns: + A honeybee Model that represent the district. + """ + # create a base model to which everything will be added + base_model = buildings[0].to_honeybee( + use_multiplier, add_plenum=add_plenum, tolerance=tolerance, + enforce_adj=enforce_adj, enforce_solid=enforce_solid) + # loop through each Building, create a model, and add it to the base one + for bldg in buildings[1:]: + base_model.add_model(bldg.to_honeybee( + use_multiplier, add_plenum=add_plenum, tolerance=tolerance, + enforce_adj=enforce_adj, enforce_solid=enforce_solid)) + return base_model
+ +
[docs] @staticmethod + def buildings_to_honeybee( + buildings, context_shades=None, shade_distance=None, + use_multiplier=True, add_plenum=False, cap=False, tolerance=0.01, + enforce_adj=True, enforce_solid=True): + """Convert an array of Buildings into several honeybee Models with self-shading. + + Each input Building will be exported into its own Model. For each Model, + the other input Buildings will appear as context shade geometry. Thus, + each Model is its own simulate-able unit accounting for the total + self-shading of the input Buildings. + + Args: + buildings: An array of Building objects to be converted into honeybee + Models that account for their own shading of one another. + context_shades: An optional array of ContextShade objects that will be + added to the honeybee Models if their bounding box overlaps with a + given building within the shade_distance. + shade_distance: An optional number to note the distance beyond which other + objects' shade should not be exported into a given Model. This is + helpful for reducing the simulation run time of each Model when other + connected buildings are too far away to have a meaningful impact on + the results. If None, all other buildings will be included as context + shade in each and every Model. Set to 0 to exclude all neighboring + buildings from the resulting models. Default: None. + use_multiplier: If True, the multipliers on this Building's Stories will be + passed along to the generated Honeybee Room objects, indicating the + simulation will be run once for each unique room and then results + will be multiplied. If False, full geometry objects will be written + for each and every floor in the building that are represented through + multipliers and all room multipliers will be 1. (Default: True). + add_plenum: Boolean to indicate whether ceiling/floor plenums should + be auto-generated for the Rooms. (Default: False). + cap: Boolean to note whether building shade representations should be capped + with a top face. Usually, this is not necessary to account for + blocked sun and is only needed when it's important to account for + reflected sun off of roofs. (Default: False). + tolerance: The minimum distance in z values of floor_height and + floor_to_ceiling_height at which adjacent Faces will be split. + Default: 0.01, suitable for objects in meters. + enforce_adj: Boolean to note whether an exception should be raised if + an adjacency between two Room2Ds is invalid (True) or if the invalid + Surface boundary condition should be replaced with an Outdoor + boundary condition (False). If False, any Walls containing + WindowParameters and an illegal boundary condition will also + be replaced with an Outdoor boundary condition. (Default: True). + enforce_solid: Boolean to note whether rooms should be translated + as solid extrusions whenever translating them with custom + roof geometry produces a non-solid result (True) or the non-solid + room geometry should be allowed to remain in the result (False). + The latter is useful for understanding why a particular roof + geometry has produced a non-solid result. (Default: True). + + Returns: + A list of honeybee Models that represent the Building. + """ + # create lists with all context representations of the buildings + shade + bldg_shades, bldg_pts, con_shades, con_pts = Building._honeybee_shades( + buildings, context_shades, shade_distance, cap, tolerance) + # loop through each Building and create a model + models = [] # list to be filled with Honeybee Models + num_bldg = len(buildings) + for i, bldg in enumerate(buildings): + model = bldg.to_honeybee( + use_multiplier, add_plenum=add_plenum, tolerance=tolerance, + enforce_adj=enforce_adj, enforce_solid=enforce_solid) + Building._add_context_to_honeybee(model, bldg_shades, bldg_pts, con_shades, + con_pts, shade_distance, num_bldg, i) + models.append(model) # append to the final list of Models + return models
+ +
[docs] @staticmethod + def stories_to_honeybee( + buildings, context_shades=None, shade_distance=None, + use_multiplier=True, add_plenum=False, cap=False, tolerance=0.01, + enforce_adj=True, enforce_solid=True): + """Convert an array of Buildings into one honeybee Model per story. + + Each Story of each input Building will be exported into its own Model. For each + Honeybee Model, the other input Buildings will appear as context shade geometry + as will all of the other stories of the same building. Thus, each Model + is its own simulate-able unit accounting for the total self-shading of + the input Buildings. + + Args: + buildings: An array of Building objects to be converted into an array of + honeybee Models with one story per model. + context_shades: An optional array of ContextShade objects that will be + added to the honeybee Models if their bounding box overlaps with a + given building within the shade_distance. + shade_distance: An optional number to note the distance beyond which other + objects' shade should not be exported into a given Model. This is + helpful for reducing the simulation run time of each Model when other + connected buildings are too far away to have a meaningful impact on + the results. If None, all other buildings will be included as context + shade in each and every Model. Set to 0 to exclude all neighboring + buildings from the resulting models. Default: None. + use_multiplier: If True, the multipliers on this Building's Stories will be + passed along to the generated Honeybee Room objects, indicating the + simulation will be run once for each unique room and then results + will be multiplied. If False, full geometry objects will be written + for each and every floor in the building that are represented through + multipliers and all room multipliers will be 1. (Default: True). + add_plenum: Boolean to indicate whether ceiling/floor plenums should + be auto-generated for the Rooms. (Default: False). + cap: Boolean to note whether building shade representations should be capped + with a top face. Usually, this is not necessary to account for + blocked sun and is only needed when it's important to account for + reflected sun off of roofs. (Default: False). + tolerance: The minimum distance in z values of floor_height and + floor_to_ceiling_height at which adjacent Faces will be split. + Default: 0.01, suitable for objects in meters. + enforce_adj: Boolean to note whether an exception should be raised if + an adjacency between two Room2Ds is invalid (True) or if the invalid + Surface boundary condition should be replaced with an Outdoor + boundary condition (False). If False, any Walls containing + WindowParameters and an illegal boundary condition will also + be replaced with an Outdoor boundary condition. (Default: True). + enforce_solid: Boolean to note whether rooms should be translated + as solid extrusions whenever translating them with custom + roof geometry produces a non-solid result (True) or the non-solid + room geometry should be allowed to remain in the result (False). + The latter is useful for understanding why a particular roof + geometry has produced a non-solid result. (Default: True). + + Returns: + A list of honeybee Models that represent the Stories. + """ + # create lists with all context representations of the buildings + shade + bldg_shades, bldg_pts, con_shades, con_pts = Building._honeybee_shades( + buildings, context_shades, shade_distance, cap, tolerance) + # loop through each Building and create a model + models = [] # list to be filled with Honeybee Models + num_bldg = len(buildings) + for i, bldg in enumerate(buildings): + dummy_model = Model(bldg.identifier) # blank model to hold context shade + Building._add_context_to_honeybee( + dummy_model, bldg_shades, bldg_pts, con_shades, con_pts, + shade_distance, num_bldg, i) + bldg_con = list(dummy_model.orphaned_shades) + if use_multiplier: + for j, story in enumerate(bldg.unique_stories): + hb_rooms = story.to_honeybee( + True, add_plenum, tolerance=tolerance, + enforce_adj=enforce_adj, enforce_solid=enforce_solid) + if bldg.has_room_3ds: + hb_rooms.extend(bldg.room_3ds_by_story(story.display_name)) + shds = bldg_con + bldg.shade_representation(j, cap, False, tolerance) + model = Model(story.identifier, hb_rooms, orphaned_shades=shds) + model.display_name = story.display_name + models.append(model) # append to the final list of Models + else: + self_shds = [story.shade_representation(cap, tolerance) + for story in bldg.unique_stories] + full_shades = [] + for j, story in enumerate(bldg.unique_stories): + for k in range(story.multiplier): + mult_shd = story.shade_representation_multiplier( + k, cap=cap, tolerance=tolerance) + mult_shd.extend([s for s_ar in self_shds[:j] for s in s_ar]) + mult_shd.extend([s for s_ar in self_shds[j + 1:] for s in s_ar]) + full_shades.append(mult_shd) + for story, shades in zip(bldg.all_stories(), full_shades): + hb_rooms = story.to_honeybee( + True, add_plenum, tolerance=tolerance, + enforce_adj=enforce_adj, enforce_solid=enforce_solid) + if bldg.has_room_3ds: + hb_rooms.extend(bldg.room_3ds_by_story(story.display_name)) + shds = bldg_con + shades + model = Model(story.identifier, hb_rooms, orphaned_shades=shds) + model.display_name = story.display_name + models.append(model) # append to the final list of Models + if bldg.has_room_3ds: # organize them by story and add them + accounted_for = bldg.room_2d_story_names + r3_story_dict = bldg._story_dict_room_3d() + shds = bldg_con + bldg.shade_representation( + None, cap, False, tolerance) + for story_id, hb_rooms in r3_story_dict.items(): + if story_id not in accounted_for: + model = Model(story_id, hb_rooms, orphaned_shades=shds) + models.append(model) # append to the final list of Models + return models
+ + def _story_dict_room_3d(self): + """Get a dictionary of 3D Honeybee Rooms organized by story.""" + r3_story_dict = {} + for room in self._room_3ds: + try: + r3_story_dict[room.story].append(room) + except KeyError: + r3_story_dict[room.story] = [room] + return r3_story_dict + + def _lowest_story_room_3ds(self): + """Get a list of Honeybee Rooms for the lowest story of the Building. + + Note that this method should typically only be used when the Building is + composed entirely of 3D Honeybee Rooms. + """ + r3_story_dict = self._story_dict_room_3d() + floor_hgts, floor_rooms = [], [] + for rooms in r3_story_dict.values(): + flr_hgt = sum(r.average_floor_height for r in rooms) / len(rooms) + floor_hgts.append(flr_hgt) + floor_rooms.append(rooms) + sort_rooms = [rs for _, rs in sorted(zip(floor_hgts, floor_rooms), + key=lambda pair: pair[0])] + return sort_rooms[0] + + @staticmethod + def _is_room_3d_extruded(hb_room, tolerance, angle_tolerance): + """Test if a 3D Room is a pure extrusion. + + Pure extrusions can be converted into Room2Ds without any loss or + simplification of geometry. + + Args: + hb_room: The 3D Honeybee Room to be tested. + tolerance: The absolute tolerance with which the Room geometry will + be evaluated. + angle_tolerance: The angle tolerance at which the geometry will + be evaluated in degrees. + + Returns: + True if the 3D Room is a pure extrusion. False if not. + """ + # set up the parameters for evaluating vertical or horizontal + vert_vec = Vector3D(0, 0, 1) + min_v_ang = math.radians(angle_tolerance) + max_v_ang = math.pi - min_v_ang + min_h_ang = (math.pi / 2) - min_v_ang + max_h_ang = (math.pi / 2) + min_v_ang + + # loop through the 3D Room faces and test them + for face in hb_room._faces: + try: # first make sure that the geometry is not degenerate + clean_geo = face.geometry.remove_colinear_vertices(tolerance) + v_ang = clean_geo.normal.angle(vert_vec) + if v_ang <= min_v_ang or v_ang >= max_v_ang: + continue + elif min_h_ang <= v_ang <= max_h_ang: + continue + return False + except AssertionError: # degenerate face to ignore + pass + return True + + @staticmethod + def _honeybee_shades(buildings, context_shades, shade_distance, cap, tolerance): + """Get lists of Honeybee shades from Building and ContextShade objects.""" + bldg_shades, bldg_pts = [], [] + con_shades, con_pts = [], [] + if shade_distance is None or shade_distance > 0: + for bldg in buildings: + b_shades = bldg.shade_representation( + cap=cap, include_room3ds=True, tolerance=tolerance) + bldg_shades.append(b_shades) + b_min, b_max = bldg.min, bldg.max + center = Point2D((b_min.x + b_max.x) / 2, (b_min.y + b_max.y) / 2) + bldg_pts.append((b_min, center, b_max)) + if context_shades is not None: + for con in context_shades: + con_shades.append(con.to_honeybee()) + c_min, c_max = con.min, con.max + center = Point2D((c_min.x + c_max.x) / 2, (c_min.y + c_max.y) / 2) + con_pts.append((c_min, center, c_max)) + return bldg_shades, bldg_pts, con_shades, con_pts + + @staticmethod + def _add_context_to_honeybee(model, bldg_shades, bldg_pts, con_shades, con_pts, + shade_distance, num_bldg, i): + """Add context shades to a Honeybee Model based on shade distance.""" + if shade_distance is None: # add all other bldg shades to the model + for j in xrange(i + 1, num_bldg): # buildings before this one + for shd in bldg_shades[j]: + model.add_shade(shd) + for k in xrange(i): # buildings after this one + for shd in bldg_shades[k]: + model.add_shade(shd) + for c_shade in con_shades: # context shades + for shd in c_shade: + if isinstance(shd, Shade): + model.add_shade(shd) + else: + model.add_shade_mesh(shd) + elif shade_distance > 0: # add only shade within the distance + for j in xrange(i + 1, num_bldg): # buildings before this one + if Building._bound_rect_in_dist(bldg_pts[i], bldg_pts[j], + shade_distance): + for shd in bldg_shades[j]: + model.add_shade(shd) + for k in xrange(i): # buildings after this one + if Building._bound_rect_in_dist(bldg_pts[i], bldg_pts[k], + shade_distance): + for shd in bldg_shades[k]: + model.add_shade(shd) + for s in xrange(len(con_shades)): # context shades + if Building._bound_rect_in_dist(bldg_pts[i], con_pts[s], + shade_distance): + for shd in con_shades[s]: + if isinstance(shd, Shade): + model.add_shade(shd) + else: + model.add_shade_mesh(shd) + + @staticmethod + def _generate_room_2ds(face3d_array, flr_to_ceiling, perim_offset, + bldg_id, flr_count, tolerance): + """Generate Room2D objects given geometry and information about their parent. + + Args: + face3d_array: An array of Face3D objects to be turned into a Story's Room2Ds. + flr_to_ceiling: The floor-to-ceiling height to use for all the Room2Ds. + perim_offset: A perimeter offset to be used to subdivide Face3Ds + bldg_id: Text for the identifier to which the rooms belong. + flr_count: Integer for the which story the building belongs to. + tolerance: Tolerance to be used in the creation of the Room2Ds. + """ + # if there is a non-zero perimeter offset, separate core vs. perimeter zones + if perim_offset != 0: + assert perim_offset > 0, 'perimeter_offset cannot be less than than 0.' + new_face3d_array = [] + for floor_face in face3d_array: + try: + floor_face = floor_face.remove_colinear_vertices(tolerance) + perimeter, core = perimeter_core_subfaces( + floor_face, perim_offset, tolerance) + new_face3d_array.extend(perimeter) + new_face3d_array.extend(core) + except Exception as e: # the generation of the polyskel failed + print('Core/perimeter generation failed:\n{}'.format(e)) + new_face3d_array.append(floor_face) # just use existing floor + face3d_array = new_face3d_array # replace with offset core/perimeter + + # create the Room2D objects + room_2ds = [] + for i, room_geo in enumerate(face3d_array): + room = Room2D('{}_Floor{}_Room{}'.format(bldg_id, flr_count, i + 1), + room_geo, flr_to_ceiling, tolerance=tolerance) + room_2ds.append(room) + + # solve for interior adjacency if there core/perimeter zoning was requested + if perim_offset != 0: + room_2ds = Room2D.intersect_adjacency( + room_2ds, tolerance, preserve_wall_props=False) + Room2D.solve_adjacency(room_2ds, tolerance) + return room_2ds + + @staticmethod + def _is_story_equivalent(face1, face2, tolerance): + """Check whether area, XY centerpoint and XY first point match between Face3D. + + Args: + face1: First Face3D to check. + face2: Second Face3D to check. + tolerance: The maximum difference between x, y, and z values at which + point vertices are considered to be the same. + + Returns: + True if face1 is geometrically equivalent to face 2 else False. + """ + # check wether the center points match within tolerance. + cent1 = face1.center + cent2 = face2.center + if abs(cent1.x - cent2.x) > tolerance or abs(cent1.y - cent2.y) > tolerance: + return False + + # check wether the point at start matches within tolerance + start1 = face1[0] + start2 = face2[0] + if abs(start1.x - start2.x) > tolerance or abs(start1.y - start2.y) > tolerance: + return False + + # check whether areas match within tolerance + area_tol = tolerance ** 2 + if abs(face1.area - face2.area) > area_tol: + return False + + return True + + @staticmethod + def _bound_rect_in_dist(bound_pts1, bound_pts2, distance): + """Check if the bounding rectangles of two footprints overlap within a distance. + + Checking the overlap of the bounding rectangles is extremely quick given this + method's use of the Separating Axis Theorem. + + Args: + bound_pts1: An array of Point2Ds (min, center, max) for the first footprint. + bound_pts2: An array of Point2Ds (min, center, max) for the second footprint. + distance: Acceptable distance between the two bounding rectangles. + """ + # Bounding rectangle check using the Separating Axis Theorem + polygon1_width = bound_pts1[2].x - bound_pts1[0].x + polygon2_width = bound_pts2[2].x - bound_pts2[0].x + dist_btwn_x = abs(bound_pts1[1].x - bound_pts2[1].x) + x_gap_btwn_rect = dist_btwn_x - (0.5 * polygon1_width) - (0.5 * polygon2_width) + + polygon1_height = bound_pts1[2].y - bound_pts1[0].y + polygon2_height = bound_pts2[2].y - bound_pts2[0].y + dist_btwn_y = abs(bound_pts1[1].y - bound_pts2[1].y) + y_gap_btwn_rect = dist_btwn_y - (0.5 * polygon1_height) - (0.5 * polygon2_height) + + if x_gap_btwn_rect > distance or y_gap_btwn_rect > distance: + return False # no overlap + return True # overlap exists + + @staticmethod + def _separated_ground_floor(base_story): + """Get a separated ground floor from a base_story.""" + bottom = base_story.duplicate() # generate a new bottom floor + bottom.multiplier = 1 + bottom.add_prefix('Ground') + return bottom + + @staticmethod + def _separated_top_floor(base_story): + """Get a separated top floor from a base_story.""" + top = base_story.duplicate() # generate a new top floor + move_vec = Vector3D(0, 0, top.floor_to_floor_height * (top.multiplier - 1)) + top.move(move_vec) + top.multiplier = 1 + top.add_prefix('Top') + return top + + def __copy__(self): + new_b = Building( + self.identifier, tuple(story.duplicate() for story in self._unique_stories), + tuple(room.duplicate() for room in self._room_3ds)) + new_b._display_name = self._display_name + new_b._user_data = None if self.user_data is None else self.user_data.copy() + new_b._properties._duplicate_extension_attr(self._properties) + return new_b + + def __len__(self): + return len(self._unique_stories) + + def __getitem__(self, key): + return self._unique_stories[key] + + def __iter__(self): + return iter(self._unique_stories) + + def __repr__(self): + return 'Building: %s' % self.display_name
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/colorobj.html b/docs/_modules/dragonfly/colorobj.html new file mode 100644 index 00000000..dd2ab49b --- /dev/null +++ b/docs/_modules/dragonfly/colorobj.html @@ -0,0 +1,1068 @@ + + + + + + + dragonfly.colorobj — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.colorobj

+# coding=utf-8
+"""Module for coloring geometry with attributes."""
+from __future__ import division
+
+from .room2d import Room2D
+
+from honeybee.colorobj import _ColorObject
+from ladybug_geometry.geometry3d.pointvector import Point3D
+
+
+
[docs]class ColorRoom2D(_ColorObject): + """Object for visualizing Room2D-level attributes. + + Args: + room_2ds: An array of honeybee Room2Ds, which will be colored with the attribute. + attr_name: A text string of an attribute that the input rooms should have. + This can have '.' that separate the nested attributes from one another. + For example, 'properties.energy.program_type'. + legend_parameters: An optional LegendParameter object to change the display + of the ColorRoom2D (Default: None). + + Properties: + * room_2ds + * attr_name + * legend_parameters + * attr_name_end + * attributes + * attributes_unique + * attributes_original + * floor_faces + * graphic_container + * min_point + * max_point + """ + __slots__ = ('_room_2ds',) + + def __init__(self, room_2ds, attr_name, legend_parameters=None): + """Initialize ColorRoom2D.""" + try: # check the input room_2ds + room_2ds = tuple(room_2ds) + except TypeError: + raise TypeError( + 'Input room_2ds must be an array. Got {}.'.format(type(room_2ds))) + assert len(room_2ds) > 0, 'ColorRoom2D must have at least one room.' + for room in room_2ds: + assert isinstance(room, Room2D), 'Expected dragonfly Room2D for ' \ + 'ColorRoom2D room_2ds. Got {}.'.format(type(room)) + self._room_2ds = room_2ds + self._calculate_min_max(room_2ds) + + # assign the legend parameters of this object + self.legend_parameters = legend_parameters + + # get the attributes of the input rooms + self._process_attribute_name(attr_name) + self._process_attributes(room_2ds) + + @property + def room_2ds(self): + """Get a tuple of dragonfly Room2Ds assigned to this object.""" + return self._room_2ds + + @property + def floor_faces(self): + """Get an list with a Face3Ds for each Room2D.""" + return [room.floor_geometry for room in self._room_2ds] + + def _calculate_min_max(self, hb_objs): + """Calculate maximum and minimum Point3D for a set of rooms.""" + st_rm_min = hb_objs[0].floor_geometry.min + st_rm_max = hb_objs[0].floor_geometry.max + min_pt = [st_rm_min.x, st_rm_min.y, st_rm_min.z] + max_pt = [st_rm_max.x, st_rm_max.y, st_rm_max.z] + + for room in hb_objs[1:]: + rm_min, rm_max = room.floor_geometry.min, room.floor_geometry.max + if rm_min.x < min_pt[0]: + min_pt[0] = rm_min.x + if rm_min.y < min_pt[1]: + min_pt[1] = rm_min.y + if rm_min.z < min_pt[2]: + min_pt[2] = rm_min.z + if rm_max.x > max_pt[0]: + max_pt[0] = rm_max.x + if rm_max.y > max_pt[1]: + max_pt[1] = rm_max.y + if rm_max.z > max_pt[2]: + max_pt[2] = rm_max.z + + self._min_point = Point3D(min_pt[0], min_pt[1], min_pt[2]) + self._max_point = Point3D(max_pt[0], max_pt[1], max_pt[2]) + + def __repr__(self): + """Color Room2D representation.""" + return 'Color Room2D: ({} Rooms() ({})'.format( + len(self._room_2ds), self.attr_name_end)
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/config.html b/docs/_modules/dragonfly/config.html new file mode 100644 index 00000000..37feacb8 --- /dev/null +++ b/docs/_modules/dragonfly/config.html @@ -0,0 +1,1073 @@ + + + + + + + dragonfly.config — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.config

+"""Dragonfly configurations.
+
+Import this into every module where access configurations are needed.
+
+Usage:
+
+.. code-block:: python
+
+    from dragonfly.config import folders
+    print(folders.dragonfly_schema_version_str)
+"""
+
+import os
+
+
+
[docs]class Folders(object): + """Dragonfly folders. + + Properties: + * dragonfly_core_version + * dragonfly_core_version_str + * dragonfly_schema_version + * dragonfly_schema_version_str + * python_package_path + """ + + def __init__(self): + # search for the version of dragonfly-core and dragonfly-schema + self._dragonfly_core_version = self._find_dragonfly_core_version() + self._dragonfly_schema_version = self._find_dragonfly_schema_version() + + @property + def dragonfly_core_version(self): + """Get a tuple for the installed version of dragonfly-core (eg. (1, 47, 26)). + + This will be None if the version could not be sensed (it was not installed + via pip). + """ + return self._dragonfly_core_version + + @property + def dragonfly_core_version_str(self): + """Get a string for the installed version of dragonfly-core (eg. "1.47.26"). + + This will be None if the version could not be sensed. + """ + if self._dragonfly_core_version is not None: + return '.'.join([str(item) for item in self._dragonfly_core_version]) + return None + + @property + def dragonfly_schema_version(self): + """Get a tuple for the installed version of dragonfly-schema (eg. (1, 5, 27)). + + This will be None if the version could not be sensed (it was not installed + via pip) or if no dragonfly-schema installation was found next to the + dragonfly-core installation. + """ + return self._dragonfly_schema_version + + @property + def dragonfly_schema_version_str(self): + """Get a string for the installed version of dragonfly-schema (eg. "1.5.27"). + + This will be None if the version could not be sensed. + """ + if self._dragonfly_schema_version is not None: + return '.'.join([str(item) for item in self._dragonfly_schema_version]) + return None + + @property + def python_package_path(self): + """Get the path to where this Python package is installed.""" + return os.path.split(os.path.dirname(__file__))[0] + + def _find_dragonfly_core_version(self): + """Get a tuple of 3 integers for the version of dragonfly_core if installed.""" + return self._find_package_version('dragonfly_core') + + def _find_dragonfly_schema_version(self): + """Get a tuple of 3 integers for the version of dragonfly_schema if installed.""" + return self._find_package_version('dragonfly_schema') + + def _find_package_version(self, package_name): + """Get a tuple of 3 integers for the version of a package.""" + df_info_folder = None + for item in os.listdir(self.python_package_path): + if item.startswith(package_name + '-') and item.endswith('.dist-info'): + if os.path.isdir(os.path.join(self.python_package_path, item)): + df_info_folder = item + break + if df_info_folder is not None: + df_info_folder = df_info_folder.replace('.dist-info', '') + ver = ''.join(s for s in df_info_folder if (s.isdigit() or s == '.')) + if ver: # version was found in the file path name + return tuple(int(d) for d in ver.split('.')) + return None
+ + +"""Object possesing all key folders within the configuration.""" +folders = Folders() +
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/context.html b/docs/_modules/dragonfly/context.html new file mode 100644 index 00000000..28341ba0 --- /dev/null +++ b/docs/_modules/dragonfly/context.html @@ -0,0 +1,1299 @@ + + + + + + + dragonfly.context — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.context

+# coding: utf-8
+"""Dragonfly Context Shade."""
+from __future__ import division
+import math
+
+from ladybug_geometry.geometry3d import Point3D, Face3D, Mesh3D
+
+from honeybee.shade import Shade
+from honeybee.shademesh import ShadeMesh
+from honeybee.typing import clean_string
+
+from ._base import _BaseGeometry
+from .properties import ContextShadeProperties
+import dragonfly.writer.context as writer
+
+
+
[docs]class ContextShade(_BaseGeometry): + """A Context Shade object defined by an array of Face3Ds and/or Mesh3Ds. + + Args: + identifier: Text string for a unique ContextShade ID. Must be < 100 characters + and not contain any spaces or special characters. + geometry: An array of ladybug_geometry Face3D and/or Mesh3D objects + that together represent the context shade. + is_detached: Boolean to note whether this object is detached from other + geometry. Cases where this should be True include shade representing + surrounding buildings or context. (Default: True). + + Properties: + * identifier + * display_name + * geometry + * is_detached + * area + * min + * max + * user_data + """ + __slots__ = ('_geometry', '_is_detached') + + def __init__(self, identifier, geometry, is_detached=True): + """Initialize ContextShade.""" + _BaseGeometry.__init__(self, identifier) # process the identifier + + # process the geometry + if not isinstance(geometry, tuple): + geometry = tuple(geometry) + assert len(geometry) > 0, 'ContextShade must have at least one geometry.' + for shd_geo in geometry: + assert isinstance(shd_geo, (Face3D, Mesh3D)), 'Expected ladybug_geometry ' \ + 'Face3D or Mesh3D. Got {}'.format(type(shd_geo)) + self._geometry = geometry + self.is_detached = is_detached + + self._properties = ContextShadeProperties(self) # properties for extensions + +
[docs] @classmethod + def from_dict(cls, data): + """Initialize an ContextShade from a dictionary. + + Args: + data: A dictionary representation of an ContextShade object. + """ + # check the type of dictionary + assert data['type'] == 'ContextShade', 'Expected ContextShade dictionary. ' \ + 'Got {}.'.format(data['type']) + + is_detached = data['is_detached'] if 'is_detached' in data else True + geometry = [] + for shd_geo in data['geometry']: + if shd_geo['type'] == 'Face3D': + geometry.append(Face3D.from_dict(shd_geo)) + else: + geometry.append(Mesh3D.from_dict(shd_geo)) + shade = cls(data['identifier'], geometry, is_detached) + if 'display_name' in data and data['display_name'] is not None: + shade.display_name = data['display_name'] + if 'user_data' in data and data['user_data'] is not None: + shade.user_data = data['user_data'] + + if data['properties']['type'] == 'ContextShadeProperties': + shade.properties._load_extension_attr_from_dict(data['properties']) + return shade
+ +
[docs] @classmethod + def from_honeybee(cls, shade): + """Initialize an ContextShade from a Honeybee Shade or ShadeMesh. + + Args: + shade: A Honeybee Shade or ShadeMesh object. + """ + con_shade = cls(shade.identifier, [shade.geometry], shade.is_detached) + con_shade._display_name = shade.display_name + con_shade._user_data = None if shade.user_data is None \ + else shade.user_data.copy() + con_shade.properties.from_honeybee(shade.properties) + return con_shade
+ + @property + def geometry(self): + """Get a tuple of Face3D and/or Mesh3D objects that represent the context shade. + """ + return self._geometry + + @property + def is_detached(self): + """Get or set a boolean for whether this object is detached from other geometry. + """ + return self._is_detached + + @is_detached.setter + def is_detached(self, value): + try: + self._is_detached = bool(value) + except TypeError: + raise TypeError( + 'Expected boolean for ContextShade.is_detached. Got {}.'.format(value)) + + @property + def area(self): + """Get a number for the total surface area of the ContextShade.""" + return sum([geo.area for geo in self._geometry]) + + @property + def min(self): + """Get a Point2D for the min bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this ContextShade is in + proximity to other objects. + """ + return self._calculate_min(self._geometry) + + @property + def max(self): + """Get a Point2D for the max bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this ContextShade is in + proximity to other objects. + """ + return self._calculate_max(self._geometry) + +
[docs] def add_prefix(self, prefix): + """Change the identifier of this object by inserting a prefix. + + This is particularly useful in workflows where you duplicate and edit + a starting object and then want to combine it with the original object + into one Model (like making a model of repeated shades) since all objects + within a Model must have unique identifiers. + + Args: + prefix: Text that will be inserted at the start of this object's identifier + and display_name. It is recommended that this prefix be short to + avoid maxing out the 100 allowable characters for dragonfly identifiers. + """ + self._identifier = clean_string('{}_{}'.format(prefix, self.identifier)) + self.display_name = '{}_{}'.format(prefix, self.display_name) + self.properties.add_prefix(prefix)
+ +
[docs] def move(self, moving_vec): + """Move this ContextShade along a vector. + + Args: + moving_vec: A ladybug_geometry Vector3D with the direction and distance + to move the object. + """ + self._geometry = tuple(shd_geo.move(moving_vec) for shd_geo in self._geometry) + self.properties.move(moving_vec)
+ +
[docs] def rotate_xy(self, angle, origin): + """Rotate this ContextShade counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle in degrees. + origin: A ladybug_geometry Point3D for the origin around which the + object will be rotated. + """ + self._geometry = tuple(shd_geo.rotate_xy(math.radians(angle), origin) + for shd_geo in self._geometry) + self.properties.rotate_xy(angle, origin)
+ +
[docs] def reflect(self, plane): + """Reflect this ContextShade across a plane. + + Args: + plane: A ladybug_geometry Plane across which the object will be reflected. + """ + self._geometry = tuple(shd_geo.reflect(plane.n, plane.o) + for shd_geo in self._geometry) + self.properties.reflect(plane)
+ +
[docs] def scale(self, factor, origin=None): + """Scale this ContextShade by a factor from an origin point. + + 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). + """ + self._geometry = tuple(shd_geo.scale(factor, origin) + for shd_geo in self._geometry) + self.properties.scale(factor, origin)
+ +
[docs] def snap_to_grid(self, grid_increment, tolerance=0.01): + """Snap this object to the nearest XY grid node defined by an increment. + + Note that, even though ContextShade geometry is defined using 3D vertices, + only the X and Y coordinates will be snapped, which is consistent with + how the Room2D.snap_to_grid method works. + + All properties assigned to the ContextShade will be preserved and any + degenerate geometries are automatically cleaned out of the result. + + Args: + grid_increment: A positive number for dimension of each grid cell. This + typically should be equal to the tolerance or larger but should + not be larger than the smallest detail of the ContextShade that you + wish to resolve. + tolerance: The minimum difference between the coordinate values at + which they are considered co-located. (Default: 0.01, + suitable for objects in meters). + """ + # define a list to hold all of the new geometry + new_geometry = [] + + # loop through the current geometry and snap the vertices + for geo in self._geometry: + if isinstance(geo, Face3D): + new_boundary, new_holes = [], None + for pt in geo.boundary: + new_x = grid_increment * round(pt.x / grid_increment) + new_y = grid_increment * round(pt.y / grid_increment) + new_boundary.append(Point3D(new_x, new_y, pt.z)) + if geo.holes is not None: + new_holes = [] + for hole in geo.holes: + new_hole = [] + for pt in hole: + new_x = grid_increment * round(pt.x / grid_increment) + new_y = grid_increment * round(pt.y / grid_increment) + new_hole.append(Point3D(new_x, new_y, pt.z)) + new_holes.append(new_hole) + n_geo = Face3D(new_boundary, geo.plane, new_holes) + try: # catch all degeneracy in the process + n_geo = n_geo.remove_duplicate_vertices(tolerance) + new_geometry.append(n_geo) + except AssertionError: # degenerate geometry + pass + elif isinstance(geo, Mesh3D): + new_vertices = [] + for pt in geo.vertices: + new_x = grid_increment * round(pt.x / grid_increment) + new_y = grid_increment * round(pt.y / grid_increment) + new_vertices.append(Point3D(new_x, new_y, pt.z)) + n_geo = Mesh3D(new_vertices, geo.faces) + new_geometry.append(n_geo) + + # rebuild the new floor geometry and assign it to the Room2D + if len(new_geometry) != 0: + self._geometry = new_geometry
+ +
[docs] def to_honeybee(self): + """Convert Dragonfly ContextShade to a list of Honeybee Shades and ShadeMeshes. + """ + shades = [] + for i, shd_geo in enumerate(self._geometry): + if isinstance(shd_geo, Face3D): + shade = Shade('{}_{}'.format(self.identifier, i), shd_geo, + is_detached=self.is_detached) + shade._properties = self.properties.to_honeybee(shade, False) + else: + shade = ShadeMesh('{}_{}'.format(self.identifier, i), shd_geo, + is_detached=self.is_detached) + shade._properties = self.properties.to_honeybee(shade, True) + shade.display_name = self.display_name + shade.user_data = None if self.user_data is None else self.user_data.copy() + shades.append(shade) + return shades
+ +
[docs] def to_dict(self, abridged=False, included_prop=None): + """Return ContextShade as a dictionary. + + Args: + abridged: Boolean to note whether the extension properties of the + object (ie. materials, transmittance schedule) should be included in + detail (False) or just referenced by identifier (True). Default: False. + included_prop: List of properties to filter keys that must be included in + output dictionary. For example ['energy'] will include 'energy' key if + available in properties to_dict. By default all the keys will be + included. To exclude all the keys from extensions use an empty list. + """ + base = {'type': 'ContextShade'} + base['identifier'] = self.identifier + base['display_name'] = self.display_name + base['properties'] = self.properties.to_dict(abridged, included_prop) + base['geometry'] = [shd_geo.to_dict() for shd_geo in self._geometry] + if not self.is_detached: + base['is_detached'] = self.is_detached + if self.user_data is not None: + base['user_data'] = self.user_data + return base
+ + @property + def to(self): + """ContextShade writer object. + + Use this method to access Writer class to write the context in other formats. + """ + return writer + + def __copy__(self): + new_shd = ContextShade(self.identifier, self._geometry, self.is_detached) + new_shd._display_name = self.display_name + new_shd._user_data = None if self.user_data is None else self.user_data.copy() + new_shd._properties._duplicate_extension_attr(self._properties) + return new_shd + + def __len__(self): + return len(self._geometry) + + def __getitem__(self, key): + return self._geometry[key] + + def __iter__(self): + return iter(self._geometry) + + def __repr__(self): + return 'ContextShade: %s' % self.display_name
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/dictutil.html b/docs/_modules/dragonfly/dictutil.html new file mode 100644 index 00000000..25fdc13b --- /dev/null +++ b/docs/_modules/dragonfly/dictutil.html @@ -0,0 +1,1032 @@ + + + + + + + dragonfly.dictutil — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.dictutil

+# coding=utf-8
+"""Utilities to convertint any dictionary to Python objects.
+
+Note that importing this module will import almost all modules within the
+library in order to be able to re-serialize almost any dictionary produced
+from the library.
+"""
+from dragonfly.model import Model
+from dragonfly.building import Building
+from dragonfly.story import Story
+from dragonfly.room2d import Room2D
+from dragonfly.context import ContextShade
+import dragonfly.windowparameter as dfw
+import dragonfly.shadingparameter as dfs
+
+import honeybee.boundarycondition as hbc
+
+
+
[docs]def dict_to_object(dragonfly_dict, raise_exception=True): + """Re-serialize a dictionary of almost any object within dragonfly. + + This includes any Model, Building, Story, Room2D, WindowParameter, + ShadingParameter, and boundary conditions. + + Args: + dragonfly_dict: A dictionary of any Dragonfly object. Note + that this should be a non-abridged dictionary to be valid. + raise_exception: Boolean to note whether an exception should be raised + if the object is not identified as a part of dragonfly. + Default: True. + + Returns: + A Python object derived from the input dragonfly_dict. + """ + try: # get the type key from the dictionary + obj_type = dragonfly_dict['type'] + except KeyError: + raise ValueError('Dragonfly dictionary lacks required "type" key.') + + if obj_type == 'Model': + return Model.from_dict(dragonfly_dict) + elif obj_type == 'Building': + return Building.from_dict(dragonfly_dict) + elif obj_type == 'Story': + return Story.from_dict(dragonfly_dict) + elif obj_type == 'Room2D': + return Room2D.from_dict(dragonfly_dict) + elif obj_type == 'ContextShade': + return ContextShade.from_dict(dragonfly_dict) + elif hasattr(dfw, obj_type): + win_class = getattr(dfw, obj_type) + return win_class.from_dict(dragonfly_dict) + elif hasattr(dfs, obj_type): + shd_class = getattr(dfs, obj_type) + return shd_class.from_dict(dragonfly_dict) + elif hasattr(hbc, obj_type): + bc_class = getattr(hbc, obj_type) + return bc_class.from_dict(dragonfly_dict) + elif raise_exception: + raise ValueError('{} is not a recognized dragonfly object'.format(obj_type))
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/extensionutil.html b/docs/_modules/dragonfly/extensionutil.html new file mode 100644 index 00000000..d5e8e30d --- /dev/null +++ b/docs/_modules/dragonfly/extensionutil.html @@ -0,0 +1,1108 @@ + + + + + + + dragonfly.extensionutil — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.extensionutil

+# coding: utf-8
+"""A series of utility functions that are useful across several dragonfly extensions."""
+from __future__ import division
+
+
+
[docs]def model_extension_dicts(data, extension_key, building_ext_dicts, story_ext_dicts, + room2d_ext_dicts, context_shade_ext_dicts): + """Get all Model property dictionaries of an extension organized by geometry type. + + Note that the order in which dictionaries appear in the output lists is the + same order as the geometry objects appear when requested from the model. + For example, the shade_ext_dicts align with the model.shades. + + Args: + data: A dictionary representation of an entire honeybee-core Model. + extension_key: Text for the key of the extension (eg. "energy", "radiance"). + + Returns: + A tuple with four elements + + - building_ext_dicts: A list of Building extension property dictionaries that + align with the serialized model.buildings. + + - story_ext_dicts: A list of Story extension property dictionaries that + align with the serialized model.stories. + + - room2d_ext_dicts: A list of Room2D extension property dictionaries that + align with the serialized model.rooms. + + - context_shade_ext_dicts: A list of ContextShade extension property + dictionaries that align with the serialized model.context_shades. + """ + assert data['type'] == 'Model', \ + 'Expected Model dictionary. Got {}.'.format(data['type']) + + # loop through the model dictionary using the same logic that the + # model does when you request buildings, stories, room_2ds, and context_shades. + if 'buildings' in data: + building_extension_dicts(data['buildings'], extension_key, building_ext_dicts, + story_ext_dicts, room2d_ext_dicts) + if 'context_shades' in data: + context_shade_extension_dicts(data['context_shades'], extension_key, + context_shade_ext_dicts) + + return building_ext_dicts, story_ext_dicts, room2d_ext_dicts, context_shade_ext_dicts
+ + +
[docs]def building_extension_dicts(building_list, extension_key, building_ext_dicts, + story_ext_dicts, room2d_ext_dicts): + """Get all Building property dictionaries of an extension organized by geometry type. + + Args: + building_list: A list of Building dictionaries. + extension_key: Text for the key of the extension (eg. "energy", "radiance"). + + Returns: + A tuple with three elements + + - building_ext_dicts: A list with the Building extension property dictionaries. + + - story_ext_dicts: A list with Story extension property dictionaries. + + - room2d_ext_dicts: A list with Room2D extension property dictionaries. + """ + for bldg_dict in building_list: + try: + building_ext_dicts.append(bldg_dict['properties'][extension_key]) + except KeyError: + building_ext_dicts.append(None) + if 'unique_stories' in bldg_dict and bldg_dict['unique_stories'] is not None: + story_extension_dicts(bldg_dict['unique_stories'], extension_key, + story_ext_dicts, room2d_ext_dicts) + return building_ext_dicts, story_ext_dicts, room2d_ext_dicts
+ + +
[docs]def story_extension_dicts(story_list, extension_key, story_ext_dicts, + room2d_ext_dicts): + """Get all Building property dictionaries of an extension organized by geometry type. + + Args: + building_list: A list of Building dictionaries. + extension_key: Text for the key of the extension (eg. "energy", "radiance"). + + Returns: + A tuple with two elements + + - story_ext_dicts: A list with Story extension property dictionaries. + + - room2d_ext_dicts: A list with Room2D extension property dictionaries. + """ + for story_dict in story_list: + try: + story_ext_dicts.append(story_dict['properties'][extension_key]) + except KeyError: + story_ext_dicts.append(None) + room2d_extension_dicts(story_dict['room_2ds'], extension_key, + room2d_ext_dicts) + return story_ext_dicts, room2d_ext_dicts
+ + +
[docs]def room2d_extension_dicts(room2d_list, extension_key, room2d_ext_dicts): + """Get all Room2D property dictionaries of an extension. + + Args: + room2d_list: A list of Room2D dictionaries. + extension_key: Text for the key of the extension (eg. "energy", "radiance"). + + Returns: + room2d_ext_dict -- A list with Room2D extension property dictionaries. + """ + for room_dict in room2d_list: + try: + room2d_ext_dicts.append(room_dict['properties'][extension_key]) + except KeyError: + room2d_ext_dicts.append(None) + return room2d_ext_dicts
+ + +
[docs]def context_shade_extension_dicts(context_shade_list, extension_key, + context_shade_ext_dicts): + """Get all ContextShade property dictionaries of an extension. + + Args: + context_shade_list: A list of ContextShade dictionaries. + extension_key: Text for the key of the extension (eg. "energy", "radiance"). + + Returns: + context_shade_ext_dicts -- A list with ContextShade extension property + dictionaries. + """ + for shd_dict in context_shade_list: + try: + context_shade_ext_dicts.append(shd_dict['properties'][extension_key]) + except KeyError: + context_shade_ext_dicts.append(None) + return context_shade_ext_dicts
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/model.html b/docs/_modules/dragonfly/model.html new file mode 100644 index 00000000..62cdd77a --- /dev/null +++ b/docs/_modules/dragonfly/model.html @@ -0,0 +1,2935 @@ + + + + + + + dragonfly.model — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.model

+# coding: utf-8
+"""Dragonfly Model."""
+from __future__ import division
+
+import os
+import io
+import re
+import json
+import datetime
+try:  # check if we are in IronPython
+    import cPickle as pickle
+except ImportError:  # wea re in cPython
+    import pickle
+
+from ladybug_geometry.geometry2d.pointvector import Point2D
+from ladybug_geometry.geometry3d.face import Face3D
+from ladybug_geometry.geometry3d.pointvector import Vector3D, Point3D
+from ladybug_geometry.geometry3d.plane import Plane
+from ladybug_geometry.geometry3d.polyface import Polyface3D
+from ladybug.futil import preparedir
+from ladybug.location import Location
+
+from honeybee.typing import float_positive, invalid_dict_error, clean_and_number_string
+from honeybee.checkdup import check_duplicate_identifiers
+from honeybee.units import conversion_factor_to_meters, parse_distance_string, \
+    UNITS, UNITS_TOLERANCES
+from honeybee.config import folders
+from honeybee.facetype import Floor, RoofCeiling
+from honeybee.boundarycondition import Outdoors, Surface, Ground, boundary_conditions
+from honeybee.shade import Shade as HBShade
+from honeybee.room import Room as HBRoom
+from honeybee.model import Model as HBModel
+
+from ._base import _BaseGeometry
+from .properties import ModelProperties
+from .building import Building
+from .context import ContextShade
+from .windowparameter import SimpleWindowRatio
+from .projection import meters_to_long_lat_factors, polygon_to_lon_lat, \
+    origin_long_lat_from_location, lon_lat_to_polygon
+from dragonfly.config import folders as df_folders
+import dragonfly.writer.model as writer
+
+
+
[docs]class Model(_BaseGeometry): + """A collection of Buildings and ContextShades for an entire model. + + Args: + identifier: Text string for a unique Model ID. Must be < 100 characters + and not contain any spaces or special characters. + buildings: A list of Building objects in the model. + context_shades: A list of ContextShade objects in the model. + units: Text for the units system in which the model geometry + exists. Default: 'Meters'. Choose from the following: + + * Meters + * Millimeters + * Feet + * Inches + * Centimeters + + tolerance: The maximum difference between x, y, and z values at which + vertices are considered equivalent. Zero indicates that no tolerance + checks should be performed and certain capabilities like to_honeybee + will not be available. None indicates that the tolerance will be + set based on the units above, with the tolerance consistently being + between 1 cm and 1 mm (roughly the tolerance implicit in the OpenStudio + SDK). (Default: None). + angle_tolerance: The max angle difference in degrees that vertices are allowed + to differ from one another in order to consider them colinear. Zero indicates + that no angle tolerance checks should be performed. (Default: 1.0). + + Properties: + * identifier + * display_name + * full_id + * units + * tolerance + * angle_tolerance + * buildings + * context_shades + * stories + * room_2ds + * room_3ds + * average_story_count + * average_story_count_above_ground + * average_height + * average_height_above_ground + * footprint_area + * floor_area + * exterior_wall_area + * exterior_aperture_area + * volume + * min + * max + * user_data + """ + __slots__ = ('_buildings', '_context_shades', + '_units', '_tolerance', '_angle_tolerance') + + def __init__(self, identifier, buildings=None, context_shades=None, + units='Meters', tolerance=None, angle_tolerance=1.0): + """A collection of Buildings and ContextShades for an entire model.""" + _BaseGeometry.__init__(self, identifier) # process the identifier + self.units = units + self.tolerance = tolerance + self.angle_tolerance = angle_tolerance + + self._buildings = [] + self._context_shades = [] + if buildings is not None: + for bldg in buildings: + assert isinstance(bldg, Building), \ + 'Expected Building. Got {}.'.format(type(bldg)) + self._buildings.append(bldg) + if context_shades is not None: + for shade in context_shades: + self.add_context_shade(shade) + + self._properties = ModelProperties(self) + +
[docs] @classmethod + def from_geojson(cls, geojson_file_path, location=None, point=Point2D(0, 0), + all_polygons_to_buildings=False, existing_to_context=False, + units='Meters', tolerance=None, angle_tolerance=1.0): + """Make a Model from a geojson file. + + Args: + geojson_file_path: Text for the full path to the geojson file to load as + Model. + location: An optional ladybug location object with longitude and + latitude data defining the origin of the geojson file. If nothing + is passed, the origin is autocalculated as the bottom-left corner + of the bounding box of all building footprints in the geojson file + (Default: None). + point: A ladybug_geometry Point2D for where the location object exists + within the space of a scene. The coordinates of this point are + expected to be in the expected units of this Model (Default: (0, 0)). + all_polygons_to_buildings: Boolean to indicate if all geometries in + the geojson file should be considered buildings. If False, this + method will only generate footprints from geometries that are + defined as a "Building" in the type field of its corresponding + properties. (Default: False). + existing_to_context: Boolean to indicate whether polygons possessing + a building_status of "Existing" under their properties should be + imported as ContextShade instead of Building objects. (Default: False). + units: Text for the units system in which the model geometry + exists. Default: 'Meters'. Choose from the following: + + * Meters + * Millimeters + * Feet + * Inches + * Centimeters + + Note that this method assumes the point coordinates are in the + same units. + tolerance: The maximum difference between x, y, and z values at which + vertices are considered equivalent. Zero indicates that no tolerance + checks should be performed and certain capabilities like to_honeybee + will not be available. None indicates that the tolerance will be + set based on the units above, with the tolerance consistently being + between 1 cm and 1 mm (roughly the tolerance implicit in the OpenStudio + SDK). (Default: None). + angle_tolerance: The max angle difference in degrees that vertices + are allowed to differ from one another in order to consider them + colinear. Zero indicates that no angle tolerance checks should + be performed. (Default: 1.0). + + Returns: + A tuple with the two items below. + + * model -- A dragonfly Model derived from the geoJSON. + + * location -- A ladybug Location object, which contains latitude and + longitude information and can be used to re-serialize the model + back to geoJSON. + """ + # parse the geoJSON into a dictionary + with open(geojson_file_path, 'r') as fp: + data = json.load(fp) + + # Get the list of building data + if all_polygons_to_buildings: + p_types = ('Polygon', 'MultiPolygon') + bldgs_data = \ + [bldg_data for bldg_data in data['features'] + if 'geometry' in bldg_data and bldg_data['geometry']['type'] in p_types] + else: + bldgs_data = [] + for bldg_data in data['features']: + if 'type' in bldg_data['properties']: + if bldg_data['properties']['type'] == 'Building': + bldgs_data.append(bldg_data) + + # Check if buildings exist + assert len(bldgs_data) > 0, 'No building footprints were found in {}.\n' \ + 'Try setting "all_polygons_to_buildings" to True.'.format(geojson_file_path) + + # if model units is not Meters, convert non-meter user inputs to meters + scale_to_meters = conversion_factor_to_meters(units) + if units != 'Meters': + point = point.scale(scale_to_meters) + + # Get long and lat in the geojson that correspond to the model origin (point). + # If location is None, derive coordinates from the geojson geometry. + if location is None: + point_lon_lat = None + if 'project' in data: + proj_data = data['project'] + if 'latitude' in proj_data and 'longitude' in proj_data: + point_lon_lat = (proj_data['latitude'], proj_data['longitude']) + if point_lon_lat is None: + point_lon_lat = cls._bottom_left_coordinate_from_geojson(bldgs_data) + location = Location(longitude=point_lon_lat[0], latitude=point_lon_lat[1]) + + # The model point may not be at (0, 0), so shift the longitude and latitude to + # get the equivalent point in longitude and latitude for (0, 0) in the model. + origin_lon_lat = origin_long_lat_from_location(location, point) + _convert_facs = meters_to_long_lat_factors(origin_lon_lat) + convert_facs = 1 / _convert_facs[0], 1 / _convert_facs[1] + + # Extract buildings + bldgs, contexts = cls._objects_from_geojson( + bldgs_data, existing_to_context, scale_to_meters, origin_lon_lat, + convert_facs) + + # Make model, in meters and then convert to user-defined units + m_id, m_name = 'Model_1', None + if 'project' in data: + m_id = data['project']['id'] if 'id' in data['project'] else m_id + m_name = data['project']['name'] if 'name' in data['project'] else m_name + model = cls(m_id, buildings=bldgs, context_shades=contexts, units='Meters', + tolerance=tolerance, angle_tolerance=angle_tolerance) + if m_name: + model.display_name = m_name + if units != 'Meters': + model.convert_to_units(units) + + return model, location
+ +
[docs] @classmethod + def from_dict(cls, data): + """Initialize a Model from a dictionary. + + Args: + data: A dictionary representation of a Model object. + """ + # check the type of dictionary + assert data['type'] == 'Model', 'Expected Model dictionary. ' \ + 'Got {}.'.format(data['type']) + + # import the units and tolerance values + units = 'Meters' if 'units' not in data or data['units'] is None \ + else data['units'] + tol = UNITS_TOLERANCES[units] if 'tolerance' not in data or \ + data['tolerance'] is None else data['tolerance'] + angle_tol = 1.0 if 'angle_tolerance' not in data or \ + data['angle_tolerance'] is None else data['angle_tolerance'] + + # import all of the geometry + buildings = None # import buildings + if 'buildings' in data and data['buildings'] is not None: + buildings = [] + for bldg in data['buildings']: + try: + buildings.append( + Building.from_dict(bldg, tol, angle_tol, sort_stories=False)) + except Exception as e: + invalid_dict_error(bldg, e) + context_shades = None # import context shades + if 'context_shades' in data and data['context_shades'] is not None: + context_shades = [] + for s in data['context_shades']: + try: + context_shades.append(ContextShade.from_dict(s)) + except Exception as e: + invalid_dict_error(s, e) + + # build the model object + model = Model(data['identifier'], buildings, context_shades, + units, tol, angle_tol) + if 'display_name' in data and data['display_name'] is not None: + model.display_name = data['display_name'] + if 'user_data' in data and data['user_data'] is not None: + model.user_data = data['user_data'] + + # assign extension properties to the model + model.properties.apply_properties_from_dict(data) + + # sort stories now that properties were ordered correctly during assignment + for building in model.buildings: + building.sort_stories() + return model
+ +
[docs] @classmethod + def from_honeybee(cls, model, conversion_method='AllRoom2D'): + """Initialize a Dragonfly Model from a Honeybee Model. + + Args: + model: A Honeybee Model to be converted to a Dragonfly Model. + conversion_method: Text to indicate how the Honeybee Rooms should be + converted to Dragonfly. Note that the AllRoom2D option may result + in some loss or simplification of the 3D Honeybee geometry but + ensures that all of Dragonfly's features for editing the rooms can + be used. The ExtrudedOnly method will convert only the 3D Rooms + that would have no loss or simplification of geometry when converted + to Room2D. AllRoom3D keeps all detailed 3D geometry on the + Building.room_3ds property, enabling you to convert the 3D Rooms + to Room2D using the Building.convert_room_3ds_to_2d() method as you + see fit. (Default: AllRoom2D). Choose from the following options. + + * AllRoom2D - All Honeybee Rooms converted to Dragonfly Room2D + * ExtrudedOnly - Only pure extrusions converted to Dragonfly Room2D + * AllRoom3D - All Honeybee Rooms left as-is on Building.room_3ds + + """ + # translate the rooms to a dragonfly building + bldgs = None + if len(model.rooms) != 0: + bldgs = [Building.from_honeybee(model, conversion_method)] + # translate the orphaned shades to context shades + shades = [] + for shd_grp in model.grouped_shades: + base_obj = shd_grp[0] + shd_geo = [s.geometry for s in shd_grp] + con_shade = ContextShade(base_obj.identifier, shd_geo, base_obj.is_detached) + con_shade.display_name = base_obj.display_name + con_shade._user_data = None if base_obj.user_data is None \ + else base_obj.user_data.copy() + con_shade.properties.from_honeybee(base_obj.properties) + shades.append(con_shade) + new_model = cls(model.identifier, bldgs, shades, model.units, + model.tolerance, model.angle_tolerance) + new_model._display_name = model._display_name + return new_model
+ +
[docs] @classmethod + def from_file(cls, df_file): + """Initialize a Model from a DFJSON or DFpkl file, auto-sensing the type. + + This will also sense if the input is a Honeybee Model and, if so, + the loaded Dragonfly model will be derived from the Honeybee one. + + Args: + df_file: Path to either a DFJSON or DFpkl file. This can also be a + HBJSON or a HBpkl from which a Dragonfly model should be derived. + """ + # sense the file type from the first character to avoid maxing memory with JSON + # this is needed since queenbee overwrites all file extensions + with io.open(df_file, encoding='utf-8') as inf: + first_char = inf.read(1) + second_char = inf.read(1) + is_json = True if first_char == '{' or second_char == '{' else False + # load the file using either DFJSON pathway or DFpkl + if is_json: + return cls.from_dfjson(df_file) + return cls.from_dfpkl(df_file)
+ +
[docs] @classmethod + def from_dfjson(cls, dfjson_file): + """Initialize a Model from a DFJSON file. + + Args: + dfjson_file: Path to DFJSON file. This can also be a HBJSON from which + a Dragonfly model should be derived. + """ + assert os.path.isfile(dfjson_file), 'Failed to find %s' % dfjson_file + with io.open(dfjson_file, encoding='utf-8') as inf: + inf.read(1) + second_char = inf.read(1) + with io.open(dfjson_file, encoding='utf-8') as inf: + if second_char == '{': + inf.read(1) + data = json.load(inf) + if 'buildings' in data or 'context_shades' in data: + return cls.from_dict(data) + else: # assume that it's a Honeybee Model to translate + hb_model = HBModel.from_dict(data) + return cls.from_honeybee(hb_model)
+ +
[docs] @classmethod + def from_dfpkl(cls, dfpkl_file): + """Initialize a Model from a DFpkl file. + + Args: + dfpkl_file: Path to DFpkl file. This can also be a HBpkl from which + a Dragonfly model should be derived. + """ + assert os.path.isfile(dfpkl_file), 'Failed to find %s' % dfpkl_file + with open(dfpkl_file, 'rb') as inf: + data = pickle.load(inf) + if 'buildings' in data or 'context_shades' in data: + return cls.from_dict(data) + else: # assume that it's a Honeybee Model to translate + hb_model = HBModel.from_dict(data) + return cls.from_honeybee(hb_model)
+ + @property + def units(self): + """Get or set Text for the units system in which the model geometry exists.""" + return self._units + + @units.setter + def units(self, value): + assert value in UNITS, '{} is not supported as a units system. ' \ + 'Choose from the following: {}'.format(value, self.units) + self._units = value + + @property + def tolerance(self): + """Get or set a number for the max meaningful difference between x, y, z values. + + This value should be in the Model's units. Zero indicates cases where + no tolerance checks should be performed. + """ + return self._tolerance + + @tolerance.setter + def tolerance(self, value): + self._tolerance = float_positive(value, 'model tolerance') if value is not None \ + else UNITS_TOLERANCES[self.units] + + @property + def angle_tolerance(self): + """Get or set a number for the max meaningful angle difference in degrees. + + Face3D normal vectors differing by this amount are not considered parallel + and Face3D segments that differ from 180 by this amount are not considered + colinear. Zero indicates cases where no angle_tolerance checks should be + performed. + """ + return self._angle_tolerance + + @angle_tolerance.setter + def angle_tolerance(self, value): + self._angle_tolerance = float_positive(value, 'model angle_tolerance') + + @property + def buildings(self): + """Get a tuple of all Building objects in the model.""" + return tuple(self._buildings) + + @property + def context_shades(self): + """Get a tuple of all ContextShade objects in the model.""" + return tuple(self._context_shades) + + @property + def stories(self): + """Get a tuple of all unique Story objects in the model.""" + return tuple(story for building in self._buildings + for story in building._unique_stories) + + @property + def room_2ds(self): + """Get a tuple of all unique Room2D objects in the model.""" + return tuple(room2d for building in self._buildings + for story in building._unique_stories + for room2d in story._room_2ds) + + @property + def room_3ds(self): + """Get a tuple of all 3D Honeybee Room objects in the model.""" + return tuple(room3d for building in self._buildings + for room3d in building._room_3ds) + + @property + def average_story_count(self): + """Get the average number of stories for the buildings in the model. + + Note that this will be a float and not an integer in most cases. + """ + return sum([bldg.story_count for bldg in self._buildings]) / len(self._buildings) + + @property + def average_story_count_above_ground(self): + """Get the average number of above-ground stories for the buildings in the model. + + Note that this will be a float and not an integer in most cases. + """ + return sum([bldg.story_count_above_ground for bldg in self._buildings]) / \ + len(self._buildings) + + @property + def average_height(self): + """Get the average height of the Buildings as an absolute Z-coordinate.""" + return sum([bldg.height for bldg in self._buildings]) / len(self._buildings) + + @property + def average_height_above_ground(self): + """Get the average building height relative to the first floor above ground.""" + return sum([bldg.height_above_ground for bldg in self._buildings]) / \ + len(self._buildings) + + @property + def footprint_area(self): + """Get a number for the total footprint area of all Buildings in the Model.""" + return sum([bldg.footprint_area for bldg in self._buildings]) + + @property + def floor_area(self): + """Get a number for the total floor area of all Buildings in the Model.""" + return sum([bldg.floor_area for bldg in self._buildings]) + + @property + def exterior_wall_area(self): + """Get a number for the total exterior wall area for all Buildings in the Model. + """ + return sum([bldg.exterior_wall_area for bldg in self._buildings]) + + @property + def exterior_aperture_area(self): + """Get a number for the total exterior aperture area for all Buildings. + """ + return sum([bldg.exterior_aperture_area for bldg in self._buildings]) + + @property + def volume(self): + """Get a number for the volume of all the Buildings in the Model. + """ + return sum([bldg.volume for bldg in self._buildings]) + + @property + def min(self): + """Get a Point2D for the min bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this Model is in proximity + to other objects. + """ + return self._calculate_min(self._buildings + self._context_shades) + + @property + def max(self): + """Get a Point2D for the max bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this Model is in proximity + to other objects. + """ + return self._calculate_max(self._buildings + self._context_shades) + +
[docs] def add_model(self, other_model): + """Add another Dragonfly Model object to this one. + + In the case that Building or Story identifiers in the other_model match + one the current model, these objects will be merged together. Room2Ds + that have matching identifiers within a merged Story will not be added + in order to avoid ID conflicts. Context Shades will also not be added if their + identifier matches one that is already in the Model. + """ + # check that the object to merge is a Model and its units are correct + assert isinstance(other_model, Model), \ + 'Expected Dragonfly Model. Got {}.'.format(type(other_model)) + if self.units != other_model.units: + other_model.convert_to_units(self.units) + # add the Buildings while checking to see if they should be merged + bldg_to_add = list(self._buildings) + for o_bldg in other_model._buildings: + for e_bldg in bldg_to_add: + if o_bldg.identifier == e_bldg.identifier: + e_bldg.add_stories(o_bldg.unique_stories) + e_bldg.add_room_3ds(o_bldg.room_3ds) + break + else: + bldg_to_add.append(o_bldg) + self._buildings = bldg_to_add + # add the ContextShades while checking for duplicate IDs + if len(other_model._context_shades) != 0: + new_context = self._context_shades + exist_set = {shd.identifier for shd in self._context_shades} + for o_shd in other_model._context_shades: + if o_shd.identifier not in exist_set: + new_context.append(o_shd) + exist_set.add(o_shd.identifier) + self._context_shades = new_context
+ +
[docs] def add_building(self, obj): + """Add a Building object to the model. + + In the case that the Building or Story identifiers of the input obj match + one the current model, these objects will be merged together. Room2Ds + that are identical within a merged Story will not be merged in order + to avoid ID conflicts. + """ + assert isinstance(obj, Building), 'Expected Building. Got {}.'.format(type(obj)) + for e_bldg in self._buildings: + if obj.identifier == e_bldg.identifier: + e_bldg.add_stories(obj.unique_stories) + e_bldg.add_room_3ds(obj.room_3ds) + break + else: + self._buildings.append(obj)
+ +
[docs] def add_context_shade(self, obj): + """Add a ContextShade object to the model.""" + assert isinstance(obj, ContextShade), \ + 'Expected ContextShade. Got {}.'.format(type(obj)) + self._context_shades.append(obj)
+ +
[docs] def buildings_by_identifier(self, identifiers): + """Get a list of Building objects in the model given Building identifiers.""" + buildings = [] + for identifier in identifiers: + for bldg in self._buildings: + if bldg.identifier == identifier: + buildings.append(bldg) + break + else: + raise ValueError( + 'Building "{}" was not found in the model.'.format(identifier)) + return buildings
+ +
[docs] def stories_by_identifier(self, identifiers): + """Get a list of Story objects in the model given Story identifiers.""" + stories, model_stories = [], self.stories + for identifier in identifiers: + for story in model_stories: + if story.identifier == identifier: + stories.append(story) + break + else: + raise ValueError( + 'Story "{}" was not found in the model.'.format(identifier)) + return stories
+ +
[docs] def room_2ds_by_identifier(self, identifiers): + """Get a list of Room2D objects in the model given Room2D identifiers.""" + room_2ds, model_room_2ds = [], self.room_2ds + for identifier in identifiers: + for room in model_room_2ds: + if room.identifier == identifier: + room_2ds.append(room) + break + else: + raise ValueError( + 'Room2D "{}" was not found in the model.'.format(identifier)) + return room_2ds
+ +
[docs] def room_3ds_by_identifier(self, identifiers): + """Get a list of 3D Honeybee Room objects in the model given Room identifiers.""" + room_3ds, model_room_3ds = [], self.room_3ds + for identifier in identifiers: + for room in model_room_3ds: + if room.identifier == identifier: + room_3ds.append(room) + break + else: + raise ValueError( + 'Room "{}" was not found in the model.'.format(identifier)) + return room_3ds
+ +
[docs] def context_shade_by_identifier(self, identifiers): + """Get a list of ContextShade objects in the model given identifiers. + """ + context_shades = [] + for identifier in identifiers: + for shd in self._context_shades: + if shd.identifier == identifier: + context_shades.append(shd) + break + else: + raise ValueError( + 'ContextShade "{}" was not found in the model.'.format(identifier)) + return context_shades
+ +
[docs] def add_prefix(self, prefix): + """Change the identifier of this object and child objects by inserting a prefix. + + This is particularly useful in workflows where you duplicate and edit + a starting object and then want to combine it with the original object + since all objects within a Model must have unique identifiers to be valid. + + Args: + prefix: Text that will be inserted at the start of this object's + (and child objects') identifier and display_name. It is recommended + that this prefix be short to avoid maxing out the 100 allowable + characters for identifiers. + """ + for bldg in self._buildings: + bldg.add_prefix(prefix) + for shade in self._context_shades: + shade.add_prefix(prefix)
+ +
[docs] def resolve_id_collisions(self): + """Resolve collisions of duplicate identifiers that exist in the Model. + + In the case that Building or Story identifiers are duplicated, these objects + will be merged together. In the case that Room2Ds that have matching + identifiers, an integer will be automatically appended to the Room2D ID + to make it unique. Context Shades and 3D Rooms that collide will similarly + have their IDs tweaked with an integer if they are duplicated. + """ + # loop through the Buildings and Stories and combine duplicated IDs + merged_buildings = [] + for o_bldg in self._buildings: + for e_bldg in merged_buildings: + if o_bldg.identifier == e_bldg.identifier: + e_bldg.add_stories(o_bldg.unique_stories, add_duplicate_ids=True) + e_bldg.add_room_3ds(o_bldg.room_3ds, add_duplicate_ids=True) + break + else: + merged_buildings.append(o_bldg) + self._buildings = merged_buildings + # loop through all Rooms and ensure their identifiers are unique + rm_dict = {} + for room_2d in self.room_2ds + self.room_3ds: + room_2d.identifier = clean_and_number_string( + room_2d.identifier, rm_dict, 'Room identifier') + # loop through all ContextShades ans ensure their identifiers are unique + shd_dict = {} + for shade in self._context_shades: + shade.identifier = clean_and_number_string( + shade.identifier, shd_dict, 'Shade identifier')
+ +
[docs] def reset_ids(self, repair_surface_bcs=True): + """Reset the identifiers of all Model objects to be derived from display_names. + + In the event that duplicate identifiers are found, an integer will be + automatically appended to the new ID to make it unique. This is similar + to the routines that automatically assign unique names to OpenStudio SDK + objects. + + Args: + repair_surface_bcs: A Boolean to note whether all Surface boundary + conditions across the model should be updated with the new + identifiers that were generated from the display names. (Default: True). + """ + # set up dictionaries to hold various pieces of information + room_map, face_map, ap_map, dr_map = {}, {}, {}, {} + bldg_dict, story_dict, rm_dict, shd_dict = {}, {}, {}, {} + face_dict, ap_dict, dr_dict = {}, {}, {} + + # loop through the objects and change their identifiers + for shade in self._context_shades: + shade.identifier = clean_and_number_string( + shade.display_name, shd_dict, 'Shade identifier') + for bldg in self._buildings: + bldg.identifier = clean_and_number_string( + bldg.display_name, bldg_dict, 'Building identifier') + for story in self.stories: + story.identifier = clean_and_number_string( + story.display_name, story_dict, 'Story identifier') + for rm in self.room_2ds + self.room_3ds: + new_id = clean_and_number_string( + rm.display_name, rm_dict, 'Room identifier') + room_map[rm.identifier] = new_id + rm.identifier = new_id + if isinstance(rm, HBRoom): + for face in rm.faces: + new_id = clean_and_number_string( + face.display_name, face_dict, 'Face identifier') + face_map[face.identifier] = new_id + face.identifier = new_id + for ap in face.apertures: + new_id = clean_and_number_string( + ap.display_name, ap_dict, 'Aperture identifier') + ap_map[ap.identifier] = new_id + ap.identifier = new_id + for dr in face.doors: + new_id = clean_and_number_string( + dr.display_name, dr_dict, 'Door identifier') + dr_map[dr.identifier] = new_id + dr.identifier = new_id + + # reset all of the Surface boundary conditions if requested + if repair_surface_bcs: + # reset all of the Surface conditions on the Room2Ds + for room in self.room_2ds: + new_bcs = [] + for bc in room.boundary_conditions: + if isinstance(bc, Surface): + old_objs = bc.boundary_condition_objects + face_id = old_objs[0].split('..')[-1] + new_adj_f = '{}..{}'.format(room_map[old_objs[1]], face_id) + new_objs = (new_adj_f, room_map[old_objs[1]]) + new_bc = Surface(new_objs) + new_bcs.append(new_bc) + else: + new_bcs.append(bc) + room.boundary_conditions = new_bcs + # reset all of the Surface conditions on the 3D Rooms + for room in self.room_3ds: + for face in room.faces: + if isinstance(face.boundary_condition, Surface): + old_objs = face.boundary_condition.boundary_condition_objects + new_objs = (face_map[old_objs[0]], room_map[old_objs[1]]) + new_bc = Surface(new_objs) + face.boundary_condition = new_bc + for ap in face.apertures: + old_objs = ap.boundary_condition.boundary_condition_objects + new_objs = (ap_map[old_objs[0]], face_map[old_objs[1]], + room_map[old_objs[2]]) + new_bc = Surface(new_objs, True) + ap.boundary_condition = new_bc + for dr in face.doors: + old_objs = dr.boundary_condition.boundary_condition_objects + new_objs = (dr_map[old_objs[0]], face_map[old_objs[1]], + room_map[old_objs[2]]) + new_bc = Surface(new_objs, True) + dr.boundary_condition = new_bc
+ +
[docs] def separate_top_bottom_floors(self, separate_mid=False): + """Separate top/bottom Stories with non-unity multipliers into their own Stories. + + The resulting first and last Stories will each have a multiplier of 1 and + duplicated middle Stories will be added as needed. This method also + automatically assigns the first story Room2Ds to have a ground contact + floor and the top story Room2Ds to have an outdoor-exposed roof. + + separate_mid: Boolean to note whether all mid-level Stories with non-unity + multipliers should be separated into two or three Stories. This means + that the top of each unique story will have outdoor-exposed roofs when + no Room2Ds are sensed above a given room. (Default: False). + """ + if not separate_mid: + for bldg in self._buildings: + bldg.separate_top_bottom_floors() + else: + p_tol = parse_distance_string('0.01m', self.units) + for bldg in self._buildings: + bldg.separate_mid_floors(p_tol)
+ +
[docs] def set_outdoor_window_parameters(self, window_parameter): + """Set all outdoor walls of the Buildings to have the same window parameters.""" + for bldg in self._buildings: + bldg.set_outdoor_window_parameters(window_parameter)
+ +
[docs] def set_outdoor_shading_parameters(self, shading_parameter): + """Set all outdoor walls of the Buildings to have the same shading parameters.""" + for bldg in self._buildings: + bldg.set_outdoor_shading_parameters(shading_parameter)
+ +
[docs] def to_rectangular_windows(self): + """Convert all of the windows of the Story to the RectangularWindows format.""" + for bldg in self._buildings: + bldg.to_rectangular_windows()
+ +
[docs] def move(self, moving_vec): + """Move this Model along a vector. + + Args: + moving_vec: A ladybug_geometry Vector3D with the direction and distance + to move the model. + """ + for bldg in self._buildings: + bldg.move(moving_vec) + for shade in self._context_shades: + shade.move(moving_vec) + self.properties.move(moving_vec)
+ +
[docs] def rotate_xy(self, angle, origin): + """Rotate this Model counterclockwise in the world XY plane by a certain angle. + + Args: + angle: An angle in degrees. + origin: A ladybug_geometry Point3D for the origin around which the + object will be rotated. + """ + for bldg in self._buildings: + bldg.rotate_xy(angle, origin) + for shade in self._context_shades: + shade.rotate_xy(angle, origin) + self.properties.rotate_xy(angle, origin)
+ +
[docs] def reflect(self, plane): + """Reflect this Model across a plane with the input normal vector and origin. + + Args: + plane: A ladybug_geometry Plane across which the object will + be reflected. + """ + for bldg in self._buildings: + bldg.reflect(plane) + for shade in self._context_shades: + shade.reflect(plane) + self.properties.reflect(plane)
+ +
[docs] def scale(self, factor, origin=None): + """Scale this Model by a factor from an origin point. + + 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). + """ + for bldg in self._buildings: + bldg.scale(factor, origin) + for shade in self._context_shades: + shade.scale(factor, origin) + self.properties.scale(factor, origin)
+ +
[docs] def convert_to_units(self, units='Meters'): + """Convert all of the geometry in this model to certain units. + + This involves scaling the geometry, scaling the Model tolerance, and + changing the Model's units property. + + Args: + units: Text for the units to which the Model geometry should be + converted. Default: Meters. Choose from the following: + + * Meters + * Millimeters + * Feet + * Inches + * Centimeters + """ + if self.units != units: + scale_fac1 = conversion_factor_to_meters(self.units) + scale_fac2 = conversion_factor_to_meters(units) + scale_fac = scale_fac1 / scale_fac2 + self.scale(scale_fac) + self.tolerance = self.tolerance * scale_fac + self.units = units
+ +
[docs] def check_all(self, raise_exception=True, detailed=False): + """Check all of the aspects of the Model for possible errors. + + Args: + raise_exception: Boolean to note whether a ValueError should be raised + if any Model errors are found. If False, this method will simply + return a text string with all errors that were found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A text string with all errors that were found. This string will be empty + of no errors were found. + """ + # set up defaults to ensure the method runs correctly + detailed = False if raise_exception else detailed + msgs = [] + assert self.tolerance != 0, \ + 'Model must have a non-zero tolerance in order to perform geometry checks.' + tol, a_tol = self.tolerance, self.angle_tolerance + # perform checks for key dragonfly model schema rules + msgs.append(self.check_duplicate_context_shade_identifiers(False, detailed)) + msgs.append(self.check_duplicate_room_2d_identifiers(False, detailed)) + msgs.append(self.check_duplicate_story_identifiers(False, detailed)) + msgs.append(self.check_duplicate_building_identifiers(False, detailed)) + msgs.append(self.check_degenerate_room_2ds(tol, False, detailed)) + msgs.append(self.check_self_intersecting_room_2ds(tol, False, detailed)) + msgs.append(self.check_window_parameters_valid(tol, False, detailed)) + msgs.append(self.check_missing_adjacencies(False, detailed)) + msgs.append(self.check_no_room2d_overlaps(tol, False, detailed)) + msgs.append(self.check_no_roof_overlaps(tol, False, detailed)) + msgs.append(self.check_all_room3d(tol, a_tol, False, detailed)) + # check the extension attributes + ext_msgs = self._properties._check_extension_attr() + if detailed: + ext_msgs = [m for m in ext_msgs if isinstance(m, list)] + msgs.extend(ext_msgs) + # output a final report of errors or raise an exception + full_msgs = [msg for msg in msgs if msg] + if detailed: + return [m for msg in full_msgs for m in msg] + full_msg = '\n'.join(full_msgs) + if raise_exception and len(full_msgs) != 0: + raise ValueError(full_msg) + return full_msg
+ +
[docs] def check_duplicate_building_identifiers(self, raise_exception=True, detailed=False): + """Check that there are no duplicate Building identifiers in the model. + + Args: + raise_exception: Boolean to note whether a ValueError should be raised + if duplicate identifiers are found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + return check_duplicate_identifiers( + self._buildings, raise_exception, 'Building', detailed, '100004', 'Core', + 'Duplicate Building Identifier')
+ +
[docs] def check_duplicate_story_identifiers(self, raise_exception=True, detailed=False): + """Check that there are no duplicate Story identifiers in the model. + + Args: + raise_exception: Boolean to note whether a ValueError should be raised + if duplicate identifiers are found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + return check_duplicate_identifiers( + self.stories, raise_exception, 'Story', detailed, '100003', 'Core', + 'Duplicate Story Identifier')
+ +
[docs] def check_duplicate_room_2d_identifiers(self, raise_exception=True, detailed=False): + """Check that there are no duplicate Room2D identifiers in the model. + + Args: + raise_exception: Boolean to note whether a ValueError should be raised + if duplicate identifiers are found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + return check_duplicate_identifiers( + self.room_2ds, raise_exception, 'Room2D', detailed, '100002', 'Core', + 'Duplicate Room2D Identifier')
+ +
[docs] def check_duplicate_context_shade_identifiers( + self, raise_exception=True, detailed=False): + """Check that there are no duplicate ContextShade identifiers in the model. + + Args: + raise_exception: Boolean to note whether a ValueError should be raised + if duplicate identifiers are found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + return check_duplicate_identifiers( + self._context_shades, raise_exception, 'ContextShade', detailed, + '100001', 'Core', 'Duplicate ContextShade Identifier')
+ +
[docs] def check_degenerate_room_2ds(self, tolerance=None, raise_exception=True, + detailed=False): + """Check that all Room2Ds are not degenerate with zero area. + + Args: + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. If None, the + Model tolerance will be used. (Default: None). + raise_exception: Boolean to note whether a ValueError should be raised + if the window parameters are not valid. + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + tolerance = self.tolerance if tolerance is None else tolerance + detailed = False if raise_exception else detailed + msgs = [] + for room in self.room_2ds: + msg = room.check_degenerate(tolerance, False, detailed) + if detailed: + msgs.extend(msg) + elif msg != '': + msgs.append(msg) + if detailed: + return msgs + full_msg = '\n'.join(msgs) + if raise_exception and len(msgs) != 0: + raise ValueError(full_msg) + return full_msg
+ +
[docs] def check_self_intersecting_room_2ds(self, tolerance=None, raise_exception=True, + detailed=False): + """Check that all Room2Ds do not intersect with themselves (like a bowtie). + + Note that objects that have duplicate vertices will not be considered + self-intersecting and are valid. + + Args: + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. If None, the + Model tolerance will be used. (Default: None). + raise_exception: Boolean to note whether a ValueError should be raised + if the window parameters are not valid. + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + tolerance = self.tolerance if tolerance is None else tolerance + detailed = False if raise_exception else detailed + msgs = [] + for room in self.room_2ds: + msg = room.check_self_intersecting(tolerance, False, detailed) + if detailed: + msgs.extend(msg) + elif msg != '': + msgs.append(msg) + if detailed: + return msgs + full_msg = '\n'.join(msgs) + if raise_exception and len(msgs) != 0: + raise ValueError(full_msg) + return full_msg
+ +
[docs] def check_window_parameters_valid( + self, tolerance=0.01, raise_exception=True, detailed=False): + """Check that all Room2Ds have window parameters produce valid apertures. + + This means that the resulting Apertures are completely bounded by their + parent wall Face and attributes like window to wall ratio are accurate. + + Args: + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. (Default: 0.01, + suitable for objects in meters). + raise_exception: Boolean to note whether a ValueError should be raised + if the window parameters are not valid. + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + detailed = False if raise_exception else detailed + msgs = [] + for room in self.room_2ds: + msg = room.check_window_parameters_valid(tolerance, False, detailed) + if detailed: + msgs.extend(msg) + elif msg != '': + msgs.append(msg) + if detailed: + return msgs + full_msg = '\n'.join(msgs) + if raise_exception and len(msgs) != 0: + raise ValueError(full_msg) + return full_msg
+ +
[docs] def check_missing_adjacencies(self, raise_exception=True, detailed=False): + """Check that all Room2Ds have adjacent objects that exist within each Story. + + Args: + raise_exception: Boolean to note whether a ValueError should be raised + if missing or invalid adjacencies are found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + bldg_ids = [] + for bldg in self._buildings: + for story in bldg._unique_stories: + adj_msg = story.check_missing_adjacencies(False, detailed) + if adj_msg: + if detailed: + bldg_ids.extend(adj_msg) + else: + bldg_ids.append('{}\n {}'.format(story.full_id, adj_msg)) + if detailed: + return bldg_ids + if bldg_ids != []: + msg = 'The following Stories have missing adjacencies in ' \ + 'the Model:\n{}'.format('\n'.join(bldg_ids)) + if raise_exception: + raise ValueError(msg) + return msg + return ''
+ +
[docs] def check_no_room2d_overlaps( + self, tolerance=None, raise_exception=True, detailed=False): + """Check that geometries of Room2Ds do not overlap with one another. + + Overlaps in Room2Ds mean that the Room volumes will collide with one + another during translation to Honeybee. + + Args: + tolerance: The minimum distance that two Room2Ds geometries can overlap + with one another and still be considered valid. If None, the Model + tolerance will be used. (Default: None). + raise_exception: Boolean to note whether a ValueError should be raised + if overlapping geometries are found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + tolerance = self.tolerance if tolerance is None else tolerance + bldg_ids = [] + for bldg in self._buildings: + for story in bldg._unique_stories: + ov_msg = story.check_no_room2d_overlaps(tolerance, False, detailed) + if ov_msg: + if detailed: + bldg_ids.extend(ov_msg) + else: + bldg_ids.append('{}\n {}'.format(bldg.full_id, ov_msg)) + if detailed: + return bldg_ids + if bldg_ids != []: + msg = 'The following Buildings have overlaps in their Room2D geometry' \ + ':\n{}'.format('\n'.join(bldg_ids)) + if raise_exception: + raise ValueError(msg) + return msg + return ''
+ +
[docs] def check_no_roof_overlaps( + self, tolerance=None, raise_exception=True, detailed=False): + """Check that geometries of RoofSpecifications do not overlap with one another. + + Overlaps make the Roof geometry unusable for translation to Honeybee. + + Args: + tolerance: The minimum distance that two Roof geometries can overlap + with one another and still be considered valid. If None, the Model + tolerance will be used. (Default: None). + raise_exception: Boolean to note whether a ValueError should be raised + if overlapping geometries are found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + tolerance = self.tolerance if tolerance is None else tolerance + bldg_ids = [] + for bldg in self._buildings: + for story in bldg._unique_stories: + ov_msg = story.check_no_roof_overlaps(tolerance, False, detailed) + if ov_msg: + if detailed: + bldg_ids.extend(ov_msg) + else: + bldg_ids.append('{}\n {}'.format(bldg.full_id, ov_msg)) + if detailed: + return bldg_ids + if bldg_ids != []: + msg = 'The following Buildings have overlaps in their roof geometry' \ + ':\n{}'.format('\n'.join(bldg_ids)) + if raise_exception: + raise ValueError(msg) + return msg + return ''
+ +
[docs] def check_all_room3d( + self, tolerance=None, angle_tolerance=None, + raise_exception=True, detailed=False): + """Check all attributes of 3D Honeybee Rooms assigned to the Model's Buildings. + + This includes checking for duplicate Room/Face/Aperture/Door/Shade identifiers, + checking planarity/self-intersection/degeneracy, checking that all rooms are, + solid, and checking the adjacencies (among other attributes). + + Args: + tolerance: tolerance: The maximum difference between x, y, and z values + at which face vertices are considered equivalent. If None, the Model + tolerance will be used. (Default: None). + angle_tolerance: The max angle difference in degrees that vertices are + allowed to differ from one another in order to consider them colinear. + If None, the Model angle_tolerance will be used. (Default: None). + raise_exception: Boolean to note whether a ValueError should be raised + if an error is found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + room_3ds = self.room_3ds + if len(room_3ds) != 0: + tol = self.tolerance if tolerance is None else tolerance + a_tol = self.angle_tolerance if angle_tolerance is None else angle_tolerance + dummy_model = HBModel( + 'validation_model', room_3ds, units=self.units, + tolerance=tol, angle_tolerance=a_tol) + return dummy_model.check_all(raise_exception, detailed) + return [] if detailed else ''
+ +
[docs] def to_honeybee(self, object_per_model='Building', shade_distance=None, + use_multiplier=True, add_plenum=False, cap=False, + solve_ceiling_adjacencies=False, tolerance=None, + enforce_adj=True, enforce_solid=True): + """Convert Dragonfly Model to an array of Honeybee Models. + + Args: + object_per_model: Text to describe how the input Buildings should be + divided across the output Models. (Default: 'Building'). Choose from + the following options: + + * District - All buildings will be added to a single Honeybee Model. + Such a Model can take a long time to simulate so this is only + recommended for small numbers of buildings or cases where + exchange of data between Buildings is necessary. + * Building - Each building will be exported into its own Model. + For each Model, the other buildings input to this component will + appear as context shade geometry. + * Story - Each Story of each Building will be exported into its + own Model. For each Honeybee Model, the other input Buildings + will appear as context shade geometry as will all of the other + stories of the same building. + + shade_distance: An optional number to note the distance beyond which other + objects' shade should not be exported into a given Model. This is + helpful for reducing the simulation run time of each Model when other + connected buildings are too far away to have a meaningful impact on + the results. If None, all other buildings will be included as context + shade in each and every Model. Set to 0 to exclude all neighboring + buildings from the resulting models. (Default: None). + use_multiplier: If True, the multipliers on this Model's Stories will be + passed along to the generated Honeybee Room objects, indicating the + simulation will be run once for each unique room and then results + will be multiplied. If False, full geometry objects will be written + for each and every floor in the building that are represented through + multipliers and all resulting multipliers will be 1. (Default: True). + add_plenum: Boolean to indicate whether ceiling/floor plenums should + be auto-generated for the Rooms. (Default: False). + cap: Boolean to note whether building shade representations should be capped + with a top face. Usually, this is not necessary to account for + blocked sun and is only needed when it's important to account for + reflected sun off of roofs. (Default: False). + solve_ceiling_adjacencies: Boolean to note whether adjacencies should be + solved between interior stories when Room2D floor and ceiling + geometries are coplanar. This ensures that Surface boundary + conditions are used instead of Adiabatic ones. Note that this input + has no effect when the object_per_model is Story. (Default: False). + tolerance: The minimum distance in z values of floor_height and + floor_to_ceiling_height at which adjacent Faces will be split. + This is also used in the generation of Windows. This must be a + positive, non-zero number. If None, the Model's own tolerance + will be used. (Default: None). + enforce_adj: Boolean to note whether an exception should be raised if + an adjacency between two Room2Ds is invalid (True) or if the invalid + Surface boundary condition should be replaced with an Outdoor + boundary condition (False). If False, any Walls containing + WindowParameters and an illegal boundary condition will also + be replaced with an Outdoor boundary condition. (Default: True). + enforce_solid: Boolean to note whether rooms should be translated + as solid extrusions whenever translating them with custom + roof geometry produces a non-solid result (True) or the non-solid + room geometry should be allowed to remain in the result (False). + The latter is useful for understanding why a particular roof + geometry has produced a non-solid result. (Default: True). + + Returns: + An array of Honeybee Models that together represent this Dragonfly Model. + """ + # check the tolerance, which is required to convert to honeybee + tolerance = self.tolerance if tolerance is None else tolerance + assert tolerance != 0, \ + 'Model tolerance must be non-zero to use Model.to_honeybee.' + + # create the model objects + if len(self.buildings) == 0: # model containing only context shade + hb_shades, hb_shade_meshes = [], [] + for shd in self.context_shades: + for s in shd.to_honeybee(): + if isinstance(s, HBShade): + hb_shades.append(s) + else: + hb_shade_meshes.append(s) + h_model = HBModel(self.identifier, orphaned_shades=hb_shades, + shade_meshes=hb_shade_meshes) + h_model.display_name = self.display_name + models = [h_model] + elif object_per_model is None or object_per_model.title() == 'Building': + models = Building.buildings_to_honeybee( + self._buildings, self._context_shades, shade_distance, + use_multiplier, add_plenum, cap, tolerance=tolerance, + enforce_adj=enforce_adj, enforce_solid=enforce_solid) + elif object_per_model.title() == 'Story': + models = Building.stories_to_honeybee( + self._buildings, self._context_shades, shade_distance, + use_multiplier, add_plenum, cap, tolerance=tolerance, + enforce_adj=enforce_adj, enforce_solid=enforce_solid) + elif object_per_model.title() == 'District': + models = [Building.district_to_honeybee( + self._buildings, use_multiplier, add_plenum, tolerance=tolerance, + enforce_adj=enforce_adj, enforce_solid=enforce_solid)] + for shd_group in self._context_shades: + for shd in shd_group.to_honeybee(): + for model in models: + if isinstance(shd, HBShade): + model.add_shade(shd) + else: + model.add_shade_mesh(shd) + else: + raise ValueError('Unrecognized object_per_model input: ' + '{}'.format(object_per_model)) + + # solve ceiling adjacencies if requested + if solve_ceiling_adjacencies and \ + object_per_model.title() in ('Building', 'District'): + story_rel_types = {} + for bldg in self.buildings: + for i, story in enumerate(bldg.unique_stories): + rel_types = [] + if i == 0 or bldg.unique_stories[i - 1].multiplier == 1: + rel_types.append(Floor) + if story.multiplier == 1: + rel_types.append(RoofCeiling) + story_rel_types[story.display_name] = tuple(rel_types) + for model in models: + self._solve_ceil_adj(model.rooms, story_rel_types, + tolerance, self.angle_tolerance) + + # change the tolerance and units systems to match the dragonfly model + for model in models: + model.units = self.units + model.tolerance = tolerance + model.angle_tolerance = self.angle_tolerance + + # transfer Model extension attributes to the honeybee models + for h_model in models: + h_model._properties = self.properties.to_honeybee(h_model) + + return models
+ +
[docs] def to_geojson_dict(self, location, point=Point2D(0, 0), tolerance=None): + """Convert Dragonfly Model to a geoJSON-style Python dictionary. + + This dictionary can be written into a JSON, which is then a valid geoJSON + that can be visualized in any geoJSON viewer. Each dragonfly Building + will appear in the geoJSON as a single feature (either as a Polygon or + a MultiPolygon). + + Args: + location: A ladybug Location object possessing longitude and latitude data. + point: A ladybug_geometry Point2D for where the location object exists + within the space of a scene. The coordinates of this point are + expected to be in the units of this Model. (Default: (0, 0)). + tolerance: The minimum distance between points at which they are + not considered touching. If None, the Model's own tolerance + will be used. (Default: None). + + Returns: + A Python dictionary in a geoJSON style with each Building in the Model + as a separate feature. + """ + # set up the base dictionary for the geoJSON + geojson_dict = {'type': 'FeatureCollection', 'features': [], 'mappers': []} + + # ensure that the Model we are working with is in meters + model = self + if self.units != 'Meters': + model = self.duplicate() # duplicate to avoid editing this object + model.convert_to_units('Meters') + point = point.scale(conversion_factor_to_meters(self.units)) + + # assign the site information in the project key + project_dict = { + 'id': self.identifier, + 'name': self.display_name, + 'city': location.city, + 'country': location.country, + 'elevation': location.elevation, + 'latitude': location.latitude, + 'longitude': location.longitude, + 'time_zone': location.time_zone, + 'cad_coordinates': [point.x, point.y] + } + geojson_dict['project'] = project_dict + + # get the conversion factors over to (longitude, latitude) + origin_lon_lat = origin_long_lat_from_location(location, point) + convert_facs = meters_to_long_lat_factors(origin_lon_lat) + tolerance = self.tolerance if tolerance is None else tolerance + + # export each building as a feature in the file + for bldg in model.buildings: + # create the base dictionary + feature_dict = {'geometry': {}, 'properties': {}, 'type': 'Feature'} + + # add the geometry including coordinates + footprint = bldg.footprint(tolerance) + if len(footprint) == 1: + feature_dict['geometry']['type'] = 'Polygon' + feature_dict['geometry']['coordinates'] = \ + self._face3d_to_geojson_coordinates( + footprint[0], origin_lon_lat, convert_facs) + else: + feature_dict['geometry']['type'] = 'MultiPolygon' + all_coords = [] + for floor in footprint: + all_coords.append( + self._face3d_to_geojson_coordinates( + floor, origin_lon_lat, convert_facs)) + feature_dict['geometry']['coordinates'] = all_coords + + # add several of the properties to the geoJSON + feature_dict['properties']['building_type'] = 'Mixed use' + feature_dict['properties']['floor_area'] = bldg.floor_area + feature_dict['properties']['footprint_area'] = bldg.footprint_area + feature_dict['properties']['id'] = bldg.identifier + feature_dict['properties']['name'] = bldg.display_name + feature_dict['properties']['number_of_stories'] = bldg.story_count + feature_dict['properties']['number_of_stories_above_ground'] = \ + bldg.story_count_above_ground + feature_dict['properties']['maximum_roof_height'] = \ + bldg.height_above_ground + feature_dict['properties']['floor_height'] = bldg.height / bldg.story_count + feature_dict['properties']['type'] = 'Building' + + # attempt to determine the year built from the construction set + year_built = datetime.date.today().year + if hasattr(bldg.properties, 'energy'): + cs_name = bldg.properties.energy.construction_set.display_name + if len(cs_name) >= 4 and all(txt.isdigit() for txt in cs_name[:4]): + year_built = int(cs_name[:4]) + feature_dict['properties']['year_built'] = year_built + + # append the feature to the global dictionary + geojson_dict['features'].append(feature_dict) + + return geojson_dict
+ +
[docs] def to_geojson(self, location, point=Point2D(0, 0), folder=None, tolerance=None): + """Convert Dragonfly Model to a geoJSON of buildings footprints. + + This geoJSON will be in a format that is compatible with the URBANopt SDK, + including properties for floor_area, footprint_area, and detailed_model_filename, + which will align with the paths to OpenStudio model (.osm) files output + from honeybee Models translated to OSM. + + Args: + location: A ladybug Location object possessing longitude and latitude data. + point: A ladybug_geometry Point2D for where the location object exists + within the space of a scene. The coordinates of this point are + expected to be in the units of this Model. (Default: (0, 0)). + folder: Text for the full path to where the geojson file will be written. + If None, a sub-folder within the honeybee default simulation + folder will be used. (Default: None). + tolerance: The minimum distance between points at which they are + not considered touching. If None, the Model's own tolerance + will be used. (Default: None). + + Returns: + The path to a geoJSON file that contains polygons for all of the + Buildings within the dragonfly model along with their properties + (floor area, number of stories, etc.). The polygons will also possess + detailed_model_filename keys that align with where OpenStudio models + would be written, assuming the input folder matches that used to + export OpenStudio models. + """ + # set the default simulation folder + if folder is None: + folder = folders.default_simulation_folder + + # get the geojson dictionary + geojson_dict = self.to_geojson_dict(location, point, tolerance) + + # write out the dictionary to a geojson file + project_folder = os.path.join( + folder, re.sub(r'[^.A-Za-z0-9_-]', '_', self.display_name)) + preparedir(project_folder, remove_content=False) + file_path = os.path.join(project_folder, '{}.geojson'.format(self.identifier)) + with open(file_path, 'w') as fp: + json.dump(geojson_dict, fp, indent=4) + return file_path
+ +
[docs] def to_dict(self, included_prop=None): + """Return Model as a dictionary. + + Args: + included_prop: List of properties to filter keys that must be included in + output dictionary. For example ['energy'] will include 'energy' key if + available in properties to_dict. By default all the keys will be + included. To exclude all the keys from extensions use an empty list. + """ + base = {'type': 'Model'} + base['identifier'] = self.identifier + base['display_name'] = self.display_name + base['properties'] = self.properties.to_dict(included_prop) + if self._buildings != []: + base['buildings'] = \ + [bldg.to_dict(True, included_prop) for bldg in self._buildings] + if self._context_shades != []: + base['context_shades'] = \ + [shd.to_dict(True, included_prop) for shd in self._context_shades] + base['units'] = self.units + if self.tolerance != 0: + base['tolerance'] = self.tolerance + if self.angle_tolerance != 0: + base['angle_tolerance'] = self.angle_tolerance + + if self.user_data is not None: + base['user_data'] = self.user_data + if df_folders.dragonfly_schema_version is not None: + base['version'] = df_folders.dragonfly_schema_version_str + return base
+ +
[docs] def to_dfjson(self, name=None, folder=None, indent=None, included_prop=None): + """Write Dragonfly model to DFJSON. + + Args: + name: A text string for the name of the DFJSON file. If None, the model + identifier wil be used. (Default: None). + folder: A text string for the directory where the DFJSON will be written. + If unspecified, the default simulation folder will be used. This + is usually at "C:\\Users\\USERNAME\\simulation" on Windows. + indent: A positive integer to set the indentation used in the resulting + DFJSON file. (Default: None). + included_prop: List of properties to filter keys that must be included in + output dictionary. For example ['energy'] will include 'energy' key if + available in properties to_dict. By default all the keys will be + included. To exclude all the keys from extensions use an empty list. + """ + # create dictionary from the Dragonfly Model + df_dict = self.to_dict(included_prop=included_prop) + # set up a name and folder for the DFJSON + if name is None: + name = self.identifier + file_name = name if name.lower().endswith('.dfjson') or \ + name.lower().endswith('.json') else '{}.dfjson'.format(name) + folder = folder if folder is not None else folders.default_simulation_folder + df_file = os.path.join(folder, file_name) + # write DFJSON + with open(df_file, 'w') as fp: + json.dump(df_dict, fp, indent=indent) + return df_file
+ +
[docs] def to_dfpkl(self, name=None, folder=None, included_prop=None): + """Writes Dragonfly model to compressed pickle file (DFpkl). + + Args: + name: A text string for the name of the pickle file. If None, the model + identifier wil be used. (Default: None). + folder: A text string for the directory where the pickle will be written. + If unspecified, the default simulation folder will be used. This + is usually at "C:\\Users\\USERNAME\\simulation." + included_prop: List of properties to filter keys that must be included in + output dictionary. For example ['energy'] will include 'energy' key if + available in properties to_dict. By default all the keys will be + included. To exclude all the keys from extensions use an empty list. + """ + # create dictionary from the Dragonfly Model + df_dict = self.to_dict(included_prop=included_prop) + # set up a name and folder for the DFpkl + if name is None: + name = self.identifier + file_name = name if name.lower().endswith('.dfpkl') or \ + name.lower().endswith('.pkl') else '{}.dfpkl'.format(name) + folder = folder if folder is not None else folders.default_simulation_folder + df_file = os.path.join(folder, file_name) + # write the Model dictionary into a file + with open(df_file, 'wb') as fp: + pickle.dump(df_dict, fp) + return df_file
+ + @property + def to(self): + """Model writer object. + + Use this method to access Writer class to write the model in other formats. + """ + return writer + +
[docs] @staticmethod + def model_dict_room_2d_subset(model_dict, room_2d_ids): + """Get a dragonfly Model dictionary that has been filtered for a Room2D subset. + + This is useful when you are only interested in visualizing or exporting a + subset of Room2Ds to a file and so it is not desirable to serialize the + entire Dragonfly Model. + + Args: + model_dict: A dictionary of a Dragonfly Model. + room_2d_ids: An optional list of the identifiers for the Room2Ds + to be included in the output dictionary. + + Returns: + A copy of the input Dragonfly Model dictionary, which contains only + the Room2Ds listed in the room_2d_ids. All ContextShade and 3D Honeybee + Rooms are removed but slanted Roof geometries are included if they are + relevant to the Room2Ds. + """ + return Model.model_dict_subset(model_dict, room_2d_ids)
+ +
[docs] @staticmethod + def model_dict_subset( + model_dict, room_2d_ids=None, room_3d_ids=None, shade_ids=None): + """Get a dragonfly Model dictionary that has been filtered for certain objects. + + This is useful when you are only interested in visualizing or exporting a + subset of objects to a file and so it is not desirable to serialize the + entire Dragonfly Model. + + Args: + model_dict: A dictionary of a Dragonfly Model. + room_2d_ids: An optional list of the identifiers for the Room2Ds + to be included in the output dictionary. If None, no Room2D + dictionaries will be in the result. (Default: None). + room_3d_ids: An optional list of the identifiers for the 3D Rooms + to be included in the output dictionary. If None, no 3D Room + dictionaries will be in the result. (Default: None). + shade_ids: An optional list of the identifiers for the ContextShades + to be included in the output dictionary. If None, no ContextShade + dictionaries will be in the result. (Default: None). + + Returns: + A copy of the input Dragonfly Model dictionary, which contains only + the Room2Ds listed in the room_2d_ids, the 3D Rooms listed in the + room_3d_ids, and ContextShades in the shade_ids. Slanted Roof geometries + are included if they are relevant to the Room2Ds. + """ + # build a copy of the model_dict with geometry excluded + ex_keys = ('buildings', 'context_shades') + filtered_model = {key: v for key, v in model_dict.items() if key not in ex_keys} + r3_ids = None + if room_3d_ids is not None and len(room_3d_ids) != 0: + r3_ids = set(room_3d_ids) + # loop through the Buildings and grab the relevant Rooms + if room_2d_ids is not None and len(room_2d_ids) != 0: + room_ids = set(room_2d_ids) + if 'buildings' in model_dict and model_dict['buildings'] is not None: + new_bldgs = [] + for b_dict in model_dict['buildings']: + r_2ds_found = False + if 'unique_stories' in b_dict and \ + b_dict['unique_stories'] is not None: + new_stories = [] + for s_dict in b_dict['unique_stories']: + r_dicts = [r for r in s_dict['room_2ds'] + if r['identifier'] in room_ids] + if len(r_dicts) != 0: + new_story = s_dict.copy() + new_story['room_2ds'] = r_dicts + new_stories.append(new_story) + if len(new_stories) != 0: + new_bldg = b_dict.copy() + new_bldg['unique_stories'] = new_stories + new_bldgs.append(new_bldg) + r_2ds_found = True + if r3_ids is not None: + if 'room_3ds' in b_dict and b_dict['room_3ds'] is not None: + new_room_3ds = [] + for r3_dict in b_dict['room_3ds']: + if r3_dict['identifier'] in r3_ids: + new_room_3ds.append(r3_dict) + if len(new_room_3ds) != 0: + if r_2ds_found: + new_bldg = new_bldgs[-1] + new_bldg['room_3ds'] = new_room_3ds + else: + new_bldg = b_dict.copy() + new_bldg['room_3ds'] = new_room_3ds + new_bldgs.append(new_bldg) + filtered_model['buildings'] = new_bldgs + elif r3_ids is not None: # only 3D Rooms to visualize + if 'buildings' in model_dict and model_dict['buildings'] is not None: + new_bldgs = [] + for b_dict in model_dict['buildings']: + if 'room_3ds' in b_dict and b_dict['room_3ds'] is not None: + new_room_3ds = [] + for r3_dict in b_dict['room_3ds']: + if r3_dict['identifier'] in r3_ids: + new_room_3ds.append(r3_dict) + if len(new_room_3ds) != 0: + new_bldg = b_dict.copy() + new_bldg['room_3ds'] = new_room_3ds + new_bldgs.append(new_bldg) + filtered_model['buildings'] = new_bldgs + # loop through the ContextShades and grab the relevant objects + if shade_ids is not None and len(shade_ids) != 0: + cs_ids = set(shade_ids) + if 'context_shades' in model_dict and \ + model_dict['context_shades'] is not None: + new_shades = [] + for cs_dict in model_dict['context_shades']: + if cs_dict['identifier'] in cs_ids: + new_shades.append(cs_dict) + filtered_model['context_shades'] = new_shades + return filtered_model
+ + @staticmethod + def _solve_ceil_adj(rooms, story_rel_types, tolerance=0.01, angle_tolerance=1): + """Solve Floor/Ceiling adjacencies between a list of rooms.""" + # intersect the Rooms with one another for matching adjacencies + HBRoom.intersect_adjacency(rooms, tolerance, angle_tolerance) + # solve all adjacencies between rooms + relevant_types = (Floor, RoofCeiling) + for i, room_1 in enumerate(rooms): + try: + for room_2 in rooms[i + 1:]: + if not Polyface3D.overlapping_bounding_boxes( + room_1.geometry, room_2.geometry, tolerance): + continue # no overlap in bounding box; adjacency impossible + for face_1 in room_1._faces: + for face_2 in room_2._faces: + if isinstance(face_2.type, relevant_types): + if face_1.geometry.is_centered_adjacent( + face_2.geometry, tolerance): + face_1.set_adjacency(face_2) + break + except IndexError: + pass # we have reached the end of the list of zones + # change any remaining Floor/Roof boundary conditions to be outdoors + relevant_bcs = (Outdoors, Surface, Ground) + for room in rooms: + rel_types = story_rel_types[room.story] + for face in room._faces: + if isinstance(face.type, rel_types): + if not isinstance(face.boundary_condition, relevant_bcs): + face.boundary_condition = boundary_conditions.outdoors + + @staticmethod + def _objects_from_geojson(bldgs_data, existing_to_context, scale_to_meters, + origin_lon_lat, convert_facs): + """Get Dragonfly Building and ContextShade objects from a geoJSON dictionary. + + Args: + bldgs_data: A list of geoJSON object dictionaries, including polygons + to be turned into buildings and context. + existing_to_context: Boolean to indicate whether polygons possessing + a building_status of "Existing" under their properties should be + imported as ContextShade instead of Building objects. + scale_to_meters: Factor for converting the building heights to meters. + origin_lon_lat: An array of two numbers in degrees for origin lat and lon. + convert_facs: A tuple with two values used to translate between + meters and longitude, latitude. + """ + bldgs, contexts = [], [] + for i, bldg_data in enumerate(bldgs_data): + # get footprints + footprint = [] + geojson_coordinates = bldg_data['geometry']['coordinates'] + prop = bldg_data['properties'] + + if bldg_data['geometry']['type'] == 'Polygon': + face3d = Model._geojson_coordinates_to_face3d( + geojson_coordinates, origin_lon_lat, convert_facs) + footprint.append(face3d) + else: # if MultiPolygon, account for multiple polygons + for _geojson_coordinates in geojson_coordinates: + face3d = Model._geojson_coordinates_to_face3d( + _geojson_coordinates, origin_lon_lat, convert_facs) + footprint.append(face3d) + + # determine whether the footprint should be context or a building + if existing_to_context and 'building_status' in prop \ + and prop['building_status'] == 'Existing': + ht = prop['maximum_roof_height'] * scale_to_meters \ + if 'maximum_roof_height' in prop else 3.5 + extru_vec = Vector3D(0, 0, ht) + geo = [Face3D.from_extrusion(seg, extru_vec) for face3d in footprint + for seg in face3d.boundary_segments] + shd_id = 'Context_{}'.format(i) if 'id' not in prop else prop['id'] + contexts.append(ContextShade(shd_id, geo)) + continue + + # Define building heights from file or assign default single-storey building + if 'maximum_roof_height' in prop and 'number_of_stories' in prop: + story_height = (prop['maximum_roof_height'] * scale_to_meters) \ + / prop['number_of_stories'] + story_heights = [story_height] * prop['number_of_stories'] + elif 'number_of_stories' in prop: + story_heights = [3.5] * prop['number_of_stories'] + else: # just import it as one story per building + story_heights = [3.5] + + # make building object + bldg_id = 'Building_{}'.format(i) if 'id' not in prop else prop['id'] + bldg = Building.from_footprint(bldg_id, footprint, story_heights) + if 'name' in prop: + bldg.display_name = prop['name'] + + # assign windows to the buildings + if 'window_to_wall_ratio' in prop: + win_par = SimpleWindowRatio(prop['window_to_wall_ratio']) + bldg.set_outdoor_window_parameters(win_par) + + # add any extension attributes and add the building to the list + bldg.properties.apply_properties_from_geojson_dict(prop) + bldgs.append(bldg) + return bldgs, contexts + + @staticmethod + def _face3d_to_geojson_coordinates(face3d, origin_lon_lat, convert_facs): + """Convert a horizontal Face3D to geoJSON coordinates.""" + coords = [polygon_to_lon_lat( + [(pt.x, pt.y) for pt in face3d.boundary], origin_lon_lat, convert_facs)] + coords[0].append(coords[0][0]) + if face3d.has_holes: + for hole in face3d.holes: + hole_verts = polygon_to_lon_lat( + [(pt.x, pt.y) for pt in hole], origin_lon_lat, convert_facs) + hole_verts.append(hole_verts[0]) + coords.append(hole_verts) + return coords + + @staticmethod + def _geojson_coordinates_to_face3d(geojson_coordinates, origin_lon_lat, + convert_facs): + """Convert geoJSON coordinates to a horizontal Face3D with zero height. + + Args: + geojson_coordinates: The coordinates from the geojson file. For 'Polygon' + geometries, this will be the list from the 'coordinates' key in the + geojson file, for 'MultiPolygon' geometries, this will be each item + in the list from the 'coordinates' key. + origin_lon_lat: An array of two numbers in degrees representing the + longitude and latitude of the scene origin in degrees. + convert_facs: A tuple with two values used to translate between + longitude, latitude and meters. + + Returns: + A Face3D object in model space coordinates converted from the geojson + coordinates. The height of the Face3D vertices will be 0. + """ + holes = None + coords = lon_lat_to_polygon(geojson_coordinates[0], origin_lon_lat, convert_facs) + coords = [Point3D(pt2d[0], pt2d[1], 0) for pt2d in coords][:-1] + + # If there are more then 1 polygons, then the other polygons are holes. + if len(geojson_coordinates) > 1: + holes = [] + for hole_geojson_coordinates in geojson_coordinates[1:]: + hole_coords = lon_lat_to_polygon( + hole_geojson_coordinates, origin_lon_lat, convert_facs) + hole_coords = [Point3D(pt2d[0], pt2d[1], 0) for pt2d in hole_coords][:-1] + holes.append(hole_coords) + + return Face3D(coords, plane=Plane(n=Vector3D(0, 0, 1)), holes=holes) + + @staticmethod + def _bottom_left_coordinate_from_geojson(bldgs_data): + """Calculate the bottom-left bounding box coordinate from geojson coordinates. + + Args: + bldgs_data: a list of dictionaries containing geojson geometries that + represent building footprints. + + Returns: + The bottom-left most corner of the bounding box around the coordinates. + """ + xs, ys = [], [] + for bldg in bldgs_data: + bldg_coords = bldg['geometry']['coordinates'] + + if bldg['geometry']['type'] == 'Polygon': + for bldg_footprint in bldg_coords: + xs.extend([coords[0] for coords in bldg_footprint]) + ys.extend([coords[1] for coords in bldg_footprint]) + else: + for bldg_footprints in bldg_coords: + for bldg_footprint in bldg_footprints: + xs.extend([coords[0] for coords in bldg_footprint]) + ys.extend([coords[1] for coords in bldg_footprint]) + + return min(xs), min(ys) + + def __add__(self, other): + new_model = self.duplicate() + new_model.add_model(other) + return new_model + + def __iadd__(self, other): + self.add_model(other) + return self + + def __copy__(self): + new_model = Model( + self.identifier, + [bldg.duplicate() for bldg in self._buildings], + [shade.duplicate() for shade in self._context_shades], + self.units, self.tolerance, self.angle_tolerance) + new_model._display_name = self.display_name + new_model._user_data = None if self.user_data is None else self.user_data.copy() + new_model._properties._duplicate_extension_attr(self._properties) + return new_model + + def __repr__(self): + return 'Dragonfly Model: %s' % self.display_name
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/projection.html b/docs/_modules/dragonfly/projection.html new file mode 100644 index 00000000..549b3829 --- /dev/null +++ b/docs/_modules/dragonfly/projection.html @@ -0,0 +1,1105 @@ + + + + + + + dragonfly.projection — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.projection

+"""Utilities for converting X,Y coordinates in meters to longitude, latitude."""
+from __future__ import division
+
+import math
+
+
+
[docs]def meters_to_long_lat_factors(origin_lon_lat=(0, 0)): + """Get conversion factors for translating meters to longitude, latitude. + + The resulting factors should obey the WSG84 assumptions for the radius of + the earth at the equator relative to the poles. + + Args: + origin_long_lat: An array of two numbers in degrees. The first value + represents the longitude of the scene origin in degrees (between -180 + and +180). The second value represents latitude of the scene origin + in degrees (between -90 and +90). Default: (0, 0). + + Returns: + A tuple with two values: + + meters_to_lon -- conversion factor for changing meters to degrees longitude. + + meters_to_lat -- conversion factor for changing meters to degrees latitude. + """ + # constants of the WSG84 system + equator_rad = 6378137.0 # radius of the earth at the equator (meters) + pole_rad = 6356752.314 # radius of the earth at the poles (meters) + + # convert everything to radians + lat = math.radians(origin_lon_lat[1]) + + # compute the conversion values + d = math.sqrt( + (equator_rad ** 2 * math.sin(lat) ** 2) + (pole_rad ** 2 * math.cos(lat) ** 2)) + r = (equator_rad * pole_rad) / d # radius of the earth at the latitude + meters_to_lat = (math.pi * r * 2) / 360 # meters in one degree of latitude + meters_to_lon = meters_to_lat * math.cos(lat) # meters in one degree of longitude + + return meters_to_lon, meters_to_lat
+ + +
[docs]def polygon_to_lon_lat(polygon, origin_lon_lat=(0, 0), conversion_factors=None): + """Get an array of (longitude, latitude) from a ladybug_geometry Polygon2D in meters. + + The resulting coordinates should obey the WSG84 assumptions for the radius of + the earth at the equator relative to the poles. + Note that this function uses a simple formula and some distortion is possible + when translating polygons several kilometers long. + + Args: + polygon: An array of (X, Y) values for coordinates in meters. + origin_lon_lat: An array of two numbers in degrees. The first value + represents the longitude of the scene origin in degrees (between -180 + and +180). The second value represents latitude of the scene origin + in degrees (between -90 and +90). Note that the "scene origin" is the + (0, 0) coordinate in the 2D space of the input polygon. Default: (0, 0). + conversion_factors: A tuple with two values used to translate between + meters and longitude, latitude. If None, these values will be automatically + calculated from the origin_lon_lat using the meters_to_long_lat_factors + method. + + Returns: + A nested array with each sub-array having 2 values for the + (longitude, latitude) of each polygon vertex. + """ + # unpack or autocalculate the conversion factors + if not conversion_factors: + meters_to_lon, meters_to_lat = meters_to_long_lat_factors(origin_lon_lat) + else: + meters_to_lon, meters_to_lat = conversion_factors + + # get the longitude, latitude values for the polygon + return [(origin_lon_lat[0] + pt[0] / meters_to_lon, + origin_lon_lat[1] + pt[1] / meters_to_lat) for pt in polygon]
+ + +
[docs]def lon_lat_to_polygon(polygon_lon_lat_coords, origin_lon_lat=(0, 0), + conversion_factors=None): + """Convert an array of (longitude, latitude) coordinates to (X, Y) coordinates in meters. + + The resulting coordinates will obey the WSG84 assumptions for the radius of + the earth at the equator relative to the poles. + Note that this function uses a simple formula and some distortion is possible + when translating polygons several kilometers long. + + Args: + polygon_lon_lat_coords: A nested array with each sub-array having 2 values for + the (longitude, latitude) of a polygon boundary. + origin_lon_lat: An array of two numbers in degrees. The first value + represents the longitude of the scene origin in degrees (between -180 + and +180). The second value represents latitude of the scene origin + in degrees (between -90 and +90). Note that the "scene origin" is the + (0, 0) coordinate in the 2D space of the input polygon. Default: (0, 0). + conversion_factors: A tuple with two values used to translate between + longitude, latitude and meters. If None, these values will be automatically + calculated from the origin_lon_lat using the inverse of the + factors computed from the meters_to_long_lat_factors method. + Returns: + An array of (X, Y) values for the boundary coordinates in meters. + """ + + # Unpack or autocalculate the conversion factors + if not conversion_factors: + meters_to_lon, meters_to_lat = meters_to_long_lat_factors(origin_lon_lat) + lon_to_meters, lat_to_meters = 1.0 / meters_to_lon, 1.0 / meters_to_lat + else: + lon_to_meters, lat_to_meters = conversion_factors + + # Get the (X, Y) values for the boundary in meters + return [((pt[0] - origin_lon_lat[0]) / lon_to_meters, + (pt[1] - origin_lon_lat[1]) / lat_to_meters) + for pt in polygon_lon_lat_coords]
+ + +
[docs]def origin_long_lat_from_location(location, point): + """Get the (longitude, latitude) of the scene origin from a location and a point. + + Args: + location: A ladybug Location object possessing longitude and latitude data. + point: A ladybug_geometry Point2D for where the location object exists + within the space of a scene. The coordinates of this point are expected + to be in meters. + + Returns: + An array of two numbers in degrees. The first value represents the longitude + of the scene origin in degrees (between -180 and +180). The second value + represents latitude of the scene origin in degrees (between -90 and +90). + """ + meters_to_lon, meters_to_lat = meters_to_long_lat_factors( + (location.longitude, location.latitude)) + return location.longitude - point.x / meters_to_lon, \ + location.latitude - point.y / meters_to_lat
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/properties.html b/docs/_modules/dragonfly/properties.html new file mode 100644 index 00000000..ad4e5d79 --- /dev/null +++ b/docs/_modules/dragonfly/properties.html @@ -0,0 +1,1604 @@ + + + + + + + dragonfly.properties — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.properties

+# coding: utf-8
+"""Extension properties for Building, Story, Room2D.
+
+These objects hold all attributes assigned by extensions like dragonfly-radiance
+and dragonfly-energy.  Note that these Property objects are not intended to exist
+on their own but should have a host object.
+"""
+from __future__ import division
+
+import honeybee.properties as hb_properties
+
+
+class _Properties(object):
+    """Base class for all Properties classes.
+
+    Args:
+        host: A dragonfly-core geometry object that hosts these properties
+            (ie. Building, Story, Room2D).
+
+    Properties:
+        * host
+
+    """
+    _exclude = ('host', 'move', 'rotate_xy', 'reflect', 'scale', 'add_prefix',
+                'reset_to_default', 'to_dict', 'to_honeybee', 'ToString')
+
+    def __init__(self, host):
+        """Initialize properties."""
+        self._host = host
+
+    @property
+    def host(self):
+        """Get the object hosting these properties."""
+        return self._host
+
+    @property
+    def _extension_attributes(self):
+        return (atr for atr in dir(self) if not atr.startswith('_')
+                and atr not in self._exclude)
+
+    def move(self, moving_vec):
+        """Apply a move transform to extension attributes.
+
+        This is useful in cases where extension attributes possess geometric data
+        that should be moved alongside the host object.
+
+        Args:
+            moving_vec: A ladybug_geometry Vector3D with the direction and distance
+                to move the face.
+        """
+        for atr in self._extension_attributes:
+            var = getattr(self, atr)
+            if not hasattr(var, 'move'):
+                continue
+            try:
+                var.move(moving_vec)
+            except Exception as e:
+                import traceback
+                traceback.print_exc()
+                raise Exception('Failed to move {}: {}'.format(var, e))
+
+    def rotate_xy(self, angle, origin):
+        """Apply a rotatation in the XY plane to extension attributes.
+
+        This is useful in cases where extension attributes possess geometric data
+        that should be rotated alongside the host object.
+
+        Args:
+            angle: An angle in degrees.
+            origin: A ladybug_geometry Point3D for the origin around which the
+                object will be rotated.
+        """
+        for atr in self._extension_attributes:
+            var = getattr(self, atr)
+            if not hasattr(var, 'rotate_xy'):
+                continue
+            try:
+                var.rotate_xy(angle, origin)
+            except Exception as e:
+                import traceback
+                traceback.print_exc()
+                raise Exception('Failed to rotate {}: {}'.format(var, e))
+
+    def reflect(self, plane):
+        """Apply a reflection transform to extension attributes.
+
+        This is useful in cases where extension attributes possess geometric data
+        that should be reflected alongside the host object.
+
+        Args:
+            plane: A ladybug_geometry Plane across which the object will
+                be reflected.
+        """
+        for atr in self._extension_attributes:
+            var = getattr(self, atr)
+            if not hasattr(var, 'reflect'):
+                continue
+            try:
+                var.reflect(plane)
+            except Exception as e:
+                import traceback
+                traceback.print_exc()
+                raise Exception('Failed to reflect {}: {}'.format(var, e))
+
+    def scale(self, factor, origin=None):
+        """Apply a scale transform to extension attributes.
+
+        This is useful in cases where extension attributes possess geometric data
+        that should be scaled alongside the host object.
+
+        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).
+        """
+        for atr in self._extension_attributes:
+            var = getattr(self, atr)
+            if not hasattr(var, 'scale'):
+                continue
+            try:
+                var.scale(factor, origin)
+            except Exception as e:
+                import traceback
+                traceback.print_exc()
+                raise Exception('Failed to scale {}: {}'.format(var, e))
+
+    def _duplicate_extension_attr(self, original_properties):
+        """Duplicate the attributes added by extensions.
+
+        This method should be called within the duplicate or __copy__ methods of
+        each dragonfly-core geometry object after the core object has been duplicated.
+        This method only needs to be called on the new (duplicated) core object and
+        the extension properties of the original core object should be passed to
+        this method as the original_properties.
+
+        Args:
+            original_properties: The properties object of the original core
+                object from which the duplicate was derived.
+        """
+        for atr in self._extension_attributes:
+            var = getattr(original_properties, atr)
+            if not hasattr(var, 'duplicate'):
+                continue
+            try:
+                setattr(self, '_' + atr, var.duplicate(self.host))
+            except Exception as e:
+                import traceback
+                traceback.print_exc()
+                raise Exception('Failed to duplicate {}: {}'.format(var, e))
+
+    def _add_prefix_extension_attr(self, prefix):
+        """Change the identifier of attributes unique to this object by adding a prefix.
+
+        This is particularly useful in workflows where you duplicate and edit
+        a starting object and then want to combine it with the original object
+        into one Model (like making a model of repeated buildings).
+
+        Notably, this method only adds the prefix to extension attributes that must
+        be unique to the object and does not add the prefix to attributes that are
+        shared across several objects.
+
+        Args:
+            prefix: Text that will be inserted at the start of the extension attributes'
+                identifier. It is recommended that this prefix be short to avoid maxing
+                out the 100 allowable characters for honeybee identifiers.
+        """
+        for atr in self._extension_attributes:
+            var = getattr(self, atr)
+            if not hasattr(var, 'add_prefix'):
+                continue
+            try:
+                var.add_prefix(prefix)
+            except Exception as e:
+                import traceback
+                traceback.print_exc()
+                raise Exception('Failed to add prefix to {}: {}'.format(var, e))
+
+    def _add_extension_attr_to_honeybee(self, host, honeybee_properties):
+        """Add Dragonfly properties for extensions to Honeybee extension properties.
+
+        This method should be called within the to_honeybee method for any
+        dragonfly-core geometry object that maps directly to a honeybee-core object.
+
+        Args:
+            host: A honeybee-core object that hosts these properties.
+            honeybee_properties: A honeybee-core Properties object to which the
+                dragonfly-core extension attributes will be added.
+        """
+        for atr in self._extension_attributes:
+            var = getattr(self, atr)
+            if not hasattr(var, 'to_honeybee'):
+                continue
+            try:
+                setattr(honeybee_properties, '_' + atr, var.to_honeybee(host))
+            except Exception as e:
+                import traceback
+                traceback.print_exc()
+                raise Exception('Failed to translate {} to_honeybee: {}'.format(var, e))
+        return honeybee_properties
+
+    def _add_extension_attr_to_dict(self, base, abridged, include):
+        """Add attributes for extensions to the base dictionary.
+
+        This method should be called within the to_dict method of each dragonfly-core
+        geometry object.
+
+        Args:
+            base: The dictionary of the core object without any extension
+                attributes. This method will add extension attributes to this
+                dictionary. For example, energy properties will appear under
+                base['properties']['energy'].
+            abridged: Boolean to note whether the attributes of the extensions should
+                be abridged (True) or full (False). For example, if a Room's energy
+                properties are abridged, the program_type attribute under the energy
+                properties dictionary will just be the identifier of the program_type. If
+                it is full (not abridged), the program_type will be a complete
+                dictionary following the ProgramType schema. Abridged dictionaries
+                should be used within the Model.to_dict but full dictionaries should
+                be used within the to_dict methods of individual objects.
+            include: List of properties to filter keys that must be included in
+                output dictionary. For example ['energy'] will include 'energy' key if
+                available in properties to_dict. By default all the keys will be
+                included. To exclude all the keys from extensions use an empty list.
+        """
+        attr = include if include is not None else self._extension_attributes
+        for atr in attr:
+            var = getattr(self, atr)
+            if not hasattr(var, 'to_dict'):
+                continue
+            try:
+                base.update(var.to_dict(abridged))
+            except Exception as e:
+                import traceback
+                traceback.print_exc()
+                raise Exception('Failed to convert {} to a dict: {}'.format(var, e))
+        return base
+
+    def _load_extension_attr_from_dict(self, property_dict):
+        """Get attributes for extensions from a dictionary of the properties.
+
+        This method should be called within the from_dict method of each dragonfly-core
+        geometry object. Specifically, this method should be called on the core
+        object after it has been created from a dictionary but lacks any of the
+        extension attributes in the dictionary.
+
+        Args:
+            property_dict: A dictionary of properties for the object (ie.
+                StoryProperties, BuildingProperties). These will be used to load
+                attributes from the dictionary and assign them to the object on which
+                this method is called.
+        """
+        for atr in self._extension_attributes:
+            var = getattr(self, atr)
+            if not hasattr(var, 'from_dict'):
+                continue
+            try:
+                setattr(self, '_' + atr, var.__class__.from_dict(
+                    property_dict[atr], self.host))
+            except KeyError:
+                pass  # the property_dict possesses no properties for that extension
+
+    def ToString(self):
+        """Overwrite .NET ToString method."""
+        return self.__repr__()
+
+    def __repr__(self):
+        """Properties representation."""
+        return 'BaseProperties'
+
+
+
[docs]class ModelProperties(_Properties): + """Dragonfly Model Properties. This class will be extended by extensions. + + Usage: + + .. code-block:: python + + model = Model('South Boston District', list_of_buildings) + model.properties -> ModelProperties + model.properties.radiance -> ModelRadianceProperties + model.properties.energy -> ModelEnergyProperties + """ + +
[docs] def to_dict(self, include=None): + """Convert properties to dictionary. + + Args: + include: A list of keys to be included in dictionary. + If None all the available keys will be included. + """ + base = {'type': 'ModelProperties'} + attr = include if include is not None else self._extension_attributes + for atr in attr: + var = getattr(self, atr) + if not hasattr(var, 'to_dict'): + continue + try: + base.update(var.to_dict()) # no abridged dictionary for model + except Exception as e: + import traceback + traceback.print_exc() + raise Exception('Failed to convert {} to a dict: {}'.format(var, e)) + return base
+ +
[docs] def apply_properties_from_dict(self, data): + """Apply extension properties from a Model dictionary to the host Model. + + Args: + data: A dictionary representation of an entire dragonfly-core Model. + """ + for atr in self._extension_attributes: + if atr not in data['properties'] or data['properties'][atr] is None: + continue + var = getattr(self, atr) + if not hasattr(var, 'apply_properties_from_dict'): + continue + try: + var.apply_properties_from_dict(data) + except Exception as e: + import traceback + traceback.print_exc() + raise Exception( + 'Failed to apply {} properties to the Model: {}'.format(atr, e))
+ +
[docs] def to_honeybee(self, host): + """Convert this Model's extension properties to honeybee Model properties. + + Args: + host: A honeybee-core Model object that hosts these properties. + """ + hb_prop = hb_properties.ModelProperties(host) + return self._add_extension_attr_to_honeybee(host, hb_prop)
+ + def _check_extension_attr(self): + """Check the attributes of extensions. + + This method should be called within the check_all method of the Model object + to ensure that the check_all functions of any extension model properties + are also called. + """ + msgs = [] + for atr in self._extension_attributes: + var = getattr(self, atr) + if not hasattr(var, 'check_all'): + continue + try: + msgs.append(var.check_all(raise_exception=False)) + except Exception as e: + import traceback + traceback.print_exc() + raise Exception('Failed to check_all for {}: {}'.format(var, e)) + return msgs + + def __repr__(self): + """Properties representation.""" + return 'ModelProperties'
+ + +
[docs]class ContextShadeProperties(_Properties): + """Dragonfly ContextShade properties. This class will be extended by extensions. + + Usage: + + .. code-block:: python + + canopy = ContextShade('Outdoor Canopy', canopy_geo) + canopy.properties -> ContextShadeProperties + canopy.properties.radiance -> ContextShadeRadianceProperties + canopy.properties.energy -> ContextShadeEnergyProperties + """ + +
[docs] def to_dict(self, abridged=False, include=None): + """Convert properties to dictionary. + + Args: + abridged: Boolean to note whether the full dictionary describing the + object should be returned (False) or just an abridged version (True). + Default: False. + include: A list of keys to be included in dictionary. + If None all the available keys will be included. + """ + base = {'type': 'ContextShadeProperties'} if not abridged else \ + {'type': 'ContextShadePropertiesAbridged'} + + base = self._add_extension_attr_to_dict(base, abridged, include) + return base
+ +
[docs] def to_honeybee(self, host, is_mesh=False): + """Convert this ContextShade's extension properties to honeybee Shade properties. + + Args: + host: A honeybee-core Shade object that hosts these properties. + is_mesh: Boolean to note whether the input host is a ShadeMesh as opposed + to a regular Shade. (Default: False). + """ + hb_prop = hb_properties.ShadeMeshProperties(host) \ + if is_mesh else hb_properties.ShadeProperties(host) + return self._add_extension_attr_to_honeybee(host, hb_prop)
+ +
[docs] def add_prefix(self, prefix): + """Change the identifier of attributes unique to this object by adding a prefix. + + Notably, this method only adds the prefix to extension attributes that must + be unique to the ContextShade and does not add the prefix to attributes that are + shared across several ContextShades. + + Args: + prefix: Text that will be inserted at the start of extension + attribute identifiers. + """ + self._add_prefix_extension_attr(prefix)
+ +
[docs] def from_honeybee(self, hb_properties): + """Transfer extension attributes from a Honeybee Shade to Dragonfly ContextShade. + + This method should be called within the from_honeybee method. + + Args: + hb_properties: The properties of the honeybee Shade that is being + translated to a Dragonfly ContextShade. + """ + for atr in self._extension_attributes: + var = getattr(self, atr) + if not hasattr(var, 'from_honeybee') or not \ + hasattr(hb_properties, atr): + continue + try: + hb_var = getattr(hb_properties, atr) + var.from_honeybee(hb_var) + except Exception as e: + import traceback + traceback.print_exc() + raise Exception( + 'Failed to translate {} from_honeybee: {}'.format(var, e))
+ + def __repr__(self): + """Properties representation.""" + return 'ContextShadeProperties'
+ + +
[docs]class BuildingProperties(_Properties): + """Dragonfly Building properties. This class will be extended by extensions. + + Usage: + + .. code-block:: python + + building = Building('Office Tower', unique_stories) + building.properties -> BuildingProperties + building.properties.radiance -> BuildingRadianceProperties + building.properties.energy -> BuildingEnergyProperties + """ + +
[docs] def to_dict(self, abridged=False, include=None): + """Convert properties to dictionary. + + Args: + abridged: Boolean to note whether the full dictionary describing the + object should be returned (False) or just an abridged version (True). + Default: False. + include: A list of keys to be included in dictionary. + If None all the available keys will be included. + """ + base = {'type': 'BuildingProperties'} if not abridged else \ + {'type': 'BuildingPropertiesAbridged'} + + base = self._add_extension_attr_to_dict(base, abridged, include) + return base
+ +
[docs] def apply_properties_from_geojson_dict(self, data): + """Apply extension properties to a host Building from a geoJSON dictionary. + + Args: + data: A dictionary representation of a geoJSON feature properties. + Specifically, this should be the "properties" key describing + a Polygon or MultiPolygon object. + """ + for atr in self._extension_attributes: + var = getattr(self, atr) + if not hasattr(var, 'apply_properties_from_geojson_dict'): + continue + try: + var.apply_properties_from_geojson_dict(data) + except Exception as e: + import traceback + traceback.print_exc() + raise Exception( + 'Failed to apply {} properties to the Building: {}'.format(atr, e))
+ +
[docs] def add_prefix(self, prefix): + """Change the identifier of attributes unique to this object by adding a prefix. + + Notably, this method only adds the prefix to extension attributes that must + be unique to the Building and does not add the prefix to attributes that are + shared across several Buildings. + + Args: + prefix: Text that will be inserted at the start of extension + attribute identifiers. + """ + self._add_prefix_extension_attr(prefix)
+ + def __repr__(self): + """Properties representation.""" + return 'BuildingProperties'
+ + +
[docs]class StoryProperties(_Properties): + """Dragonfly Story properties. This class will be extended by extensions. + + Usage: + + .. code-block:: python + + story = Story('Ground Floor Retail', room_2ds) + story.properties -> StoryProperties + story.properties.radiance -> StoryRadianceProperties + story.properties.energy -> StoryEnergyProperties + """ + +
[docs] def to_dict(self, abridged=False, include=None): + """Convert properties to dictionary. + + Args: + abridged: Boolean to note whether the full dictionary describing the + object should be returned (False) or just an abridged version (True). + Default: False. + include: A list of keys to be included in dictionary. + If None all the available keys will be included. + """ + base = {'type': 'StoryProperties'} if not abridged else \ + {'type': 'StoryPropertiesAbridged'} + + base = self._add_extension_attr_to_dict(base, abridged, include) + return base
+ +
[docs] def add_prefix(self, prefix): + """Change the identifier of attributes unique to this object by adding a prefix. + + Notably, this method only adds the prefix to extension attributes that must + be unique to the Story and does not add the prefix to attributes that are + shared across several Stories. + + Args: + prefix: Text that will be inserted at the start of extension + attribute identifiers. + """ + self._add_prefix_extension_attr(prefix)
+ + def __repr__(self): + """Properties representation.""" + return 'StoryProperties'
+ + +
[docs]class Room2DProperties(_Properties): + """Dragonfly Room2D properties. This class will be extended by extensions. + + Usage: + + .. code-block:: python + + room = Room2D('Office', geometry) + room.properties -> Room2DProperties + room.properties.radiance -> Room2DRadianceProperties + room.properties.energy -> Room2DEnergyProperties + """ + +
[docs] def to_dict(self, abridged=False, include=None): + """Convert properties to dictionary. + + Args: + abridged: Boolean to note whether the full dictionary describing the + object should be returned (False) or just an abridged version (True). + Default: False. + include: A list of keys to be included in dictionary. + If None all the available keys will be included. + """ + base = {'type': 'Room2DProperties'} if not abridged else \ + {'type': 'Room2DPropertiesAbridged'} + + base = self._add_extension_attr_to_dict(base, abridged, include) + return base
+ +
[docs] def to_honeybee(self, host): + """Convert this Room2D's extension properties to honeybee Room properties. + + Args: + host: A honeybee-core Room object that hosts these properties. + """ + hb_prop = hb_properties.RoomProperties(host) + return self._add_extension_attr_to_honeybee(host, hb_prop)
+ +
[docs] def add_prefix(self, prefix): + """Change the identifier of attributes unique to this object by adding a prefix. + + Notably, this method only adds the prefix to extension attributes that must + be unique to the Room2D (eg. single-room HVAC systems) and does not add + the prefix to attributes that are shared across several Rooms2Ds (eg. + ConstructionSets). + + Args: + prefix: Text that will be inserted at the start of extension + attribute identifiers. + """ + self._add_prefix_extension_attr(prefix)
+ +
[docs] def from_honeybee(self, hb_properties): + """Transfer extension attributes from a Honeybee Room to Dragonfly Room2D. + + This method should be called within the from_honeybee method. + + Args: + hb_properties: The properties of the honeybee Room that is being + translated to a Dragonfly Room2D. + """ + for atr in self._extension_attributes: + var = getattr(self, atr) + if not hasattr(var, 'from_honeybee') or not \ + hasattr(hb_properties, atr): + continue + try: + hb_var = getattr(hb_properties, atr) + var.from_honeybee(hb_var) + except Exception as e: + import traceback + traceback.print_exc() + raise Exception( + 'Failed to translate {} from_honeybee: {}'.format(var, e))
+ + def __repr__(self): + """Properties representation.""" + return 'Room2DProperties'
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/roof.html b/docs/_modules/dragonfly/roof.html new file mode 100644 index 00000000..26ba6faa --- /dev/null +++ b/docs/_modules/dragonfly/roof.html @@ -0,0 +1,1431 @@ + + + + + + + dragonfly.roof — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.roof

+# coding: utf-8
+"""Roof specification with instructions for generating sloped roofs over a Story."""
+from __future__ import division
+import math
+
+from ladybug_geometry.geometry2d import Vector2D, Point2D, Ray2D, LineSegment2D, \
+    Polygon2D
+from ladybug_geometry.geometry3d import Vector3D, Point3D, Face3D, Polyface3D
+from ladybug_geometry.intersection2d import closest_point2d_on_line2d, \
+    closest_point2d_on_line2d_infinite
+
+
+
[docs]class RoofSpecification(object): + """A roof specification with instructions for generating sloped roofs over a Story. + + Args: + geometry: An array of Face3D objects representing the geometry of the Roof. + None of these geometries should overlap in plan and, together, these + Face3D should either completely cover or skip each Room2D of the Story + to which the RoofSpecification is assigned. + + Properties: + * geometry + * boundary_geometry_2d + * planes + * parent + * has_parent + * min + * max + """ + __slots__ = ('_geometry', '_parent', '_ridge_line_info', '_ridge_line_tolerance') + + def __init__(self, geometry): + """Initialize RoofSpecification.""" + self.geometry = geometry + self._parent = None # will be set when RoofSpecification is added to a Story + +
[docs] @classmethod + def from_dict(cls, data): + """Initialize an RoofSpecification from a dictionary. + + Args: + data: A dictionary representation of an RoofSpecification object. + """ + # check the type of dictionary + assert data['type'] == 'RoofSpecification', 'Expected RoofSpecification ' \ + 'dictionary. Got {}.'.format(data['type']) + geometry = tuple(Face3D.from_dict(shd_geo) for shd_geo in data['geometry']) + return cls(geometry)
+ + @property + def geometry(self): + """Get or set a tuple of Face3D objects representing the geometry of the Roof. + """ + return self._geometry + + @geometry.setter + def geometry(self, value): + if not isinstance(value, tuple): + value = tuple(value) + assert len(value) > 0, 'RoofSpecification must have at least one Face3D.' + for geo in value: + assert isinstance(geo, Face3D), \ + 'Expected Face3D for RoofSpecification. Got {}'.format(type(geo)) + self._geometry = value + self._ridge_line_info = None + self._ridge_line_tolerance = None + + @property + def boundary_geometry_2d(self): + """Get a tuple of Polygon2D for the boundaries around each Face3D in geometry. + + These Polygons will be in the World XY coordinate system instead of the + coordinate system of the Face3D's plane. + """ + return tuple( + Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in geo.boundary)) + for geo in self._geometry) + + @property + def planes(self): + """Get a tuple of Planes for each Face3D in geometry. + """ + return tuple(geo.plane for geo in self._geometry) + + @property + def parent(self): + """Parent Story if assigned. None if not assigned.""" + return self._parent + + @property + def has_parent(self): + """Boolean noting whether this RoofSpecification has a parent Story.""" + return self._parent is not None + + @property + def min(self): + """Get a Point2D for the min bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this RoofSpecification is in + proximity to other objects. + """ + return self._calculate_min(self._geometry) + + @property + def max(self): + """Get a Point2D for the max bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this RoofSpecification is in + proximity to other objects. + """ + return self._calculate_max(self._geometry) + +
[docs] def overlap_count(self, tolerance=0.01): + """Get the number of times that the Roof geometries overlap with one another. + + This should be zero for the RoofSpecification to be valid. + + Args: + tolerance: The minimum distance that two Roof geometries can overlap + with one another and still be considered valid. Default: 0.01, + suitable for objects in meters. + + Returns: + An integer for the number of times that the roof geometries overlap + with one another beyond the tolerance. + """ + geo_2d = self.boundary_geometry_2d + overlap_count = 0 + for i, poly_1 in enumerate(geo_2d): + try: + for poly_2 in geo_2d[i + 1:]: + if poly_1.polygon_relationship(poly_2, tolerance) >= 0: + overlap_count += 1 + except IndexError: + pass # we have reached the end of the list + return overlap_count
+ +
[docs] def move(self, moving_vec): + """Move this RoofSpecification along a vector. + + Args: + moving_vec: A ladybug_geometry Vector3D with the direction and distance + to move the object. + """ + self._geometry = tuple(geo.move(moving_vec) for geo in self._geometry)
+ +
[docs] def rotate_xy(self, angle, origin): + """Rotate RoofSpecification counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle in degrees. + origin: A ladybug_geometry Point3D for the origin around which the + object will be rotated. + """ + self._geometry = tuple(geo.rotate_xy(math.radians(angle), origin) + for geo in self._geometry)
+ +
[docs] def reflect(self, plane): + """Reflect this RoofSpecification across a plane. + + Args: + plane: A ladybug_geometry Plane across which the object will be reflected. + """ + self._geometry = tuple(geo.reflect(plane.n, plane.o) for geo in self._geometry)
+ +
[docs] def scale(self, factor, origin=None): + """Scale this RoofSpecification by a factor from an origin point. + + 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). + """ + self._geometry = tuple(geo.scale(factor, origin) for geo in self._geometry)
+ +
[docs] def update_geometry_3d(self, new_face_3d, face_index): + """Change one of the Face3D in this RoofSpecification.geometry. + + This method is intended to be used when the roof geometry has been edited + by some external means and this RoofSpecification should be updated + for coordination. + + Args: + new_face_3d: A Face3D for a new roof geometry that is to replace one + of the existing Face3D in the roof. + face_index: An integer for the index of the Face3D in the roof to + be replaced. + """ + assert isinstance(new_face_3d, Face3D), \ + 'Expected Face3D for RoofSpecification.update_geometry_3d. ' \ + 'Got {}'.format(type(new_face_3d)) + geo_list = list(self._geometry) + geo_list[face_index] = new_face_3d + self._geometry = tuple(geo_list) + self._ridge_line_info = None + self._ridge_line_tolerance = None
+ +
[docs] def update_geometry_2d(self, new_polygon_2d, polygon_index): + """Change one of the Face3D in this roof by supplying a 2D Polygon. + + This method is intended to be used when the roof geometry has been edited + by some external means and this RoofSpecification should be updated + for coordination. It it particularly helpful when the external means + of editing has happened in 2D plan view and only the boundary of the + roof should be updated while the plane of the roof geometry is held + constant. + + Args: + new_polygon_2d: A Polygon2D for a new roof geometry that is to replace + one of the existing geometries in the roof. Ideally, this is + one of this RoofSpecification's boundary_geometry_2d polygons + that has been edited. + polygon_index: An integer for the index of the boundary polygon in + the roof to be replaced. + """ + assert isinstance(new_polygon_2d, Polygon2D), \ + 'Expected Polygon2D for RoofSpecification.update_geometry_2d. ' \ + 'Got {}'.format(type(new_polygon_2d)) + proj_dir = Vector3D(0, 0, 1) # direction to project onto Roof planes + roof_plane = self.geometry[polygon_index].plane + roof_verts = [] + for pt2 in new_polygon_2d.vertices: + pt3 = roof_plane.project_point(Point3D(pt2.x, pt2.y), proj_dir) + roof_verts.append(pt3) + new_face_3d = Face3D(roof_verts, plane=roof_plane) + self.update_geometry_3d(new_face_3d, polygon_index)
+ +
[docs] def align(self, line_ray, distance, tolerance=0.01): + """Move naked roof vertices within a given distance of a line to be on that line. + + This is useful for coordinating the Roof specification with the alignment + of Room2Ds that belong to the same Story as this Roof. + + Note that the planes of the input roof Face3Ds will be preserved. This way, + the internal structure of the roof geometry will be conserved but the roof will + be extended to cover Room2Ds that might have otherwise been aligned to + the point that they have no Roof geometry above them. + + Args: + line_ray: A ladybug_geometry Ray2D or LineSegment2D to which the roof + vertices will be aligned. Ray2Ds will be interpreted as being infinite + in both directions while LineSegment2Ds will be interpreted as only + existing between two points. + distance: The maximum distance between a vertex and the line_ray where + the vertex will be moved to lie on the line_ray. Vertices beyond + this distance will be left as they are. + tolerance: The minimum distance between vertices below which they are + considered co-located. This is used to ensure that the alignment process + does not create new overlaps in the roof geometry. (Default: 0.01, + suitable for objects in meters). + """ + # check the input line_ray + if isinstance(line_ray, Ray2D): + closest_func = closest_point2d_on_line2d_infinite + elif isinstance(line_ray, LineSegment2D): + closest_func = closest_point2d_on_line2d + else: + msg = 'Expected Ray2D or LineSegment2D. Got {}.'.format(type(line_ray)) + raise TypeError(msg) + + # get the polygons and intersect them for matching segments + polygons, planes = self.boundary_geometry_2d, self.planes + poly_ridge_info = self._compute_ridge_line_info(tolerance) + + # loop through the polygons and align the vertices + new_polygons = [] + for poly, poly_info in zip(polygons, poly_ridge_info): + new_poly = [] + for pt, pt_info in zip(poly, poly_info): + if len(pt_info) == 0: # not on a ridge line; move it anywhere + close_pt = closest_func(pt, line_ray) + if pt.distance_to_point(close_pt) <= distance: + new_poly.append(close_pt) + else: + new_poly.append(pt) + elif len(pt_info) == 1: # only move it along a singe ridge line + r_line = pt_info[0] + vec_ang = math.degrees(r_line.v.angle(line_ray.v)) + if 1 <= vec_ang <= 179: # not parallel; ridge will be intact + close_pt = closest_func(pt, line_ray) + if pt.distance_to_point(close_pt) <= distance: + new_poly.append(close_pt) + else: + new_poly.append(pt) + else: + new_poly.append(pt) + else: # multiple ridge lines; don't move that point! + new_poly.append(pt) + new_polygons.append(new_poly) + + # project the points back onto the roof + proj_dir = Vector3D(0, 0, 1) + new_geo = [] + for poly, r_pl in zip(new_polygons, planes): + new_pts3d = [] + for pt2 in poly: + new_pts3d.append(r_pl.project_point(Point3D.from_point2d(pt2), proj_dir)) + new_geo.append(Face3D(new_pts3d, plane=r_pl)) + self.geometry = new_geo
+ +
[docs] def snap_to_grid(self, grid_increment, tolerance=0.01): + """Snap naked roof vertices to the nearest grid node defined by an increment. + + This is useful for coordinating the Roof specification with the grid snapping + of Room2Ds that belong to the same Story as this Roof. + + Note that the planes of the input roof Face3Ds will be preserved. This way, + the internal structure of the roof geometry will be conserved but the roof + will be extended to cover Room2Ds that might have otherwise been snapped to + the a node where they have no Roof geometry above them. + + Args: + grid_increment: A positive number for dimension of each grid cell. This + typically should be equal to the tolerance or larger but should + not be larger than the smallest detail of the Room2D that you + wish to resolve. + tolerance: The minimum distance between vertices below which they are + considered co-located. (Default: 0.01, + suitable for objects in meters). + """ + # get the polygons and intersect them for matching segments + polygons, planes = self.boundary_geometry_2d, self.planes + poly_ridge_info = self._compute_ridge_line_info(tolerance) + + # loop through the polygons and snap the vertices + new_polygons = [] + for poly, poly_info in zip(polygons, poly_ridge_info): + new_poly = [] + for pt, pt_info in zip(poly, poly_info): + if len(pt_info) == 0: # not on a ridge line; move it anywhere + new_x = grid_increment * round(pt.x / grid_increment) + new_y = grid_increment * round(pt.y / grid_increment) + new_poly.append(Point2D(new_x, new_y, pt.z)) + else: # on a ridge line; don't move that point! + new_poly.append(pt) + new_polygons.append(new_poly) + + # project the points back onto the roof + proj_dir = Vector3D(0, 0, 1) + new_geo = [] + for poly, r_pl in zip(new_polygons, planes): + new_pts3d = [] + for pt2 in poly: + new_pts3d.append(r_pl.project_point(Point3D.from_point2d(pt2), proj_dir)) + new_geo.append(Face3D(new_pts3d, plane=r_pl)) + self.geometry = new_geo
+ + def _compute_ridge_line_info(self, tolerance): + """Get a matrix of values for the ridge lines associated with each vertex. + + Ridge lines are defined as lines shared between two roof geometries. + + The matrix will have one sub-list for each polygon in the boundary_geometry_2d + and each sub-list will contain a sub-sub-list for each vertex. This sub-sub-list + with contain LineSegment2Ds for each ridge line that the vertex is a part of. + Vertices that belong to only one roof geometry will get an empty list in + the matrix, indicating that the vertex can be moved in any direction without + changing the roof structure. Vertices belonging to two roof geometries will + get a single ridge line LineSegment2D in the list, indicating that the vertex can + be moved along this vector without changing the structure of the whole roof. + Vertices belonging to more than one roof geometry will get multiple + LineSegment2Ds in the list, which usually means that moving the vertex in + any direction will change the Roof structure. + + This method is hidden because it caches the results, meaning that it does + not need to be recomputed for multiple alignment lines when the roof geometry + or the tolerance does not change. + """ + if self._ridge_line_info is None or self._ridge_line_tolerance != tolerance: + # turn the polygons into Face3D in the XY plane + proj_faces = [] + for poly in self.boundary_geometry_2d: + proj_face = Face3D(tuple(Point3D(pt.x, pt.y) for pt in poly)) + proj_faces.append(proj_face) + # join the projected Face3D into a Polyface3D and get all naked edges + roof_p_face = Polyface3D.from_faces(proj_faces, tolerance) + roof_p_face = roof_p_face.merge_overlapping_edges( + tolerance, math.radians(1)) + internal_ed = roof_p_face.internal_edges + # check whether each Face3D vertex lies on an internal edge + ridge_info = [] + for proj_face in proj_faces: + face_info = [] + for pt in proj_face.boundary: + pt_rid = [] + for ed in internal_ed: + if ed.distance_to_point(pt) <= tolerance: + ln_2 = LineSegment2D( + Point2D(ed.p.x, ed.p.y), Vector2D(ed.v.x, ed.v.y)) + pt_rid.append(ln_2) + face_info.append(pt_rid) + ridge_info.append(face_info) + self._ridge_line_info = ridge_info + self._ridge_line_tolerance = tolerance + return self._ridge_line_info + +
[docs] def to_dict(self): + """Return RoofSpecification as a dictionary.""" + base = {'type': 'RoofSpecification'} + base['geometry'] = [geo.to_dict() for geo in self._geometry] + return base
+ +
[docs] def duplicate(self): + """Get a copy of this object.""" + return self.__copy__()
+ + @staticmethod + def _calculate_min(geometry_objects): + """Calculate min Point2D around an array of geometry with min attributes. + + This is used in all functions that calculate bounding rectangles around + dragonfly objects and assess when two objects are in close proximity. + """ + min_pt = [geometry_objects[0].min.x, geometry_objects[0].min.y] + + for room in geometry_objects[1:]: + if room.min.x < min_pt[0]: + min_pt[0] = room.min.x + if room.min.y < min_pt[1]: + min_pt[1] = room.min.y + + return Point2D(min_pt[0], min_pt[1]) + + @staticmethod + def _calculate_max(geometry_objects): + """Calculate max Point2D around an array of geometry with max attributes. + + This is used in all functions that calculate bounding rectangles around + dragonfly objects and assess when two objects are in close proximity. + """ + max_pt = [geometry_objects[0].max.x, geometry_objects[0].max.y] + + for room in geometry_objects[1:]: + if room.max.x > max_pt[0]: + max_pt[0] = room.max.x + if room.max.y > max_pt[1]: + max_pt[1] = room.max.y + + return Point2D(max_pt[0], max_pt[1]) + + def __copy__(self): + return RoofSpecification(self._geometry) + + def __len__(self): + return len(self._geometry) + + def __getitem__(self, key): + return self._geometry[key] + + def __iter__(self): + return iter(self._geometry) + +
[docs] def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__()
+ + def __repr__(self): + return 'RoofSpecification: [{} geometries]'.format(len(self._geometry))
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/room2d.html b/docs/_modules/dragonfly/room2d.html new file mode 100644 index 00000000..1847d530 --- /dev/null +++ b/docs/_modules/dragonfly/room2d.html @@ -0,0 +1,5435 @@ + + + + + + + dragonfly.room2d — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.room2d

+# coding: utf-8
+"""Dragonfly Room2D."""
+from __future__ import division
+
+import math
+
+from ladybug_geometry.geometry2d import Point2D, Vector2D, Ray2D, LineSegment2D, \
+    Polyline2D, Polygon2D
+from ladybug_geometry.geometry3d import Point3D, Vector3D, Ray3D, LineSegment3D, \
+    Plane, Polyline3D, Face3D, Polyface3D
+from ladybug_geometry.intersection2d import closest_point2d_between_line2d
+from ladybug_geometry.intersection3d import closest_point3d_on_line3d, \
+    closest_point3d_on_line3d_infinite, intersect_line3d_plane_infinite
+import ladybug_geometry.boolean as pb
+from ladybug_geometry_polyskel.polysplit import perimeter_core_subfaces
+
+from honeybee.typing import float_positive, clean_string, clean_and_id_string
+import honeybee.boundarycondition as hbc
+from honeybee.boundarycondition import boundary_conditions as bcs
+from honeybee.boundarycondition import _BoundaryCondition, Outdoors, Surface, Ground
+from honeybee.facetype import Floor, Wall, AirBoundary, RoofCeiling
+from honeybee.facetype import face_types as ftyp
+from honeybee.shade import Shade
+from honeybee.door import Door
+from honeybee.face import Face
+from honeybee.room import Room
+
+from ._base import _BaseGeometry
+from .properties import Room2DProperties
+import dragonfly.windowparameter as glzpar
+from dragonfly.windowparameter import _WindowParameterBase, _AsymmetricBase, \
+    SimpleWindowRatio, RectangularWindows, DetailedWindows
+import dragonfly.skylightparameter as skypar
+from dragonfly.skylightparameter import _SkylightParameterBase, DetailedSkylights, \
+    GriddedSkylightArea, GriddedSkylightRatio
+import dragonfly.shadingparameter as shdpar
+from dragonfly.shadingparameter import _ShadingParameterBase
+import dragonfly.writer.room2d as writer
+
+
+
[docs]class Room2D(_BaseGeometry): + """A volume defined by an extruded floor plate, representing a single room or space. + + Args: + identifier: Text string for a unique Room2D ID. Must be < 100 characters and + not contain any spaces or special characters. + floor_geometry: A single horizontal Face3D object representing the + floor plate of the Room. Note that this Face3D must be horizontal + to be valid. + floor_to_ceiling_height: A number for the height above the floor where the + ceiling begins. This should be in the same units system as the input + floor_geometry. Typical values range from 3 to 5 meters. + boundary_conditions: A list of boundary conditions that match the number of + segments in the input floor_geometry. These will be used to assign + boundary conditions to each of the walls of the Room in the resulting + model. If None, all boundary conditions will be Outdoors or Ground + depending on whether ceiling of the room is below 0 (the assumed + ground plane). Default: None. + window_parameters: A list of WindowParameter objects that dictate how the + window geometries will be generated for each of the walls. If None, + no windows will exist over the entire Room2D. Default: None. + shading_parameters: A list of ShadingParameter objects that dictate how the + shade geometries will be generated for each of the walls. If None, + no shades will exist over the entire Room2D. Default: None. + is_ground_contact: A boolean noting whether this Room2D has its floor + in contact with the ground. Default: False. + is_top_exposed: A boolean noting whether this Room2D has its ceiling + exposed to the outdoors. Default: False. + tolerance: The maximum difference between z values at which point vertices + are considered to be in the same horizontal plane. This is used to check + that all vertices of the input floor_geometry lie in the same horizontal + floor plane. Default is 0, which will not perform any check. + + Properties: + * identifier + * display_name + * full_id + * floor_geometry + * floor_to_ceiling_height + * boundary_conditions + * window_parameters + * shading_parameters + * air_boundaries + * is_ground_contact + * is_top_exposed + * skylight_parameters + * parent + * has_parent + * floor_segments + * floor_segments_2d + * segment_count + * segment_normals + * floor_height + * ceiling_height + * volume + * floor_area + * exterior_wall_area + * exterior_aperture_area + * is_core + * is_perimeter + * min + * max + * center + * user_data + """ + __slots__ = ('_floor_geometry', '_segment_count', '_floor_to_ceiling_height', + '_boundary_conditions', '_window_parameters', '_shading_parameters', + '_air_boundaries', '_is_ground_contact', '_is_top_exposed', + '_skylight_parameters', '_parent', '_abridged_properties') + + def __init__(self, identifier, floor_geometry, floor_to_ceiling_height, + boundary_conditions=None, window_parameters=None, + shading_parameters=None, is_ground_contact=False, is_top_exposed=False, + tolerance=0): + """A volume defined by an extruded floor plate, representing a single room.""" + _BaseGeometry.__init__(self, identifier) # process the identifier + + # process the floor_geometry + assert isinstance(floor_geometry, Face3D), \ + 'Expected ladybug_geometry Face3D. Got {}'.format(type(floor_geometry)) + if floor_geometry.normal.z >= 0: # ensure upward-facing Face3D + self._floor_geometry = floor_geometry + else: + self._floor_geometry = floor_geometry.flip() + # ensure a global 2D origin, which helps in solve adjacency and the dict schema + o_pl = Plane(Vector3D(0, 0, 1), Point3D(0, 0, self._floor_geometry.plane.o.z)) + self._floor_geometry = Face3D(self._floor_geometry.boundary, + o_pl, self._floor_geometry.holes) + # check that the floor_geometry lies in the same horizontal plane. + if tolerance != 0: + z_vals = tuple(pt.z for pt in self._floor_geometry.vertices) + assert max(z_vals) - min(z_vals) <= tolerance, 'Not all of Room2D ' \ + '"{}" vertices lie within the same horizontal plane.'.format(identifier) + + # process segment count and floor-to-ceiling height + self._segment_count = len(self.floor_segments) + self.floor_to_ceiling_height = floor_to_ceiling_height + + # process the boundary conditions + if boundary_conditions is None: + bc = bcs.outdoors if self.ceiling_height > 0 else bcs.ground + self._boundary_conditions = [bc] * len(self) + else: + value = self._check_wall_assigned_object( + boundary_conditions, 'boundary_conditions') + for val in value: + assert isinstance(val, _BoundaryCondition), \ + 'Expected BoundaryCondition. Got {}'.format(type(value)) + self._boundary_conditions = value + + # process the window and shading parameters + self.window_parameters = window_parameters + self.shading_parameters = shading_parameters + + # ensure all wall-assigned objects align with the geometry if it has been flipped + if floor_geometry.normal.z < 0: + new_bcs, new_win_pars, new_shd_pars = Room2D._flip_wall_assigned_objects( + floor_geometry, self._boundary_conditions, self._window_parameters, + self._shading_parameters) + self._boundary_conditions = new_bcs + self._window_parameters = new_win_pars + self._shading_parameters = new_shd_pars + + # process the top and bottom exposure properties + self.is_ground_contact = is_ground_contact + self.is_top_exposed = is_top_exposed + self._skylight_parameters = None + + self._air_boundaries = None # will be set if it's ever used + self._parent = None # _parent will be set when Room2D is added to a Story + self._abridged_properties = None # will be set when originating from abridged + self._properties = Room2DProperties(self) # properties for extensions + +
[docs] @classmethod + def from_dict(cls, data, tolerance=0, persist_abridged=False): + """Initialize a Room2D from a dictionary. + + Args: + data: A dictionary representation of a Room2D object. + tolerance: The maximum difference between z values at which point vertices + are considered to be in the same horizontal plane. This is used to check + that all vertices of the input floor_geometry lie in the same horizontal + floor plane. Default is 0, which will not perform any check. + persist_abridged: Set to True when the properties of the Room2D dictionary + are abridged and you want to ensure that these exact same abridged + properties persist into the output of Room2D.to_dict(abridged=True). + It is useful when trying to edit the Room2D independently of a + Model and there are no plans to edit any extension properties of + the Room2D. THIS IS AN ADVANCED OPTION. (Default: False). + """ + # check the type of dictionary + assert data['type'] == 'Room2D', 'Expected Room2D dictionary. ' \ + 'Got {}.'.format(data['type']) + + # re-assemble the floor_geometry + bound_verts = [Point3D(pt[0], pt[1], data['floor_height']) + for pt in data['floor_boundary']] + if 'floor_holes' in data: + hole_verts = [[Point3D(pt[0], pt[1], data['floor_height']) + for pt in hole] for hole in data['floor_holes']] + else: + hole_verts = None + floor_geometry = Face3D(bound_verts, None, hole_verts) + + # re-assemble boundary conditions + if 'boundary_conditions' in data and data['boundary_conditions'] is not None: + b_conditions = [] + for bc_dict in data['boundary_conditions']: + try: + bc_class = getattr(hbc, bc_dict['type']) + except AttributeError: + raise ValueError( + 'Boundary condition "{}" is not supported in this honeybee ' + 'installation.'.format(bc_dict['type'])) + b_conditions.append(bc_class.from_dict(bc_dict)) + else: + b_conditions = None + + # re-assemble window parameters + if 'window_parameters' in data and data['window_parameters'] is not None: + glz_pars = [] + for i, glz_dict in enumerate(data['window_parameters']): + if glz_dict is not None: + if glz_dict['type'] == 'DetailedWindows': + segment = cls.floor_segment_by_index(floor_geometry, i) + glz_pars.append(DetailedWindows.from_dict(glz_dict, segment)) + else: + try: + glz_class = getattr(glzpar, glz_dict['type']) + except AttributeError: + raise ValueError( + 'Window parameter "{}" is not recognized.'.format( + glz_dict['type'])) + glz_pars.append(glz_class.from_dict(glz_dict)) + else: + glz_pars.append(None) + else: + glz_pars = None + + # re-assemble shading parameters + if 'shading_parameters' in data and data['shading_parameters'] is not None: + shd_pars = [] + for shd_dict in data['shading_parameters']: + if shd_dict is not None: + try: + shd_class = getattr(shdpar, shd_dict['type']) + except AttributeError: + raise ValueError( + 'Shading parameter "{}" is not recognized.'.format( + shd_dict['type'])) + shd_pars.append(shd_class.from_dict(shd_dict)) + else: + shd_pars.append(None) + else: + shd_pars = None + + # get the top and bottom exposure properties + grnd = data['is_ground_contact'] if 'is_ground_contact' in data else False + top = data['is_top_exposed'] if 'is_top_exposed' in data else False + + # create the Room2D object + room = Room2D(data['identifier'], floor_geometry, + data['floor_to_ceiling_height'], + b_conditions, glz_pars, shd_pars, grnd, top, tolerance) + + # assign any skylight parameters if they are specified + if 'skylight_parameters' in data and data['skylight_parameters'] is not None: + try: + sky_class = getattr(skypar, data['skylight_parameters']['type']) + except AttributeError: + raise ValueError( + 'Skylight parameter "{}" is not recognized.'.format( + data['skylight_parameters']['type'])) + room.skylight_parameters = sky_class.from_dict(data['skylight_parameters']) + + # set all of the other optional properties + if 'air_boundaries' in data and data['air_boundaries'] is not None: + room.air_boundaries = data['air_boundaries'] + if 'display_name' in data and data['display_name'] is not None: + room._display_name = data['display_name'] + if 'user_data' in data and data['user_data'] is not None: + room.user_data = data['user_data'] + + if data['properties']['type'] == 'Room2DProperties': + room.properties._load_extension_attr_from_dict(data['properties']) + elif persist_abridged and \ + data['properties']['type'] == 'Room2DPropertiesAbridged': + room._abridged_properties = data['properties'] + return room
+ +
[docs] @classmethod + def from_honeybee(cls, room, tolerance): + """Initialize a Room2D from a Honeybee Room. + + Note that Dragonfly Room2Ds are abstractions of Honeybee Rooms and there + will be loss of information if the Honeybee Room is not an extruded floor + plate or if extension properties are assigned to individual Faces + or Apertures instead of at the Room level. + + If the Honeybee Room contains no Floor Faces, None will be returned. + + Args: + room: A Honeybee Room object. + tolerance: The maximum difference between values at which point vertices + are considered to be the same. + """ + # first get the floor_geometry for the Room2D using the horizontal boundary + flr_geo = room.horizontal_boundary(match_walls=True, tolerance=tolerance) + flr_geo = flr_geo if flr_geo.normal.z >= 0 else flr_geo.flip() + + # match the segments of the floor geometry to walls of the Room + segs = flr_geo.boundary_segments if flr_geo.holes is None else \ + flr_geo.boundary_segments + \ + tuple(seg for hole in flr_geo.hole_segments for seg in hole) + boundary_conditions = [bcs.outdoors] * len(segs) + window_parameters = [None] * len(segs) + air_bounds = [False] * len(segs) + for i, seg in enumerate(segs): + wall_f = cls._segment_wall_face(room, seg, tolerance) + if wall_f is not None: + boundary_conditions[i] = wall_f.boundary_condition + if len(wall_f._apertures) != 0 or len(wall_f._doors) != 0: + sf_objs = wall_f._apertures + wall_f._doors + w_geos = [sf.geometry for sf in sf_objs] + is_drs = [isinstance(sf, Door) for sf in sf_objs] + if abs(wall_f.normal.z) <= 0.01: # vertical wall + window_parameters[i] = DetailedWindows.from_face3ds( + w_geos, seg, is_drs) + else: # angled wall; scale the Y to covert to vertical + w_p = Plane(Vector3D(seg.v.y, -seg.v.x, 0), seg.p, seg.v) + w3d = [Face3D([p.project(w_p.n, w_p.o) for p in geo.boundary]) + for geo in w_geos] + window_parameters[i] = DetailedWindows.from_face3ds( + w3d, seg, is_drs) + if isinstance(wall_f.type, AirBoundary): + air_bounds[i] = True + + # determine the ceiling height, and top/bottom boundary conditions + floor_to_ceiling_height = room.geometry.max.z - room.geometry.min.z + is_ground_contact = all([isinstance(f.boundary_condition, Ground) + for f in room.faces if isinstance(f.type, Floor)]) + is_top_exposed = all([isinstance(f.boundary_condition, Outdoors) + for f in room.faces if isinstance(f.type, RoofCeiling)]) + + # create the Dragonfly Room2D + room_2d = cls( + room.identifier, flr_geo, floor_to_ceiling_height, + boundary_conditions, window_parameters, None, + is_ground_contact, is_top_exposed, tolerance) + + # check if there are any skylights to be added + skylights, are_doors = [], [] + for f in room.faces: + if isinstance(f.type, RoofCeiling): + sf_objs = f._apertures + f._doors + for sf in sf_objs: + verts2d = tuple(Point2D(pt.x, pt.y) for pt in sf.geometry.boundary) + skylights.append(Polygon2D(verts2d)) + are_doors.append(isinstance(sf, Door)) + if len(skylights) != 0: + room_2d.skylight_parameters = DetailedSkylights(skylights, are_doors) + + # add the extra optional attributes + final_ab = [] + for v, bc in zip(air_bounds, room_2d._boundary_conditions): + v_f = v if isinstance(bc, Surface) else False + final_ab.append(v_f) + room_2d.air_boundaries = final_ab + room_2d._display_name = room._display_name + room_2d._user_data = None if room.user_data is None else room.user_data.copy() + room_2d.properties.from_honeybee(room.properties) + return room_2d
+ +
[docs] @classmethod + def from_polygon(cls, identifier, polygon, floor_height, floor_to_ceiling_height, + boundary_conditions=None, window_parameters=None, + shading_parameters=None, is_ground_contact=False, + is_top_exposed=False): + """Create a Room2D from a ladybug-geometry Polygon2D and a floor_height. + + Note that this method is not recommended for a Room with one or more holes + (like a courtyard) since polygons cannot have holes within them. + + Args: + identifier: Text string for a unique Room2D ID. Must be < 100 characters + and not contain any spaces or special characters. + polygon: A single Polygon2D object representing the floor plate of the Room. + floor_height: A float value to place the polygon within 3D space. + floor_to_ceiling_height: A number for the height above the floor where the + ceiling begins. Typical values range from 3 to 5 meters. + boundary_conditions: A list of boundary conditions that match the number of + segments in the input floor_geometry. These will be used to assign + boundary conditions to each of the walls of the Room in the resulting + model. If None, all boundary conditions will be Outdoors or Ground + depending on whether ceiling of the room is below 0 (the assumed + ground plane). Default: None. + window_parameters: A list of WindowParameter objects that dictate how the + window geometries will be generated for each of the walls. If None, + no windows will exist over the entire Room2D. Default: None. + shading_parameters: A list of ShadingParameter objects that dictate how the + shade geometries will be generated for each of the walls. If None, + no shades will exist over the entire Room2D. Default: None. + is_ground_contact: A boolean to note whether this Room2D has its floor + in contact with the ground. Default: False. + is_top_exposed: A boolean to note whether this Room2D has its ceiling + exposed to the outdoors. Default: False. + """ + # check the input polygon and ensure it's counter-clockwise + assert isinstance(polygon, Polygon2D), \ + 'Expected ladybug_geometry Polygon2D. Got {}'.format(type(polygon)) + if polygon.is_clockwise: + polygon = polygon.reverse() + if boundary_conditions is not None: + boundary_conditions = list(reversed(boundary_conditions)) + if window_parameters is not None: + new_win_pars = [] + for seg, win_par in zip(polygon.segments, reversed(window_parameters)): + if isinstance(win_par, _AsymmetricBase): + new_win_pars.append(win_par.flip(seg.length)) + else: + new_win_pars.append(win_par) + window_parameters = new_win_pars + if shading_parameters is not None: + shading_parameters = list(reversed(shading_parameters)) + + # build the Face3D without using right-hand rule to ensure alignment w/ bcs + base_plane = Plane(Vector3D(0, 0, 1), Point3D(0, 0, floor_height)) + vert3d = tuple(base_plane.xy_to_xyz(_v) for _v in polygon.vertices) + floor_geometry = Face3D(vert3d, base_plane, enforce_right_hand=False) + + return cls(identifier, floor_geometry, floor_to_ceiling_height, + boundary_conditions, window_parameters, shading_parameters, + is_ground_contact, is_top_exposed)
+ +
[docs] @classmethod + def from_vertices(cls, identifier, vertices, floor_height, floor_to_ceiling_height, + boundary_conditions=None, window_parameters=None, + shading_parameters=None, is_ground_contact=False, + is_top_exposed=False): + """Create a Room2D from 2D vertices with each vertex as an iterable of 2 floats. + + Note that this method is not recommended for a Room with one or more holes + (like a courtyard) since the distinction between hole vertices and boundary + vertices cannot be derived from a single list of vertices. + + Args: + identifier: Text string for a unique Room2D ID. Must be < 100 characters + and not contain any spaces or special characters. + vertices: A flattened list of 2 or more vertices as (x, y) that trace + the outline of the floor plate. + floor_height: A float value to place the polygon within 3D space. + floor_to_ceiling_height: A number for the height above the floor where the + ceiling begins. Typical values range from 3 to 5 meters. + boundary_conditions: A list of boundary conditions that match the number of + segments in the input floor_geometry. These will be used to assign + boundary conditions to each of the walls of the Room in the resulting + model. If None, all boundary conditions will be Outdoors or Ground + depending on whether ceiling of the room is below 0 (the assumed + ground plane). Default: None. + window_parameters: A list of WindowParameter objects that dictate how the + window geometries will be generated for each of the walls. If None, + no windows will exist over the entire Room2D. Default: None. + shading_parameters: A list of ShadingParameter objects that dictate how the + shade geometries will be generated for each of the walls. If None, + no shades will exist over the entire Room2D. Default: None. + is_ground_contact: A boolean to note whether this Room2D has its floor + in contact with the ground. Default: False. + is_top_exposed: A boolean to note whether this Room2D has its ceiling + exposed to the outdoors. Default: False. + """ + polygon = Polygon2D(tuple(Point2D(*v) for v in vertices)) + return cls.from_polygon( + identifier, polygon, floor_height, floor_to_ceiling_height, + boundary_conditions, window_parameters, shading_parameters, + is_ground_contact, is_top_exposed)
+ + @property + def floor_geometry(self): + """A horizontal Face3D object representing the floor plate of the Room.""" + return self._floor_geometry + + @property + def floor_to_ceiling_height(self): + """Get or set a number for the distance between the floor and the ceiling.""" + return self._floor_to_ceiling_height + + @floor_to_ceiling_height.setter + def floor_to_ceiling_height(self, value): + self._floor_to_ceiling_height = float_positive(value, 'floor-to-ceiling height') + assert self._floor_to_ceiling_height != 0, 'Room2D floor-to-ceiling height ' \ + 'cannot be zero.' + + @property + def boundary_conditions(self): + """Get or set a tuple of boundary conditions for the wall boundary conditions.""" + return tuple(self._boundary_conditions) + + @boundary_conditions.setter + def boundary_conditions(self, value): + value = self._check_wall_assigned_object(value, 'boundary conditions') + for val, glz in zip(value, self._window_parameters): + assert val in bcs, 'Expected BoundaryCondition. Got {}'.format(type(value)) + if glz is not None: + assert isinstance(val, (Outdoors, Surface)), \ + '{} cannot be assigned to a wall with windows.'.format(val) + self._boundary_conditions = value + + @property + def window_parameters(self): + """Get or set a tuple of WindowParameters describing how to generate windows. + """ + return tuple(self._window_parameters) + + @window_parameters.setter + def window_parameters(self, value): + if value is not None: + value = self._check_wall_assigned_object(value, 'window_parameters') + for val, bc in zip(value, self._boundary_conditions): + if val is not None: + assert isinstance(val, _WindowParameterBase), \ + 'Expected Window Parameters. Got {}'.format(type(value)) + assert isinstance(bc, (Outdoors, Surface)), \ + '{} cannot be assigned to a wall with windows.'.format(bc) + self._window_parameters = value + else: + self._window_parameters = [None for i in range(len(self))] + + @property + def shading_parameters(self): + """Get or set a tuple of ShadingParameters describing how to generate shades. + """ + return tuple(self._shading_parameters) + + @shading_parameters.setter + def shading_parameters(self, value): + if value is not None: + value = self._check_wall_assigned_object(value, 'shading_parameters') + for val in value: + if val is not None: + assert isinstance(val, _ShadingParameterBase), \ + 'Expected Shading Parameters. Got {}'.format(type(value)) + self._shading_parameters = value + else: + self._shading_parameters = [None for i in range(len(self))] + + @property + def air_boundaries(self): + """Get or set a tuple of booleans for whether each wall has an air boundary type. + + False values indicate a standard opaque type while True values indicate + an AirBoundary type. All walls will be False by default. Note that any + walls with a True air boundary must have a Surface boundary condition + without any windows. + """ + if self._air_boundaries is None: + self._air_boundaries = [False] * len(self) + return tuple(self._air_boundaries) + + @air_boundaries.setter + def air_boundaries(self, value): + if value is not None: + value = self._check_wall_assigned_object(value, 'air boundaries') + value = [bool(val) for val in value] + all_props = zip(value, self._boundary_conditions, self._window_parameters) + for val, bnd, glz in all_props: + if val: + assert isinstance(bnd, Surface), 'Air boundaries must be assigned ' \ + 'to walls with Surface boundary conditions. Not {}.'.format(bnd) + assert glz is None, \ + 'Air boundaries cannot be assigned to a wall with windows.' + self._air_boundaries = value + + @property + def is_ground_contact(self): + """Get or set a boolean noting whether the floor is in contact with the ground. + """ + return self._is_ground_contact + + @is_ground_contact.setter + def is_ground_contact(self, value): + self._is_ground_contact = bool(value) + + @property + def is_top_exposed(self): + """Get or set a boolean noting whether the ceiling is exposed to the outdoors. + """ + return self._is_top_exposed + + @is_top_exposed.setter + def is_top_exposed(self, value): + self._is_top_exposed = bool(value) + + @property + def skylight_parameters(self): + """Get or set SkylightParameters describing how to generate skylights. + """ + return self._skylight_parameters + + @skylight_parameters.setter + def skylight_parameters(self, value): + if value is not None: + assert isinstance(value, _SkylightParameterBase), \ + 'Expected Skylight Parameters. Got {}'.format(type(value)) + self._skylight_parameters = value + + @property + def parent(self): + """Get the parent Story if it is assigned. None if it is not assigned.""" + return self._parent + + @property + def has_parent(self): + """Get a boolean noting whether this Room2D has a parent Story.""" + return self._parent is not None + + @property + def floor_segments(self): + """Get a list of LineSegment3D objects for each wall of the Room.""" + return self._floor_geometry.boundary_segments if self._floor_geometry.holes is \ + None else self._floor_geometry.boundary_segments + \ + tuple(seg for hole in self._floor_geometry.hole_segments for seg in hole) + + @property + def floor_segments_2d(self): + """Get a list of LineSegment2D objects for each wall of the Room.""" + return self._floor_geometry.boundary_polygon2d.segments if \ + self._floor_geometry.holes is None else \ + self._floor_geometry.boundary_polygon2d.segments + \ + tuple(seg for hole in self._floor_geometry.hole_polygon2d + for seg in hole.segments) + + @property + def segment_count(self): + """Get the number of segments making up the floor geometry. + + This is equal to the number of walls making up the Room. + """ + return self._segment_count + + @property + def segment_normals(self): + """Get a list of Vector2D objects for the normal of each segment.""" + return [Vector2D(seg.v.y, -seg.v.x).normalize() for seg in self.floor_segments] + + @property + def floor_height(self): + """Get a number for the height of the floor above the ground.""" + return self._floor_geometry[0].z + + @property + def ceiling_height(self): + """Get a number for the height of the ceiling above the ground.""" + return self.floor_height + self.floor_to_ceiling_height + + @property + def volume(self): + """Get a number for the volume of the Room.""" + return self.floor_area * self.floor_to_ceiling_height + + @property + def floor_area(self): + """Get a number for the floor area of the Room.""" + return self._floor_geometry.area + + @property + def exterior_wall_area(self): + """Get a the total wall area of the Room with an Outdoors boundary condition. + """ + wall_areas = [] + for seg, bc in zip(self.floor_segments, self._boundary_conditions): + if isinstance(bc, Outdoors): + wall_areas.append(seg.length * self.floor_to_ceiling_height) + return sum(wall_areas) + + @property + def interior_wall_area(self): + """Get a the total wall area of the Room without an Outdoors or Ground BC. + """ + wall_areas = [] + for seg, bc in zip(self.floor_segments, self._boundary_conditions): + if not isinstance(bc, (Outdoors, Ground)): + wall_areas.append(seg.length * self.floor_to_ceiling_height) + return sum(wall_areas) + + @property + def exterior_aperture_area(self): + """Get a the total aperture area of the Room with an Outdoors boundary condition. + """ + glz_areas = [] + for seg, bc, glz in zip(self.floor_segments, self._boundary_conditions, + self._window_parameters): + if isinstance(bc, Outdoors) and glz is not None: + area = glz.area_from_segment(seg, self.floor_to_ceiling_height) + glz_areas.append(area) + return sum(glz_areas) + + @property + def is_core(self): + """Get a boolean for whether the Room2D is in the core of a story. + + Core Room2Ds have no walls exposed to the outdoors. + """ + return self.exterior_wall_area == 0 + + @property + def is_perimeter(self): + """Get a boolean for whether the Room2D is on the perimeter of a story. + + Perimeter Room2Ds have walls exposed to the outdoors. + """ + return self.exterior_wall_area != 0 + + @property + def min(self): + """Get a Point2D for the min bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this Room2D is in proximity + to other Room2Ds. + """ + return self._floor_geometry.boundary_polygon2d.min + + @property + def max(self): + """Get a Point2D for the max bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this Room2D is in proximity + to other Room2Ds. + """ + return self._floor_geometry.boundary_polygon2d.max + + @property + def center(self): + """Get a Point2D for the center bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this Room2D is inside + other polygons. + """ + return self._floor_geometry.boundary_polygon2d.center + +
[docs] def label_point(self, tolerance=0.01): + """Get a Point3D to label this Room2D in 3D space. + + This point will always lie within the polygon formed by the floor_geometry + regardless of whether this geometry is concave or has holes. + + Args: + tolerance: The tolerance to which the pole_of_inaccessibility will + be computed in the event that the floor_geometry is concave or + has holes. Note that this does not need to be equal to the Model + tolerance and should usually be larger than the Model tolerance + to avoid long calculation times. (Default: 0.01). + """ + return self.floor_geometry.center if self.floor_geometry.is_convex else \ + self.floor_geometry.pole_of_inaccessibility(tolerance)
+ +
[docs] def segment_orientations(self, north_vector=Vector2D(0, 1)): + """A list of numbers between 0 and 360 for the orientation of the segments. + + 0 = North, 90 = East, 180 = South, 270 = West + + Args: + north_vector: A ladybug_geometry Vector2D for the north direction. + Default is the Y-axis (0, 1). + """ + normals = (Vector2D(sg.v.y, -sg.v.x) for sg in self.floor_segments) + return [math.degrees(north_vector.angle_clockwise(norm)) for norm in normals]
+ +
[docs] def set_outdoor_window_parameters(self, window_parameter): + """Set all of the outdoor walls to have the same window parameters.""" + assert isinstance(window_parameter, _WindowParameterBase), \ + 'Expected Window Parameters. Got {}'.format(type(window_parameter)) + glz_ps = [] + for bc in self._boundary_conditions: + glz_p = window_parameter if isinstance(bc, Outdoors) else None + glz_ps.append(glz_p) + self._window_parameters = glz_ps
+ +
[docs] def set_outdoor_shading_parameters(self, shading_parameter): + """Set all of the outdoor walls to have the same shading parameters.""" + assert isinstance(shading_parameter, _ShadingParameterBase), \ + 'Expected Window Parameters. Got {}'.format(type(shading_parameter)) + shd_ps = [] + for bc in self._boundary_conditions: + shd_p = shading_parameter if isinstance(bc, Outdoors) else None + shd_ps.append(shd_p) + self._shading_parameters = shd_ps
+ +
[docs] def to_rectangular_windows(self): + """Convert all of the windows of the Room2D to the RectangularWindows format.""" + glz_ps = [] + for seg, glz in zip(self.floor_segments, self._window_parameters): + if glz is not None: + glz = glz.to_rectangular_windows(seg, self.floor_to_ceiling_height) + glz_ps.append(glz) + self._window_parameters = glz_ps
+ +
[docs] def to_detailed_windows(self): + """Convert all of the windows of the Room2D to the DetailedWindows format.""" + glz_ps = [] + for seg, glz in zip(self.floor_segments, self._window_parameters): + if glz is not None and not isinstance(glz, DetailedWindows): + glz = glz.to_rectangular_windows(seg, self.floor_to_ceiling_height) + glz = glz.to_detailed_windows() + glz_ps.append(glz) + self._window_parameters = glz_ps
+ +
[docs] def add_prefix(self, prefix): + """Change the identifier of this object by inserting a prefix. + + This is particularly useful in workflows where you duplicate and edit + a starting object and then want to combine it with the original object + into one Model (like making a model of repeated rooms) since all objects + within a Model must have unique identifiers. + + Args: + prefix: Text that will be inserted at the start of this object's + (and child segments') identifier and display_name. It is recommended + that this prefix be short to avoid maxing out the 100 allowable + characters for dragonfly identifiers. + """ + self._identifier = clean_string('{}_{}'.format(prefix, self.identifier)) + if self._display_name is not None: + self.display_name = '{}_{}'.format(prefix, self.display_name) + self.properties.add_prefix(prefix) + for i, bc in enumerate(self._boundary_conditions): + if isinstance(bc, Surface): + new_face_id = '{}_{}'.format(prefix, bc.boundary_condition_objects[0]) + new_room_id = '{}_{}'.format(prefix, bc.boundary_condition_objects[1]) + self._boundary_conditions[i] = \ + Surface((new_face_id, new_room_id))
+ +
[docs] def generate_grid(self, x_dim, y_dim=None, offset=1.0): + """Get a gridded Mesh3D object offset from the floor of this room. + + Note that the x_dim and y_dim refer to dimensions within the XY coordinate + system of the floor Faces's plane. So rotating the planes of the floor geometry + will result in rotated grid cells. + + Args: + x_dim: The x dimension of the grid cells as a number. + y_dim: The y dimension of the grid cells as a number. Default is None, + which will assume the same cell dimension for y as is set for x. + offset: A number for how far to offset the grid from the base face. + Default is 1.0, which will not offset the grid to be 1 unit above + the floor. + """ + return self.floor_geometry.mesh_grid(x_dim, y_dim, offset, False)
+ +
[docs] def set_adjacency( + self, other_room_2d, self_seg_index, other_seg_index, + resolve_window_conflicts=True): + """Set a segment of this Room2D to be adjacent to another and vice versa. + + Note that, adjacent segments must possess matching WindowParameters in + order to be valid. + + Args: + other_room_2d: Another Room2D object to be set adjacent to this one. + self_seg_index: An integer for the wall segment of this Room2D that + will be set adjacent to the other_room_2d. + other_seg_index:An integer for the wall segment of the other_room_2d + that will be set adjacent to this Room2D. + resolve_window_conflicts: Boolean to note whether conflicts between + window parameters of adjacent segments should be resolved during + adjacency setting or an error should be raised about the mismatch. + Resolving conflicts will default to the window parameters with the + larger are and assign them to the other segment. (Default: True). + """ + assert isinstance(other_room_2d, Room2D), \ + 'Expected dragonfly Room2D. Got {}.'.format(type(other_room_2d)) + # set the boundary conditions of the segments + ids_1 = ('{}..Face{}'.format(self.identifier, self_seg_index + 1), + self.identifier) + ids_2 = ('{}..Face{}'.format(other_room_2d.identifier, other_seg_index + 1), + other_room_2d.identifier) + self._boundary_conditions[self_seg_index] = Surface(ids_2) + other_room_2d._boundary_conditions[other_seg_index] = Surface(ids_1) + # check that the window parameters match between segments + wp1 = self._window_parameters[self_seg_index] + wp2 = other_room_2d._window_parameters[other_seg_index] + if wp1 is not None or wp2 is not None: + if wp1 != wp2 or isinstance(wp1, DetailedWindows): + if resolve_window_conflicts: + ftc1 = self.floor_to_ceiling_height + ftc2 = other_room_2d.floor_to_ceiling_height + min_ftc = min((ftc1, ftc2)) + seg1 = self.floor_segments[self_seg_index] + a1 = wp1.area_from_segment(seg1, min_ftc) if wp1 is not None else 0 + seg2 = other_room_2d.floor_segments[other_seg_index] + a2 = wp2.area_from_segment(seg2, min_ftc) if wp2 is not None else 0 + if a1 > a2: + other_room_2d._window_parameters[other_seg_index] = \ + wp1.flip(seg2.length) if isinstance(wp1, _AsymmetricBase) \ + else wp1 + else: + self._window_parameters[self_seg_index] = wp2.flip(seg1.length) \ + if isinstance(wp2, _AsymmetricBase) else wp2 + else: + if wp1 != wp2: + msg = 'Window parameters do not match between adjacent ' \ + 'Rooms "{}" and "{}".'.format( + self.identifier, other_room_2d.identifier) + raise AssertionError(msg)
+ +
[docs] def set_boundary_condition(self, seg_index, boundary_condition): + """Set a single segment of this Room2D to have a certain boundary condition. + + Args: + seg_index: An integer for the wall segment of this Room2D for which + the boundary condition will be set. + boundary_condition: A boundary condition object. + """ + assert boundary_condition in bcs, \ + 'Expected boundary condition. Got {}.'.format(type(boundary_condition)) + if self._window_parameters[seg_index] is not None: + assert isinstance(boundary_condition, (Outdoors, Surface)), '{} cannot be ' \ + 'assigned to a wall with windows.'.format(boundary_condition) + self._boundary_conditions[seg_index] = boundary_condition
+ +
[docs] def set_air_boundary(self, seg_index): + """Set a single segment of this Room2D to have an air boundary type. + + Args: + seg_index: An integer for the wall segment of this Room2D for which + the boundary condition will be set. + """ + self.air_boundaries # trigger generation of values if they don't exist + assert self._window_parameters[seg_index] is None, \ + 'Air boundaries cannot be assigned to a wall with windows.' + assert isinstance(self._boundary_conditions[seg_index], Surface), \ + 'Air boundaries must be assigned to walls with Surface boundary conditions.' + self._air_boundaries[seg_index] = True
+ +
[docs] def set_window_parameter(self, seg_index, window_parameter=None): + """Set a single segment of this Room2D to have a certain window parameter. + + Args: + seg_index: An integer for the wall segment of this Room2D for which + the window parameter will be set. + window_parameter: A window parameter object to be assigned to the segment. + If None, any existing WindowParameters assigned to the segment + will be removed. (Default: None). + """ + if window_parameter is not None: + assert isinstance(window_parameter, _WindowParameterBase), \ + 'Expected Window Parameters. Got {}'.format(type(window_parameter)) + accept_bc = (Outdoors, Surface) + assert isinstance(self._boundary_conditions[seg_index], accept_bc), \ + 'Windows cannot be assigned to a wall with {} boundary ' \ + 'condition.'.format(self._boundary_conditions[seg_index]) + self._window_parameters[seg_index] = window_parameter
+ +
[docs] def offset_skylight_parameters(self, offset_distance=0.05, tolerance=0.01): + """Offset detailed skylights so all vertices are inside the Room2D. + + Args: + offset_distance: Distance from the edge of the room 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). + """ + if isinstance(self._skylight_parameters, DetailedSkylights): + self._skylight_parameters.offset_polygons_for_face( + self.floor_geometry, offset_distance, tolerance)
+ +
[docs] def reset_adjacency(self): + """Set all Surface boundary conditions of this Room2D to be Outdoors.""" + for i, bc in enumerate(self._boundary_conditions): + if isinstance(bc, Surface): + self._boundary_conditions[i] = bcs.outdoors
+ +
[docs] def move(self, moving_vec): + """Move this Room2D along a vector. + + Args: + moving_vec: A ladybug_geometry Vector3D with the direction and distance + to move the room. + """ + self._floor_geometry = self._floor_geometry.move(moving_vec) + if isinstance(self._skylight_parameters, DetailedSkylights): + self._skylight_parameters = self._skylight_parameters.move(moving_vec) + self.properties.move(moving_vec)
+ +
[docs] def rotate_xy(self, angle, origin): + """Rotate this Room2D counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle in degrees. + origin: A ladybug_geometry Point3D for the origin around which the + object will be rotated. + """ + self._floor_geometry = self._floor_geometry.rotate_xy( + math.radians(angle), origin) + if isinstance(self._skylight_parameters, DetailedSkylights): + self._skylight_parameters = self._skylight_parameters.rotate(angle, origin) + self.properties.rotate_xy(angle, origin)
+ +
[docs] def reflect(self, plane): + """Reflect this Room2D across a plane. + + Args: + plane: A ladybug_geometry Plane across which the object will be reflected. + """ + assert plane.n.z == 0, \ + 'Plane normal must be in XY plane to use it on Room2D.reflect.' + self._floor_geometry = self._floor_geometry.reflect(plane.n, plane.o) + if self._floor_geometry.normal.z < 0: # ensure upward-facing Face3D + new_bcs, new_win_pars, new_shd_pars = Room2D._flip_wall_assigned_objects( + self._floor_geometry, self._boundary_conditions, + self._window_parameters, self._shading_parameters) + self._boundary_conditions = new_bcs + self._window_parameters = new_win_pars + self._shading_parameters = new_shd_pars + self._floor_geometry = self._floor_geometry.flip() + if isinstance(self._skylight_parameters, DetailedSkylights): + self._skylight_parameters = self._skylight_parameters.reflect(plane) + self.properties.reflect(plane)
+ +
[docs] def scale(self, factor, origin=None): + """Scale this Room2D by a factor from an origin point. + + Note that this will scale both the Room2D geometry and the WindowParameters + and FacadeParameters assigned to this 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). + """ + # scale the Room2D geometry + self._floor_geometry = self._floor_geometry.scale(factor, origin) + self._floor_to_ceiling_height = self._floor_to_ceiling_height * factor + + # scale the window parameters + for i, win_par in enumerate(self._window_parameters): + if win_par is not None: + self._window_parameters[i] = win_par.scale(factor) + + # scale the shading parameters + for i, shd_par in enumerate(self._shading_parameters): + if shd_par is not None: + self._shading_parameters[i] = shd_par.scale(factor) + + # scale the skylight parameters + if self._skylight_parameters is not None: + self._skylight_parameters = self._skylight_parameters.scale(factor, origin) \ + if isinstance(self._skylight_parameters, DetailedSkylights) else \ + self._skylight_parameters.scale(factor) + + self.properties.scale(factor, origin)
+ +
[docs] def snap_to_grid(self, grid_increment): + """Snap this Room2D's vertices to the nearest grid node defined by an increment. + + All properties assigned to the Room2D will be preserved and the number of + vertices will remain constant. This means that this method can often create + duplicate vertices and it might be desirable to run the remove_duplicate_vertices + method after running this one. + + Args: + grid_increment: A positive number for dimension of each grid cell. This + typically should be equal to the tolerance or larger but should + not be larger than the smallest detail of the Room2D that you + wish to resolve. + """ + # loop through the vertices and snap them + new_boundary, new_holes = [], None + for pt in self._floor_geometry.boundary: + new_x = grid_increment * round(pt.x / grid_increment) + new_y = grid_increment * round(pt.y / grid_increment) + new_boundary.append(Point3D(new_x, new_y, pt.z)) + if self._floor_geometry.holes is not None: + new_holes = [] + for hole in self._floor_geometry.holes: + new_hole = [] + for pt in hole: + new_x = grid_increment * round(pt.x / grid_increment) + new_y = grid_increment * round(pt.y / grid_increment) + new_hole.append(Point3D(new_x, new_y, pt.z)) + new_holes.append(new_hole) + + # rebuild the new floor geometry and assign it to the Room2D + self._floor_geometry = Face3D( + new_boundary, self._floor_geometry.plane, new_holes)
+ +
[docs] def align(self, line_ray, distance): + """Move any Room2D vertices within a given distance of a line to be on that line. + + This is useful to clean up cases where wall segments have a lot of + zig zags in them. + + All properties assigned to the Room2D will be preserved and the number of + vertices will remain constant. This means that this method can often create + duplicate vertices and it might be desirable to run the remove_duplicate_vertices + method after running this one. + + Args: + line_ray: A ladybug_geometry Ray2D or LineSegment2D to which the Room2D + vertices will be aligned. Ray2Ds will be interpreted as being infinite + in both directions while LineSegment2Ds will be interpreted as only + existing between two points. + distance: The maximum distance between a vertex and the line_ray where + the vertex will be moved to lie on the line_ray. Vertices beyond + this distance will be left as they are. + """ + # create a 3D version of the line_ray for the closest point calculation + if isinstance(line_ray, Ray2D): + line_ray_3d = Ray3D( + Point3D(line_ray.p.x, line_ray.p.y, self.floor_height), + Vector3D(line_ray.v.x, line_ray.v.y, 0) + ) + closest_func = closest_point3d_on_line3d_infinite + elif isinstance(line_ray, LineSegment2D): + line_ray_3d = LineSegment3D( + Point3D(line_ray.p.x, line_ray.p.y, self.floor_height), + Vector3D(line_ray.v.x, line_ray.v.y, 0) + ) + closest_func = closest_point3d_on_line3d + else: + msg = 'Expected Ray2D or LineSegment2D. Got {}.'.format(type(line_ray)) + raise TypeError(msg) + + # loop through the vertices and align them + new_boundary, new_holes = [], None + for pt in self._floor_geometry.boundary: + close_pt = closest_func(pt, line_ray_3d) + if pt.distance_to_point(close_pt) <= distance: + new_boundary.append(close_pt) + else: + new_boundary.append(pt) + if self._floor_geometry.holes is not None: + new_holes = [] + for hole in self._floor_geometry.holes: + new_hole = [] + for pt in hole: + close_pt = closest_func(pt, line_ray_3d) + if pt.distance_to_point(close_pt) <= distance: + new_hole.append(close_pt) + else: + new_hole.append(pt) + new_holes.append(new_hole) + + # rebuild the new floor geometry and assign it to the Room2D + self._floor_geometry = Face3D( + new_boundary, self._floor_geometry.plane, new_holes)
+ +
[docs] def snap_to_line_end_points(self, line, distance): + """Snap this Room2D's vertices to the endpoints of a line segment. + + All properties assigned to this Room2D will be preserved and the number of + vertices will remain constant. This means that this method can often create + duplicate vertices and it might be desirable to run the remove_duplicate_vertices + method after running this one. + + Args: + line: A ladybug_geometry LineSegment2D to which the Room2D + vertices will be snapped if they are near the end points. + distance: The maximum distance between a Room2D vertex and the polyline where + the vertex will be moved to lie on the polyline. Vertices beyond + this distance will be left as they are. + """ + # create a 3D version of the line segment + if isinstance(line, LineSegment2D): + line_ray_3d = LineSegment3D( + Point3D(line.p.x, line.p.y, self.floor_height), + Vector3D(line.v.x, line.v.y, 0) + ) + else: + msg = 'Expected LineSegment2D. Got {}.'.format(type(line)) + raise TypeError(msg) + + # get lists of vertices for the Room2D.floor_geometry to be edited + edit_boundary = self._floor_geometry.boundary + edit_holes = self._floor_geometry.holes \ + if self._floor_geometry.has_holes else None + + # perform the snapping operation to snap them + vertices = line_ray_3d.endpoints + new_boundary, new_holes = [], None + for pt in edit_boundary: + dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices] + sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0]) + if sort_pt[0][0] <= distance: + new_boundary.append(sort_pt[0][1]) + else: + new_boundary.append(pt) + if edit_holes is not None: + new_holes = [] + for hole in edit_holes: + new_hole = [] + for pt in hole: + dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices] + sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0]) + if sort_pt[0][0] <= distance: + new_hole.append(sort_pt[0][1]) + else: + new_hole.append(pt) + new_holes.append(new_hole) + + # rebuild the new floor geometry and assign it to the Room2D + self._floor_geometry = Face3D( + new_boundary, self._floor_geometry.plane, new_holes)
+ +
[docs] def pull_to_segments(self, line_segments, distance, snap_vertices=True): + """Pull this Room2D's vertices to several LineSegment2D. + + This includes both an alignment to the line segments as well as an optional + snapping to the line end points. + + The benefit of calling this method as opposed to iterating over the + segments and calling align (and snap_to_line_end_points) is that this + method will only align (and snap) to the closest segment across all of + the input line_segments. This often helps avoid snapping to undesirable + line segments, particularly when there are two ore more segments that + are within the distance. + + Args: + line_segments: A list of ladybug_geometry LineSegment2D to which this + Room2D's vertices will be pulled. + distance: The maximum distance between a Room2D vertex and the line_segments + where the vertex will be moved to lie on the segments. Vertices beyond + this distance will be left as they are. + snap_vertices: A boolean to note whether Room2D vertices that are + close to the segment end points within the distance should be snapped + to the end point instead of simply being aligned to the nearest + segment. (Default: True). + """ + # create a 3D version of the relevant line segments + lines_3d = [] + for line in line_segments: + if isinstance(line, LineSegment2D): + if line.length > distance: + line_3d = LineSegment3D( + Point3D(line.p.x, line.p.y, self.floor_height), + Vector3D(line.v.x, line.v.y, 0) + ) + lines_3d.append(line_3d) + else: + msg = 'Expected LineSegment2D. Got {}.'.format(type(line)) + raise TypeError(msg) + + # get lists of vertices for the Room2D.floor_geometry to be edited + edit_boundary = self._floor_geometry.boundary + edit_holes = self._floor_geometry.holes \ + if self._floor_geometry.has_holes else None + + # loop through the Room2D vertices and align them to the segments + new_boundary = [] + for pt in edit_boundary: + dists, c_pts = [], [] + for line_ray_3d in lines_3d: + close_pt = closest_point3d_on_line3d(pt, line_ray_3d) + c_pts.append(close_pt) + dists.append(pt.distance_to_point(close_pt)) + sort_pt = sorted(zip(dists, c_pts), key=lambda pair: pair[0]) + if sort_pt[0][0] <= distance: + new_boundary.append(sort_pt[0][1]) + else: + new_boundary.append(pt) + edit_boundary = new_boundary + if edit_holes is not None: + new_holes = [] + for hole in edit_holes: + new_hole = [] + for pt in hole: + dists, c_pts = [], [] + for line_ray_3d in lines_3d: + close_pt = closest_point3d_on_line3d(pt, line_ray_3d) + c_pts.append(close_pt) + dists.append(pt.distance_to_point(close_pt)) + sort_pt = sorted(zip(dists, c_pts), key=lambda pair: pair[0]) + if sort_pt[0][0] <= distance: + new_hole.append(sort_pt[0][1]) + else: + new_hole.append(pt) + new_holes.append(new_hole) + edit_holes = new_holes + + # if snap_vertices was requested, perform an additional operation to snap them + if snap_vertices: + vertices = [] + for line in lines_3d: + vertices.append(line.p1) + vertices.append(line.p2) + new_boundary = [] + for pt in edit_boundary: + dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices] + sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0]) + if sort_pt[0][0] <= distance: + new_boundary.append(sort_pt[0][1]) + else: + new_boundary.append(pt) + edit_boundary = new_boundary + if edit_holes is not None: + new_holes = [] + for hole in edit_holes: + new_hole = [] + for pt in hole: + dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices] + sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0]) + if sort_pt[0][0] <= distance: + new_hole.append(sort_pt[0][1]) + else: + new_hole.append(pt) + new_holes.append(new_hole) + edit_holes = new_holes + + # rebuild the new floor geometry and assign it to the Room2D + self._floor_geometry = Face3D( + edit_boundary, self._floor_geometry.plane, edit_holes)
+ +
[docs] def pull_to_polyline(self, polyline, distance, snap_vertices=True): + """Pull this Room2D's vertices to a Polyline2D. + + This includes both an alignment to the polyline's segments as well as an + optional snapping to the polyline's vertices. + + All properties assigned to this Room2D will be preserved and the number of + vertices will remain constant. This means that this method can often create + duplicate vertices and it might be desirable to run the remove_duplicate_vertices + method after running this one. + + Args: + polyline: A ladybug_geometry Polyline2D to which this Room2D's vertices + will be pulled. + distance: The maximum distance between a Room2D vertex and the polyline where + the vertex will be moved to lie on the polyline. Vertices beyond + this distance will be left as they are. + snap_vertices: A boolean to note whether Room2D vertices that are + close to the polyline vertices within the distance should be snapped + to the polyline vertex instead of simply being aligned to the nearest + polyline segment. (Default: True). + """ + # create LineSegment3Ds from the polyline + line_segs = [] + for seg in polyline.segments: + pt_3d = Point3D(seg.p.x, seg.p.y, self.floor_height) + line_ray_3d = LineSegment3D(pt_3d, Vector3D(seg.v.x, seg.v.y, 0)) + line_segs.append(line_ray_3d) + line_segs.append(line_segs[0].flip()) # ensure last vertex is counted + + # pull this Room2D to the segments + self._pull_to_poly_segments(line_segs, distance, snap_vertices)
+ +
[docs] def pull_to_polygon(self, polygon, distance, snap_vertices=True): + """Pull this Room2D's vertices to a Polygon2D. + + This includes both an alignment to the polygon's segments as well as an + optional snapping to the polygon's vertices. + + All properties assigned to this Room2D will be preserved and the number of + vertices will remain constant. This means that this method can often create + duplicate vertices and it might be desirable to run the remove_duplicate_vertices + method after running this one. + + Args: + polygon: A ladybug_geometry Polygon2D to which this Room2D's vertices + will be pulled. + distance: The maximum distance between a Room2D vertex and the polygon where + the vertex will be moved to lie on the polygon. Vertices beyond + this distance will be left as they are. + snap_vertices: A boolean to note whether Room2D vertices that are + close to the polygon vertices within the distance should be snapped + to the polygon vertex instead of simply being aligned to the nearest + polygon segment. (Default: True). + """ + # create LineSegment3Ds from the polygon + line_segs = [] + for seg in polygon.segments: + pt_3d = Point3D(seg.p.x, seg.p.y, self.floor_height) + line_ray_3d = LineSegment3D(pt_3d, Vector3D(seg.v.x, seg.v.y, 0)) + line_segs.append(line_ray_3d) + + # pull this Room2D to the segments + self._pull_to_poly_segments(line_segs, distance, snap_vertices)
+ +
[docs] def pull_to_room_2d(self, room_2d, distance, snap_vertices=True): + """Pull this Room2D's vertices to another Room2D. + + This includes both an alignment to the other Room2D's segments as well + as an optional snapping to the Room2D's vertices. + + All properties assigned to this Room2D will be preserved and the number of + vertices will remain constant. This means that this method can often create + duplicate vertices and it might be desirable to run the remove_duplicate_vertices + method after running this one. + + Args: + room_2d: A Room2D to which this Room2D's vertices will be pulled. + distance: The maximum distance between a Room2D vertex and the other + Room2D where the vertex will be moved to lie on the other Room2D. + Vertices beyond this distance will be left as they are. + snap_vertices: A boolean to note whether Room2D vertices that are + close to the other Room2D vertices within the distance should be snapped + to the Room2D vertex instead of simply being aligned to the nearest + Room2D segment. (Default: True). + """ + # convert the other Room2D to a list of polygons + f_geo = room_2d.floor_geometry + other_room_polys = [Polygon2D([Point2D(pt.x, pt.y) for pt in f_geo.boundary])] + if f_geo.has_holes: + for hole in f_geo.holes: + h_poly = Polygon2D([Point2D(pt.x, pt.y) for pt in hole]) + other_room_polys.append(h_poly) + # pull this Room2D to each of the polygons + for o_poly in other_room_polys: + self.pull_to_polygon(o_poly, distance, snap_vertices)
+ + def _pull_to_poly_segments(self, line_segments, distance, snap_vertices=True): + """Pull this Room2D's vertices to LineSegment3D originating from a poly-line/gon. + + Args: + line_segments: A list of ladybug_geometry LineSegment3D with Z-values at + this Room2D's floor_height to which this Room2D's vertices + will be pulled. + distance: The maximum distance between a Room2D vertex and the line_segments + where the vertex will be moved to lie on the segments. Vertices beyond + this distance will be left as they are. + snap_vertices: A boolean to note whether Room2D vertices that are + close to the segment end points within the distance should be snapped + to the end point instead of simply being aligned to the nearest + segment. (Default: True). + """ + # get lists of vertices for the Room2D.floor_geometry to be edited + edit_boundary = self._floor_geometry.boundary + edit_holes = self._floor_geometry.holes \ + if self._floor_geometry.has_holes else None + + # loop through the Room2D vertices and align them to the segments + new_boundary = [] + for pt in edit_boundary: + dists, c_pts = [], [] + for line_ray_3d in line_segments: + close_pt = closest_point3d_on_line3d(pt, line_ray_3d) + c_pts.append(close_pt) + dists.append(pt.distance_to_point(close_pt)) + sort_pt = sorted(zip(dists, c_pts), key=lambda pair: pair[0]) + if sort_pt[0][0] <= distance: + new_boundary.append(sort_pt[0][1]) + else: + new_boundary.append(pt) + edit_boundary = new_boundary + if edit_holes is not None: + new_holes = [] + for hole in edit_holes: + new_hole = [] + for pt in hole: + dists, c_pts = [], [] + for line_ray_3d in line_segments: + close_pt = closest_point3d_on_line3d(pt, line_ray_3d) + c_pts.append(close_pt) + dists.append(pt.distance_to_point(close_pt)) + sort_pt = sorted(zip(dists, c_pts), key=lambda pair: pair[0]) + if sort_pt[0][0] <= distance: + new_hole.append(sort_pt[0][1]) + else: + new_hole.append(pt) + new_holes.append(new_hole) + edit_holes = new_holes + + # if snap_vertices was requested, perform an additional operation to snap them + if snap_vertices: + vertices = [line.p for line in line_segments] + new_boundary = [] + for pt in edit_boundary: + dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices] + sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0]) + if sort_pt[0][0] <= distance: + new_boundary.append(sort_pt[0][1]) + else: + new_boundary.append(pt) + edit_boundary = new_boundary + if edit_holes is not None: + new_holes = [] + for hole in edit_holes: + new_hole = [] + for pt in hole: + dists = [pt.distance_to_point(pt_3d) for pt_3d in vertices] + sort_pt = sorted(zip(dists, vertices), key=lambda pair: pair[0]) + if sort_pt[0][0] <= distance: + new_hole.append(sort_pt[0][1]) + else: + new_hole.append(pt) + new_holes.append(new_hole) + edit_holes = new_holes + + # rebuild the new floor geometry and assign it to the Room2D + self._floor_geometry = Face3D( + edit_boundary, self._floor_geometry.plane, edit_holes) + +
[docs] def coordinate_room_2d_vertices(self, room_2d, distance, tolerance=0.01): + """Insert vertices to this Room2D to coordinate this Room2D with another Room2D. + + This is sometimes a useful operation to run after using the pull_to_room_2d + method in order to address the case that the Room2D to which this one was + pulled has more vertices along the adjacency boundary than this Room2D. + In this case, the adjacency between the two Room2Ds will not be clean and + extra vertices must be inserted into this Room2D so that geometry matches + along the room adjacency. + + Any vertices of the neighboring input room_2d that are within the specified + distance but cannot be matched to a vertex on this Room2D within the tolerance + will be inserted into this Room2D, splitting the wall segment in the process. + + Args: + room_2d: A Room2D with which the vertices of this Room2D will be coordinated. + distance: The maximum distance between a Room2D vertex and the other + Room2D where the vertex will be moved to lie on the other Room2D. + Vertices beyond this distance will be left as they are. + tolerance: The minimum difference between the coordinate values at + which they are considered co-located. (Default: 0.01, + suitable for objects in meters). + """ + # determine all of the vertices of the other Room2D that should be inserted + self_segs = list(self.floor_segments_2d) + self_pts_2d = [seg.p for seg in self_segs] + other_pts_2d = [seg.p for seg in room_2d.floor_segments_2d] + insert_pts = [] + for o_pt in other_pts_2d: + possible_insert = False + for i, seg in enumerate(self_segs): + if seg.distance_to_point(o_pt) < distance: + possible_insert = True + break + if possible_insert: + for s_pt in self_pts_2d: + if s_pt.distance_to_point(o_pt) <= tolerance: + break + else: + insert_pts.append((i, o_pt)) + + # loop through the segments and split them if insertion points were found + if len(insert_pts) == 0: + return + sort_int_pts = sorted(insert_pts, key=lambda x: x[0], reverse=True) + edit_code = ['K'] * len(self_segs) + for ins_ind, pt in sort_int_pts: + split_seg = self_segs[ins_ind] + new_seg1 = LineSegment2D.from_end_points(split_seg.p1, pt) + new_seg2 = LineSegment2D.from_end_points(pt, split_seg.p2) + self_segs[ins_ind] = new_seg2 + self_segs.insert(ins_ind, new_seg1) + edit_code.insert(ins_ind, 'A') + + # create a new floor_geometry Face3D and update the geometry with the edit code + z_val = self.floor_geometry.boundary[0].z + if not self.floor_geometry.has_holes: + pts = [Point3D(seg.p.x, seg.p.y, z_val) for seg in self_segs] + new_geo = Face3D(pts, self.floor_geometry.plane) + else: + joined_segs = Polyline2D.join_segments(self_segs, tolerance) + new_loops = [] + for p_line in joined_segs: + pts = [Point3D(pt.x, pt.y, z_val) for pt in p_line.vertices[:-1]] + new_loops.append(pts) + new_geo = Face3D(new_loops[0], self.floor_geometry.plane, new_loops[1:]) + self.update_floor_geometry(new_geo, edit_code, tolerance)
+ +
[docs] def remove_duplicate_vertices(self, tolerance=0.01): + """Remove duplicate vertices from this Room2D. + + All properties assigned to the Room2D will be preserved. + + Args: + tolerance: The minimum distance between a vertex and the line it lies + upon at which point the vertex is considered colinear. (Default: 0.01, + suitable for objects in meters). + + Returns: + A list of integers for the indices of segments that have been removed. + """ + # loop through the vertices and remove any duplicates + exist_abs = self.air_boundaries + new_bound, new_bcs, new_win, new_shd, new_abs = [], [], [], [], [] + b_pts = self.floor_geometry.boundary + b_pts = b_pts[1:] + (b_pts[0],) + removed_indices = [] + for i, vert in enumerate(b_pts): + if not vert.is_equivalent(b_pts[i - 1], tolerance): + new_bound.append(b_pts[i - 1]) + new_bcs.append(self._boundary_conditions[i]) + new_win.append(self._window_parameters[i]) + new_shd.append(self._shading_parameters[i]) + new_abs.append(exist_abs[i]) + else: + removed_indices.append(i) + new_holes = None + if self.floor_geometry.has_holes: + new_holes, seg_count = [], len(b_pts) + for hole in self.floor_geometry.holes: + new_h_pts = [] + h_pts = hole[1:] + (hole[0],) + for i, vert in enumerate(h_pts): + if not vert.is_equivalent(h_pts[i - 1], tolerance): + new_h_pts.append(h_pts[i - 1]) + new_bcs.append(self._boundary_conditions[seg_count + i]) + new_win.append(self._window_parameters[seg_count + i]) + new_shd.append(self._shading_parameters[seg_count + i]) + new_abs.append(exist_abs[i]) + else: + removed_indices.append(i) + new_holes.append(new_h_pts) + seg_count += len(h_pts) + + # assign the geometry and properties + try: + self._floor_geometry = Face3D( + new_bound, self.floor_geometry.plane, new_holes) + except AssertionError as e: # usually a sliver face of some kind + raise ValueError( + 'Room2D "{}" is degenerate with dimensions less than the ' + 'tolerance.\n{}'.format(self.display_name, e)) + self._segment_count = len(new_bcs) + self._boundary_conditions = new_bcs + self._window_parameters = new_win + self._shading_parameters = new_shd + self._air_boundaries = new_abs + return removed_indices
+ +
[docs] def remove_degenerate_holes(self, tolerance=0.01): + """Remove any holes in this Room2D with an area that evaluates to zero. + + All properties assigned to the Room2D will be preserved. + + Args: + tolerance: The minimum difference between the coordinate values at + which they are considered co-located. (Default: 0.01, + suitable for objects in meters). + """ + if self.floor_geometry.has_holes: # first identify any zero-area holes + holes_to_remove = [] + for i, hole in enumerate(self.floor_geometry.holes): + tf = Face3D(hole, self.floor_geometry.plane) + max_dim = max((tf.max.x - tf.min.x, tf.max.y - tf.min.y)) + if tf.area < max_dim * tolerance: + holes_to_remove.append(i) + # if zero-area holes were found, rebuild the Room2D + if len(holes_to_remove) > 0: + self._remove_holes(holes_to_remove)
+ +
[docs] def remove_small_holes(self, area_threshold): + """Remove any holes in this Room2D that are below a certain area threshold. + + All properties assigned to the Room2D will be preserved. + + Args: + area_threshold: A number for the area below which holes will be removed. + """ + if self.floor_geometry.has_holes: # first identify any holes to remove + holes_to_remove = [] + for i, hole in enumerate(self.floor_geometry.holes): + tf = Face3D(hole, self.floor_geometry.plane) + if tf.area < area_threshold: + holes_to_remove.append(i) + # if removable holes were found, rebuild the Room2D + if len(holes_to_remove) > 0: + self._remove_holes(holes_to_remove)
+ + def _remove_holes(self, holes_to_remove): + """Remove holes in the Room2D given the indices of the holes. + + Args: + holes_to_remove: A list of integers for the indices of holes to be removed. + """ + # first collect the properties of the boundary + exist_abs = self.air_boundaries + new_bcs, new_win, new_shd, new_abs = [], [], [], [] + seg_count = len(self.floor_geometry.boundary) + for i in range(seg_count): + new_bcs.append(self._boundary_conditions[i]) + new_win.append(self._window_parameters[i]) + new_shd.append(self._shading_parameters[i]) + new_abs.append(exist_abs[i]) + # collect the properties of the new holes + new_holes = [] + for hi, hole in enumerate(self.floor_geometry.holes): + if hi not in holes_to_remove: + for i, vert in enumerate(hole): + new_bcs.append(self._boundary_conditions[seg_count + i]) + new_win.append(self._window_parameters[seg_count + i]) + new_shd.append(self._shading_parameters[seg_count + i]) + new_abs.append(exist_abs[i]) + new_holes.append(hole) + seg_count += len(hole) + # reset the properties of the Room2D + self._floor_geometry = Face3D( + self.floor_geometry.boundary, self.floor_geometry.plane, new_holes) + self._segment_count = len(new_bcs) + self._boundary_conditions = new_bcs + self._window_parameters = new_win + self._shading_parameters = new_shd + self._air_boundaries = new_abs + +
[docs] def update_floor_geometry(self, new_floor_geometry, edit_code, tolerance=0.01): + """Change the floor_geometry of the Room2D with segment-altering specifications. + + This method is intended to be used when the floor geometry has been edited + by some external means and this Room2D should be updated for coordination. + + The method tries to infer whether an removed floor segment means that an + original segment has been merged into another or removed completely using + the colinearity of the original segments. A removed segment that is colinear + with its neighbor will be merged into it while a removed segment that was + not colinear will simply be deleted. Similarly, the method will infer if + an added segment indicates a split in an original segment using colinearity. + When the result in the new_floor_geometry is two colinear segments, + properties of the original segment will be split across the new segments. + Otherwise the new segment will receive default properties. + + Args: + new_floor_geometry: A Face3D for the new floor_geometry of this Room2D. + Note that this method expects the plane of this Face3D to match + the original floor_geometry Face3D and for the counter-clockwise + vertex ordering of the segments to be the same as the original + floor geometry (though segments can obviously be added or removed). + edit_code: A text string that indicates the operations that were + performed on the original floor_geometry segments to yield the + new_floor_geometry. The following letters are used in this code + to indicate the following: + + * K = a segment that has been kept (possibly moved but not removed) + * X = a segment that has been removed + * A = a segment that has been added + + For example, KXKAKKA means that the first segment was kept, the + next removed, the next kept, the next added, followed by two kept + segments and ending in an added segment. + tolerance: The minimum difference between the coordinate values at + which they are considered co-located, used to determine + colinearity. Default: 0.01, suitable for objects in meters. + """ + # process the new floor geometry so that it abides by Room2D rules + if new_floor_geometry.normal.z <= 0: # ensure upward-facing Face3D + new_floor_geometry = new_floor_geometry.flip() + o_pl = Plane(Vector3D(0, 0, 1), Point3D(0, 0, new_floor_geometry.plane.o.z)) + new_floor_geometry = Face3D(new_floor_geometry.boundary, o_pl, + new_floor_geometry.holes) + + # get the original and the new floor segments + orig_segs = self.floor_segments + new_segs = new_floor_geometry.boundary_segments if new_floor_geometry.holes is \ + None else new_floor_geometry.boundary_segments + \ + tuple(seg for hole in new_floor_geometry.hole_segments for seg in hole) + + # figure out the new properties based on the edit code + new_bcs, new_win, new_shd = [], [], [] + last_o_seg = orig_segs[-1] + orig_i, new_i = 0, 0 + for edit_val in edit_code: + if edit_val == 'K': + new_bcs.append(self._boundary_conditions[orig_i]) + new_win.append(self._window_parameters[orig_i]) + new_shd.append(self._shading_parameters[orig_i]) + last_o_seg = orig_segs[orig_i] + orig_i += 1 + new_i += 1 + elif edit_val == 'X': + # determine if the removed segment is colinear + del_seg = orig_segs[orig_i] + full_line = LineSegment3D.from_end_points(last_o_seg.p1, del_seg.p2) + if full_line.distance_to_point(del_seg.p1) <= tolerance: # colinear! + if len(new_bcs) != 0: + # TODO: figure out a strategy to merge first to end of the list + new_bcs[-1] = bcs.outdoors + new_win[-1] = DetailedWindows.merge( + (new_win[-1], self._window_parameters[orig_i]), + (last_o_seg, del_seg), self.floor_to_ceiling_height) + last_o_seg = full_line + orig_i += 1 + elif edit_val == 'A': + # determine if the added segment is colinear and within the original + add_seg = new_segs[new_i] + if last_o_seg.distance_to_point(add_seg.p1) <= tolerance and \ + last_o_seg.distance_to_point(add_seg.p2) <= tolerance: + # colinear! + new_bcs.append(self._boundary_conditions[orig_i]) + if len(new_win) != 0 and new_win[-1] is not None: + # TODO: figure out a strategy to split the end of the list + p_lin = LineSegment3D.from_end_points(last_o_seg.p1, add_seg.p1) + a_lin = LineSegment3D.from_end_points(add_seg.p1, last_o_seg.p2) + w_to_spl = new_win.pop(-1) + new_win.extend(w_to_spl.split((p_lin, a_lin), tolerance)) + last_o_seg = a_lin + else: + new_win.append(None) + new_shd.append(self._shading_parameters[orig_i]) + else: # not colinear; use default properties + new_bcs.append(bcs.outdoors) + new_win.append(None) + new_shd.append(None) + new_i += 1 + + # assign the updated properties to this Room2D + self._floor_geometry = new_floor_geometry + self._segment_count = len(new_segs) + assert self._segment_count == len(new_bcs), 'The operations in the edit_code ' \ + 'denote a geometry with {} segments but the new_floor_geometry has {} ' \ + 'segments.'.format(len(new_bcs), self._segment_count) + self._boundary_conditions = new_bcs + self._window_parameters = new_win + self._shading_parameters = new_shd + self._air_boundaries = None # reset to avoid any conflicts
+ +
[docs] def remove_colinear_vertices(self, tolerance=0.01, preserve_wall_props=True): + """Get a version of this Room2D without colinear or duplicate vertices. + + Args: + tolerance: The minimum distance between a vertex and the line it lies + upon at which point the vertex is considered colinear. Default: 0.01, + suitable for objects in meters. + preserve_wall_props: Boolean to note whether existing window parameters + and Ground boundary conditions should be preserved as vertices are + removed. If False, all boundary conditions are replaced with Outdoors, + all window parameters are erased, and this method will execute quickly. + If True, an attempt will be made to merge window parameters together + across colinear segments, translating simple window parameters to + rectangular ones if necessary. Also, existing Ground boundary + conditions will be kept. (Default: True). + + Returns: + A new Room2D derived from this one with its colinear vertices removed. + """ + if not preserve_wall_props: + try: # remove colinear vertices from the Room2D + new_geo = self.floor_geometry.remove_colinear_vertices(tolerance) + except AssertionError as e: # usually a sliver face of some kind + raise ValueError( + 'Room2D "{}" is degenerate with dimensions less than the ' + 'tolerance.\n{}'.format(self.display_name, e)) + rebuilt_room = Room2D( + self.identifier, new_geo, self.floor_to_ceiling_height, + is_ground_contact=self.is_ground_contact, + is_top_exposed=self.is_top_exposed) + else: + ftc_height = self.floor_to_ceiling_height + if not self.floor_geometry.has_holes: # only need to evaluate one list + pts_3d = self.floor_geometry.vertices + pts_2d = self.floor_geometry.polygon2d + segs_2d = pts_2d.segments + bound_cds = self.boundary_conditions + win_pars = self.window_parameters + bound_verts, new_bcs, new_w_par = self._remove_colinear_props( + pts_3d, pts_2d, segs_2d, bound_cds, win_pars, ftc_height, tolerance) + holes = None + else: + pts_3d = self.floor_geometry.boundary + pts_2d = self.floor_geometry.boundary_polygon2d + segs_2d = pts_2d.segments + st_i = len(pts_3d) + bound_cds = self.boundary_conditions[:st_i] + win_pars = self.window_parameters[:st_i] + bound_verts, new_bcs, new_w_par = self._remove_colinear_props( + pts_3d, pts_2d, segs_2d, bound_cds, win_pars, ftc_height, tolerance) + holes = [] + for i, pts_3d in enumerate(self.floor_geometry.holes): + pts_2d = self.floor_geometry.hole_polygon2d[i] + segs_2d = pts_2d.segments + bound_cds = self.boundary_conditions[st_i:st_i + len(pts_3d)] + win_pars = self.window_parameters[st_i:st_i + len(pts_3d)] + st_i += len(pts_3d) + h_verts, h_bcs, h_w_par = self._remove_colinear_props( + pts_3d, pts_2d, segs_2d, bound_cds, win_pars, + ftc_height, tolerance) + holes.append(h_verts) + new_bcs.extend(h_bcs) + new_w_par.extend(h_w_par) + + # create the new Room2D + new_geo = Face3D(bound_verts, holes=holes) + rebuilt_room = Room2D( + self.identifier, new_geo, self.floor_to_ceiling_height, + boundary_conditions=new_bcs, window_parameters=new_w_par, + is_ground_contact=self.is_ground_contact, + is_top_exposed=self.is_top_exposed) + + # assign overall properties to the rebuilt room + rebuilt_room._skylight_parameters = self._skylight_parameters + rebuilt_room._display_name = self._display_name + rebuilt_room._user_data = self._user_data + rebuilt_room._parent = self._parent + rebuilt_room._abridged_properties = self._abridged_properties + rebuilt_room._properties._duplicate_extension_attr(self._properties) + return rebuilt_room
+ +
[docs] def remove_short_segments(self, distance, angle_tolerance=1.0): + """Get a version of this Room2D with consecutive short segments removed. + + To patch over the segments, an attempt will first be made to find the + intersection of the two neighboring segments. If these two lines are parallel, + they will simply be connected with a segment. + + Properties assigned to the Room2D will be preserved for the segments that + are not removed. + + Args: + distance: The maximum length of a segment below which the segment + will be considered for removal. + angle_tolerance: The max angle difference in degrees that vertices + are allowed to differ from one another in order to consider them + colinear. (Default: 1). + """ + # first check if there are contiguous short segments to be removed + segs = [self._floor_geometry.boundary_segments] + if self._floor_geometry.has_holes: + for hole in self._floor_geometry.hole_segments: + segs.append(hole) + sh_seg_i = [[i for i, s in enumerate(sg) if s.length <= distance] for sg in segs] + if len(segs[0]) - len(sh_seg_i[0]) < 3: + return None # large distance means the whole Face becomes removed + if all(len(s) <= 1 for s in sh_seg_i): + return self # no short segments to remove + del_seg_i = [] + for sh_seg in sh_seg_i: + del_seg = set() + for i, seg_i in enumerate(sh_seg): + test_val = seg_i - sh_seg[i - 1] + if test_val == 1 or (seg_i == 0 and test_val < 0): + del_seg.add(sh_seg[i - 1]) + del_seg.add(seg_i) + if 0 in sh_seg and len(sh_seg) - 1 in sh_seg: + del_seg.add(0) + del_seg.add(len(sh_seg) - 1) + del_seg_i.append(sorted(list(del_seg))) + if all(len(s) == 0 for s in del_seg_i): + return self # there are short segments but they're not contiguous + + # contiguous short segments found + # collect the vertices and indices of properties to be removed + a_tol = math.radians(angle_tolerance) + prev_i, final_pts, del_prop_i = 0, [], [] + for p_segs, del_i in zip(segs, del_seg_i): + if len(del_i) != 0: + # set up variables to handle getting the last vertex to connect to + new_points, in_del, post_del = [], False, False + if 0 in del_i and len(p_segs) - 1 in del_i: + last_i, in_del = -1, True + try: + while del_i[last_i] - del_i[last_i - 1] == 1: + last_i -= 1 + except IndexError: # entire hole to be removed + for i in range(len(p_segs)): + del_prop_i.append(prev_i + i) + p_segs = [] + # loop through the segments and delete the short ones + for i, lin in enumerate(p_segs): + if i in del_i: + if not in_del: + last_i = i + in_del = True + del_prop_i.append(prev_i + i) + rel_i = i + 1 if i + 1 != len(p_segs) else 0 + if rel_i not in del_i: # we are at the end of the deletion + # see if we can repair the hole by extending segments + l3a, l3b = p_segs[last_i - 1], p_segs[rel_i] + l2a = Ray2D(Point2D(l3a.p.x, l3a.p.y), + Vector2D(l3a.v.x, l3a.v.y)) + l2b = Ray2D(Point2D(l3b.p.x, l3b.p.y), + Vector2D(l3b.v.x, l3b.v.y)) + v_ang = l2a.v.angle(l2b.v) + if v_ang <= a_tol or v_ang >= math.pi - a_tol: # parallel + new_points.append(p_segs[last_i].p) + del_prop_i.pop(-1) # put back the last property + else: # extend lines to the intersection + int_pt = self._intersect_line2d_infinite(l2a, l2b) + int_pt3 = Point3D(int_pt.x, int_pt.y, self.floor_height) + new_points.append(int_pt3) + post_del = True + in_del = False + else: + if not post_del: + new_points.append(lin.p) + post_del = False + if post_del: + new_points.pop(0) # put back the last property + if len(new_points) != 0: + final_pts.append(new_points) + else: # no short segments to remove on this hole or boundary + final_pts.append([lin.p for lin in p_segs]) + prev_i += len(p_segs) + + # create the geometry and convert properties for the new segments + holes = None if len(final_pts) == 1 else final_pts[1:] + new_geo = Face3D(final_pts[0], self.floor_geometry.plane, holes) + new_bcs = self._boundary_conditions[:] + new_win = self._window_parameters[:] + new_shd = self._shading_parameters[:] + new_abs = list(self.air_boundaries) + all_props = (new_bcs, new_win, new_shd, new_abs) + for prop_list in all_props: + for di in reversed(del_prop_i): + prop_list.pop(di) + + # create the final rebuilt Room2D and return it + rebuilt_room = Room2D( + self.identifier, new_geo, self.floor_to_ceiling_height, new_bcs, new_win, + new_shd, self.is_ground_contact, self.is_top_exposed) + rebuilt_room._air_boundaries = new_abs + rebuilt_room._skylight_parameters = self._skylight_parameters + rebuilt_room._display_name = self._display_name + rebuilt_room._user_data = self._user_data + rebuilt_room._parent = self._parent + rebuilt_room._abridged_properties = self._abridged_properties + rebuilt_room._properties._duplicate_extension_attr(self._properties) + return rebuilt_room
+ +
[docs] def subtract_room_2ds(self, room_2ds, tolerance=0.01): + """Get (a) version(s) of this Room2D with other Room2Ds subtracted from it. + + This is useful for resolving overlaps between Room2Ds of the same Story. + + Args: + room_2d: A Room2D that will be subtracted from this Room2D. + tolerance: The maximum difference between point values for them to be + considered distinct from one another. (Default: 0.01; suitable + for objects in Meters). + + Returns: + A list of Room2D for the result of splitting this Room2D with the + input line. Will be a list with only the current Room2D if the line + does not split it into two or more pieces. + """ + # first check that the two geometries have the same Z coordinate + self_face = self.floor_geometry + z_v = self_face[0].z + other_faces = [] + for room_2d in room_2ds: + face2 = room_2d.floor_geometry + if abs(self_face[0].z - face2[0].z) > tolerance: + new_bound = [Point3D(pt.x, pt.y, z_v) for pt in face2.boundary] + new_orig = Point3D(face2[0].x, face2[0].y, z_v) + new_plane = Plane(n=face2.plane.n, o=new_orig) + new_holes = [[Point3D(p.x, p.y, z_v) for p in h] for h in face2.holes] \ + if face2.has_holes else None + face2 = Face3D(new_bound, new_plane, new_holes) + other_faces.append(face2) + + # subtract the other Room2Ds from this one + ang_tol = math.radians(1) + new_geos = self_face.coplanar_difference(other_faces, tolerance, ang_tol) + if len(new_geos) == 1 and new_geos[0] is self_face: + return [self] # the Face3D did not overlap with one another + new_geos.sort(key=lambda x: x.area, reverse=True) + + # create the final rebuilt Room2Ds and return them + new_rooms = [] + for i, new_geo in enumerate(new_geos): + rm_id = self.identifier if i == 0 else '{}{}'.format(self.identifier, i) + rebuilt_room = Room2D( + rm_id, new_geo, self.floor_to_ceiling_height, + is_ground_contact=self.is_ground_contact, + is_top_exposed=self.is_top_exposed) + self._match_and_transfer_wall_props(rebuilt_room, tolerance) + if i == 0: + rebuilt_room._skylight_parameters = self._skylight_parameters + rebuilt_room._display_name = self._display_name + rebuilt_room._user_data = self._user_data + rebuilt_room._parent = self._parent + rebuilt_room._abridged_properties = self._abridged_properties + rebuilt_room._properties._duplicate_extension_attr(self._properties) + new_rooms.append(rebuilt_room) + return new_rooms
+ +
[docs] def split_with_line(self, line, tolerance=0.01): + """Get versions of this Room2D that are split by a line. + + If the input line does not intersect this Room2D in a manner that splits + it into two or more pieces, a list with only the current room will be + returned. + + Args: + line: A LineSegment2D object that will be used to split this Room2D + into two or more pieces. + tolerance: The maximum difference between point values for them to be + considered distinct from one another. (Default: 0.01; suitable + for objects in Meters). + + Returns: + A list of Room2D for the result of splitting this Room2D with the + input line. Will be a list with only the current Room2D if the line + does not split it into two or more pieces. + """ + # create a 3D version of the line for the closest point calculation + if isinstance(line, LineSegment2D): + # check if the coordinate values are too high to resolve with tolerance + t_up = tolerance * 1e6 + if line.p.x > t_up or line.p.y > t_up or line.v.x > t_up or line.v.y > t_up: + min_pt, max_pt = self.min, self.max + base, hgt = max_pt.x - min_pt.x, max_pt.y - min_pt.y + bound_rect = Polygon2D.from_rectangle(min_pt, Vector2D(0, 1), base, hgt) + inter_pts = bound_rect.intersect_line_ray(line) + if len(inter_pts) == 2: + line = LineSegment2D.from_end_points(inter_pts[0], inter_pts[1]) + line_3d = LineSegment3D(Point3D(line.p.x, line.p.y, self.floor_height), + Vector3D(line.v.x, line.v.y, 0)) + else: + msg = 'Expected LineSegment2D. Got {}.'.format(type(line)) + raise TypeError(msg) + # split the Room2D with the line + new_geos = self.floor_geometry.split_with_line(line_3d, tolerance) + if new_geos is None or len(new_geos) == 1: + return [self] # the Face3D did not overlap with one another + # create the final Room2Ds + return self._create_split_rooms(new_geos, tolerance)
+ +
[docs] def split_with_polyline(self, polyline, tolerance=0.01): + """Get versions of this Room2D that are split into two or more by a polyline. + + If the input polyline does not intersect this Room2D in a manner that splits + it into two or more pieces, a list with only the current room will be + returned. + + Args: + polyline: A Polyline2D object that will be used to split this Room2D + into two or more pieces. + tolerance: The maximum difference between point values for them to be + considered distinct from one another. (Default: 0.01; suitable + for objects in Meters). + + Returns: + A list of Room2D for the result of splitting this Room2D with the + input polyline. Will be a list with only the current Room2D if the + polyline does not split it into two or more pieces. + """ + # create a 3D version of the polyline for the closest point calculation + if isinstance(polyline, Polyline2D): + polyline_3d = Polyline3D( + [Point3D(pt.x, pt.y, self.floor_height) for pt in polyline]) + else: + msg = 'Expected Polyline2D. Got {}.'.format(type(polyline)) + raise TypeError(msg) + # split the Room2D with the polyline + new_geos = self.floor_geometry.split_with_polyline(polyline_3d, tolerance) + if new_geos is None or len(new_geos) == 1: + return [self] # the Face3D did not overlap with one another + # create the final Room2Ds + return self._create_split_rooms(new_geos, tolerance)
+ +
[docs] def split_with_polygon(self, polygon, tolerance=0.01): + """Get versions of this Room2D that are split into two or more by a polygon. + + If the input polygon does not intersect this Room2D in a manner that splits + it into two or more pieces, a list with only the current room will be + returned. + + Args: + polygon: A Polygon2D object that will be used to split this Room2D + into two or more pieces. + tolerance: The maximum difference between point values for them to be + considered distinct from one another. (Default: 0.01; suitable + for objects in Meters). + + Returns: + A list of Room2D for the result of splitting this Room2D with the + input polygon. Will be a list with only the current Room2D if the + polygon does not split it into two or more pieces. + """ + # create a 3D version of the polygon for the closest point calculation + if isinstance(polygon, Polygon2D): + face_3d = Face3D( + [Point3D(pt.x, pt.y, self.floor_height) for pt in polygon]) + else: + msg = 'Expected Polygon2D. Got {}.'.format(type(polygon)) + raise TypeError(msg) + # split the Room2D with the polygon + ang_tol = math.radians(1) + new_geos, _ = Face3D.coplanar_split( + self.floor_geometry, face_3d, tolerance, ang_tol) + if new_geos is None or len(new_geos) == 1: + return [self] # the Face3D did not overlap with one another + # create the final Room2Ds + return self._create_split_rooms(new_geos, tolerance)
+ +
[docs] def split_with_lines(self, lines, tolerance=0.01): + """Get versions of this Room2D that are split by a line. + + Using this method is distinct from looping over the Room2D.split_with_line + in that this method will resolve cases where multiple segments branch out + from nodes in a network of input lines. So, if three line segments + meet at a point in the middle of this Room2D and each extend past the + edges of this Room2D, this method can split the Room2D in 3 parts whereas + looping over the Room2D.split_with_line will not do this given that each + individual segment cannot split the Room2D. + + If the input lines together do not intersect this Room2D in a manner + that splits it into two or more pieces, a list with only the current + room will be returned. + + Args: + lines: A list of LineSegment2D objects that will be used to split + this Room2D into two or more pieces. + tolerance: The maximum difference between point values for them to be + considered distinct from one another. (Default: 0.01; suitable + for objects in Meters). + + Returns: + A list of Room2D for the result of splitting this Room2D with the + input line. Will be a list with only the current Room2D if the line + does not split it into two or more pieces. + """ + # create 3D versions of the lines for the closest point calculation + lines_3d = [] + t_up = tolerance * 1e6 + for line in lines: + if isinstance(line, LineSegment2D): + # check if the coordinate values are too high to resolve with tolerance + if line.p.x > t_up or line.p.y > t_up or \ + line.v.x > t_up or line.v.y > t_up: + min_pt, max_pt = self.min, self.max + base, hgt = max_pt.x - min_pt.x, max_pt.y - min_pt.y + bound_rect = Polygon2D.from_rectangle( + min_pt, Vector2D(0, 1), base, hgt) + inter_pts = bound_rect.intersect_line_ray(line) + if len(inter_pts) == 2: + line = LineSegment2D.from_end_points(inter_pts[0], inter_pts[1]) + line_3d = LineSegment3D(Point3D(line.p.x, line.p.y, self.floor_height), + Vector3D(line.v.x, line.v.y, 0)) + lines_3d.append(line_3d) + else: + msg = 'Expected LineSegment2D. Got {}.'.format(type(line)) + raise TypeError(msg) + # split the Room2D with the line + new_geos = self.floor_geometry.split_with_lines(lines_3d, tolerance) + if new_geos is None or len(new_geos) == 1: + return [self] # the Face3D did not overlap with one another + # create the final Room2Ds + return self._create_split_rooms(new_geos, tolerance)
+ + def _create_split_rooms(self, face_3ds, tolerance): + """Create Room2Ds from Face3Ds that were split from this Room2D.""" + # create the Room2Ds + new_rooms = [] + for i, new_geo in enumerate(face_3ds): + rm_id = '{}{}'.format(self.identifier, i) + rebuilt_room = Room2D( + rm_id, new_geo, self.floor_to_ceiling_height, + is_ground_contact=self.is_ground_contact, + is_top_exposed=self.is_top_exposed) + self._match_and_transfer_wall_props(rebuilt_room, tolerance) + rebuilt_room._display_name = self._display_name + rebuilt_room._user_data = self._user_data + rebuilt_room._parent = self._parent + rebuilt_room._abridged_properties = self._abridged_properties + rebuilt_room._properties._duplicate_extension_attr(self._properties) + new_rooms.append(rebuilt_room) + + # split the skylights if they exist + if self.skylight_parameters is not None: + room_faces = [r.floor_geometry for r in new_rooms] + new_skys = self.skylight_parameters.split(room_faces, tolerance) + for room, sky_par in zip(new_rooms, new_skys): + room.skylight_parameters = sky_par + + return new_rooms + +
[docs] def check_horizontal(self, tolerance=0.01, raise_exception=True): + """Check whether the Room2D's floor geometry is horizontal within a tolerance. + + Args: + tolerance: The maximum difference between z values at which + face vertices are considered at different heights. Default: 0.01, + suitable for objects in meters. + raise_exception: Boolean to note whether a ValueError should be raised + if the room floor geometry is not horizontal. + """ + z_vals = tuple(pt.z for pt in self._floor_geometry.vertices) + if max(z_vals) - min(z_vals) <= tolerance: + return '' + msg = 'Room "{}" is not horizontal to within {} tolerance.'.format( + self.display_name, tolerance) + if raise_exception: + raise ValueError(msg) + return msg
+ +
[docs] def check_degenerate(self, tolerance=0.01, raise_exception=True, detailed=False): + """Check whether the Room2D's floor geometry is degenerate with zero area. + + Args: + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. (Default: 0.01, + suitable for objects in meters). + raise_exception: If True, a ValueError will be raised if the object + intersects with itself. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + degenerate = False + try: + self.floor_geometry.remove_colinear_vertices(tolerance) + except AssertionError: # degenerate geometry found! + degenerate = True + + if degenerate: + msg = 'Room2D "{}" has degenerate floor geometry with zero ' \ + 'area.'.format(self.display_name) + if raise_exception: + raise ValueError(msg) + full_msg = self._validation_message_child( + msg, self, detailed, '100101', + error_type='Degenerate Room Geometry') + if detailed: + return [full_msg] + if raise_exception: + raise ValueError(full_msg) + return full_msg + return [] if detailed else ''
+ +
[docs] def check_self_intersecting(self, tolerance=0.01, raise_exception=True, + detailed=False): + """Check whether the Room2D's floor geometry intersects itself (like a bowtie). + + Note that objects that have duplicate vertices will not be considered + self-intersecting and are valid. + + Args: + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. (Default: 0.01, + suitable for objects in meters). + raise_exception: If True, a ValueError will be raised if the object + intersects with itself. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + if self.floor_geometry.is_self_intersecting: + msg = 'Room2D "{}" has floor geometry with self-intersecting ' \ + 'edges.'.format(self.display_name) + try: # see if it is self-intersecting because of a duplicate vertex + new_geo = self.floor_geometry.remove_duplicate_vertices(tolerance) + if not new_geo.is_self_intersecting: + return [] if detailed else '' # valid with removed dup vertex + except AssertionError: + pass # zero area face; treat it as self-intersecting + full_msg = self._validation_message_child( + msg, self, detailed, '100102', + error_type='Self-Intersecting Room Geometry') + if detailed: + return [full_msg] + if raise_exception: + raise ValueError(full_msg) + return full_msg + return [] if detailed else ''
+ +
[docs] def check_window_parameters_valid( + self, tolerance=0.01, raise_exception=True, detailed=False): + """Check whether the window and skylight parameters produce valid apertures. + + This means that this Room's windows do not overlap with one another and, + in the case of detailed windows, the polygons do not self-intersect. It + also means that skylights do not extend past the boundary of the room. + + Args: + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. (Default: 0.01, + suitable for objects in meters). + raise_exception: Boolean to note whether a ValueError should be raised + if the window parameters are not valid. + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + detailed = False if raise_exception else detailed + msgs = [] + checkable_par = (RectangularWindows, DetailedWindows) + for i, wp in enumerate(self._window_parameters): + if wp is not None and isinstance(wp, checkable_par): + msg = wp.check_window_overlaps(tolerance) + if msg != '': + msgs.append(' Segment ({}) - {}'.format(i, msg)) + if isinstance(wp, DetailedWindows): + msg = wp.check_self_intersecting(tolerance) + if msg != '': + msgs.append(' Segment ({}) - {}'.format(i, msg)) + if isinstance(self._skylight_parameters, DetailedSkylights): + msg = self._skylight_parameters.check_valid_for_face(self.floor_geometry) + if msg != '': + msgs.append(' Skylights - {}'.format(msg)) + msg = self._skylight_parameters.check_overlaps(tolerance) + if msg != '': + msgs.append(' Skylights - {}'.format(msg)) + msg = self._skylight_parameters.check_self_intersecting(tolerance) + if msg != '': + msgs.append(' Skylights - {}'.format(msg)) + if len(msgs) == 0: + return [] if detailed else '' + full_msg = 'Room2D "{}" contains invalid window parameters.' \ + '\n {}'.format(self.display_name, '\n '.join(msgs)) + full_msg = self._validation_message_child( + full_msg, self, detailed, '100103', error_type='Invalid Window Parameters') + if detailed: + return [full_msg] + if raise_exception: + raise ValueError(full_msg) + return full_msg
+ +
[docs] def to_core_perimeter(self, perimeter_offset, air_boundary=False, tolerance=0.01): + """Translate this Room2D into a list of Room2Ds separated by core and perimeter. + + All of the resulting Room2Ds will have the same properties as this initial + Room2D with all windows and boundary conditions conserved. All of the + newly-created interior walls between the core and perimeter Room2Ds will + have Surface boundary conditions. + + Args: + perimeter_offset: An optional positive number that will be used to offset + the perimeter of the all_story_geometry to create core/perimeter + zones. If this value is 0, no offset will occur and each story + will be represented with a single Room2D per polygon. + air_boundary: A boolean to note whether all of the new wall adjacencies + should be set to an AirBoundary type. (Default: False). + tolerance: The maximum difference between x, y, and z values at which + point vertices are considered to be the same. This is also needed as + a means to determine which floor geometries are equivalent to one + another and should be a part the same Story. Default: 0.01, suitable + for objects in meters. + + Returns: + A list of Room2D for core Room2Ds followed by perimeter Room2Ds. If the + current Room2D cannot be converted into core and perimeter Room2Ds, + a list with the current Room2D instance will be returned. + """ + # create the floor Face3Ds from this Room2D's floor_geometry + tol = tolerance + try: + perimeter, core = perimeter_core_subfaces( + self.floor_geometry, perimeter_offset, tol) + new_face3d_array = perimeter + core + except Exception: # the generation of the polyskel failed; possibly neg offset + return [self] # just use existing floor + + # create the new Room2D objects from the result + parent_zip = ( + self.floor_segments_2d, self.boundary_conditions, + self.window_parameters, self.shading_parameters, self.air_boundaries + ) + new_rooms = [] + for i, floor_geo in enumerate(new_face3d_array): + # determine the segments of the new Room2D + if floor_geo.normal.z < 0: # ensure upward-facing Face3D + floor_geo = floor_geo.flip() + o_p = Plane(Vector3D(0, 0, 1), Point3D(0, 0, floor_geo.plane.o.z)) + floor_geo = Face3D(floor_geo.boundary, o_p, floor_geo.holes) + new_room_seg = floor_geo.boundary_polygon2d.segments \ + if not floor_geo.has_holes \ + else floor_geo.boundary_polygon2d.segments + \ + tuple(seg for hole in floor_geo.hole_polygon2d for seg in hole.segments) + # match the new segments to the existing properties + new_bcs, new_win, new_shd, new_abs = [], [], [], [] + for new_seg in new_room_seg: + p1, p2 = new_seg.p1, new_seg.p2 + for seg, bc, wp, sp, ab in zip(*parent_zip): + if seg.distance_to_point(p1) <= tol and \ + seg.distance_to_point(p2) <= tol: + new_bcs.append(bc) + new_win.append(wp) + new_shd.append(sp) + new_abs.append(ab) + break + else: + new_bcs.append(bcs.outdoors) + new_win.append(None) + new_shd.append(None) + new_abs.append(False) + new_id = '{}_{}'.format(self.identifier, i) + new_room = Room2D( + new_id, floor_geo, self.floor_to_ceiling_height, new_bcs, new_win, + new_shd, self.is_ground_contact, self.is_top_exposed, tol) + new_room.air_boundaries = new_abs + new_room.display_name = '{}_{}'.format(self.display_name, i) + new_room._properties._duplicate_extension_attr(self._properties) + new_rooms.append(new_room) + + # re-assign skylights if they exist + if self.skylight_parameters is not None: + room_faces = [r.floor_geometry for r in new_rooms] + new_skys = self.skylight_parameters.split(room_faces, tol) + for room, sky_par in zip(new_rooms, new_skys): + room.skylight_parameters = sky_par + + # solve adjacency between the Room2Ds + new_rooms = Room2D.intersect_adjacency(new_rooms, tol) + adj_info = Room2D.solve_adjacency(new_rooms, tol) + if air_boundary: # set air boundary type if requested + for room_pair in adj_info: + for room_adj in room_pair: + room, wall_i = room_adj + room.set_air_boundary(wall_i) + return new_rooms
+ +
[docs] def to_honeybee( + self, multiplier=1, add_plenum=False, tolerance=0.01, + enforce_bc=True, enforce_solid=True): + """Convert Dragonfly Room2D to a Honeybee Room. + + Args: + multiplier: An integer greater than 0 that denotes the number of times + the room is repeated. You may want to set this differently depending + on whether you are exporting each room as its own geometry (in which + case, this should be 1) or you only want to simulate the "unique" room + once and have the results multiplied. Default: 1. + add_plenum: Boolean to indicate whether ceiling/floor plenums should + be auto-generated for the Room in which case this output will + be a list instead of a single Room. The height of the ceiling plenum + will be autocalculated as the difference between the Room2D + ceiling height and Story ceiling height. The height of the floor + plenum will be autocalculated as the difference between the Room2D + floor height and Story floor height. (Default: False). + tolerance: The minimum distance in z values of floor_height and + floor_to_ceiling_height at which adjacent Faces will be split. + This is also used in the generation of Windows, and to check if the + Room ceiling is adjacent to the upper floor of the Story before + generating a plenum. Default: 0.01, suitable for objects in meters. + enforce_bc: Boolean to note whether an exception should be raised if + apertures are assigned to Wall with an illegal boundary conditions + (True) or if the invalid boundary condition should be replaced + with an Outdoor boundary condition (False). (Default: True). + enforce_solid: Boolean to note whether the room should be translated + as a solid extrusion whenever translating the room with custom + roof geometry produces a non-solid result (True) or the non-solid + room geometry should be allowed to remain in the result (False). + The latter is useful for understanding why a particular roof + geometry has produced a non-solid result. (Default: True). + + Returns: + A tuple with the two items below. + + * hb_room -- If add_plenum is False, this will be honeybee-core Room + representing the dragonfly Room2D. If the add_plenum argument is True, + this item will be a list of honeybee-core Rooms, with the hb_room as + the first item, and up to two additional items: + + * ceil_plenum -- A honeybee-core Room representing the ceiling + plenum. If there isn't enough space between the Story + floor_to_floor_height and the Room2D floor_to_ceiling height, + this item will be None. + + * floor_plenum -- A honeybee-core Room representing the floor plenum. + If there isn't enough space between the Story floor_height and + the Room2D floor_height, this item will be None. + + * adjacencies -- A list of tuples that record any adjacencies that + should be set on the level of the Story to which the Room2D belongs. + Each tuple will have a honeybee Face as the first item and a + tuple of Surface.boundary_condition_objects as the second item. + """ + # create the honeybee Room + has_roof, shade_geo = False, None + if self._parent is not None and self._parent._roof is not None: + # generate the room volume from the slanted roof + if self.is_top_exposed and multiplier == 1: + room_polyface, roof_face_i, shade_geo = \ + self._room_volume_with_roof(self._parent._roof, tolerance) + if room_polyface is None: # complete failure to interpret roof + has_roof = False + elif enforce_solid and not room_polyface.is_solid: + has_roof = False + else: + has_roof = True + if not has_roof: # generate the Room volume normally through extrusion + room_polyface = Polyface3D.from_offset_face( + self._floor_geometry, self.floor_to_ceiling_height) + roof_face_i = [-1] + + # create the honeybee Room and set the RoofCeiling faces + hb_room = Room.from_polyface3d( + self.identifier, room_polyface, ground_depth=self.floor_height - 1) + roof_faces = [] + for i in roof_face_i: + rfc = hb_room[i] + rfc.type = ftyp.roof_ceiling + roof_faces.append(rfc) + + # assign BCs and record any Surface conditions to be set on the story level + adjacencies = [] + for i, bc in enumerate(self._boundary_conditions): + if not isinstance(bc, Surface): + hb_room[i + 1]._boundary_condition = bc + else: + adjacencies.append((hb_room[i + 1], bc.boundary_condition_objects)) + + # assign windows, shading, and air boundary properties to walls + for i, glz_par in enumerate(self._window_parameters): + if glz_par is not None: + hb_face = hb_room[i + 1] + try: + glz_par.add_window_to_face(hb_face, tolerance) + except AssertionError as e: + if enforce_bc: + raise e + hb_room[i + 1]._boundary_condition = bcs.outdoors + hb_room[i + 1].remove_sub_faces() + glz_par.add_window_to_face(hb_face, tolerance) + if has_roof and isinstance(glz_par, _AsymmetricBase): + valid_ap = [] + for ap in hb_face._apertures: + if hb_face.geometry._is_sub_face(ap.geometry): + valid_ap.append(ap) + if len(hb_face._apertures) != len(valid_ap): + hb_face.remove_apertures() + hb_face.add_apertures(valid_ap) + for i, shd_par in enumerate(self._shading_parameters): + if shd_par is not None: + shd_par.add_shading_to_face(hb_room[i + 1], tolerance) + if self._air_boundaries is not None: + for i, a_bnd in enumerate(self._air_boundaries): + if a_bnd: + hb_room[i + 1].type = ftyp.air_boundary + + # ensure matching adjacent Faces across the Story + if self._parent is not None and not has_roof: + new_faces = self._split_walls_along_height(hb_room, tolerance, add_plenum) + if len(new_faces) != len(hb_room): + # rebuild the room with split surfaces + hb_room = Room(self.identifier, new_faces, tolerance, 0.1) + # update adjacencies with the new split face + for i, adj in enumerate(adjacencies): + face_id = adj[0].identifier + for face in hb_room.faces: + if face.identifier == face_id: + adjacencies[i] = (face, adj[1]) + break + + # set the story, multiplier, display_name, and user_data + if self.has_parent: + hb_room.story = self.parent.display_name + hb_room.multiplier = multiplier + hb_room._display_name = self._display_name + hb_room._user_data = self._user_data + + # assign boundary conditions for the roof and floor + try: + hb_room[0].boundary_condition = bcs.adiabatic + for rf in roof_faces: + rf.boundary_condition = bcs.adiabatic + except AttributeError: + pass # honeybee_energy is not loaded and Adiabatic type doesn't exist + if self._is_ground_contact: + hb_room[0].boundary_condition = bcs.ground + if self._is_top_exposed: + for rf in roof_faces: + rf.boundary_condition = bcs.outdoors + + # assign any Shade geometries like dormers that might result from the roof + if shade_geo: + for i, s_geo in enumerate(shade_geo): + shd_id = '{}_RoofShade{}'.format(self.identifier, i) + hb_room.add_outdoor_shade(Shade(shd_id, s_geo)) + + # transfer any extension properties assigned to the Room2D and return result + hb_room._properties = self.properties.to_honeybee(hb_room) + if not add_plenum or has_roof: + if self._skylight_parameters is not None: + if self._is_top_exposed: + for rf in roof_faces: + self._skylight_parameters.add_skylight_to_face(rf, tolerance) + return hb_room, adjacencies + + # add plenums if requested and return results + hb_plenums = self._honeybee_plenums(hb_room, tolerance=tolerance) + for hb_plenum in hb_plenums: # transfer the parent's construction set + hb_plenum._properties = self.properties.to_honeybee(hb_plenum) + hb_plenum.exclude_floor_area = True + try: # set the program to unconditioned plenum and assign infiltration + hb_plenum.properties.energy.program_type = None + hb_plenum.properties.energy.hvac = None + hb_plenum.properties.energy._shw = None + hb_plenum.properties.energy.infiltration = \ + hb_room.properties.energy.infiltration + except AttributeError: + pass # honeybee-energy is not loaded; ignore all these energy properties + + # set the skylights if top is exposed and there's no ceiling plenum + if self._skylight_parameters is not None: + if self._is_top_exposed: + if len(hb_plenums) == 0: + self._skylight_parameters.add_skylight_to_face( + hb_room[-1], tolerance) + elif len(hb_plenums) == 1 and \ + hb_plenums[0].identifier.endswith('floor_plenum'): + self._skylight_parameters.add_skylight_to_face( + hb_room[-1], tolerance) + + # return the rooms and the adjacency information + return [hb_room] + hb_plenums, adjacencies
+ +
[docs] def to_dict(self, abridged=False, included_prop=None): + """Return Room2D as a dictionary. + + Args: + abridged: Boolean to note whether the extension properties of the + object (ie. program_type, construction_set) should be included in detail + (False) or just referenced by identifier (True). Default: False. + included_prop: List of properties to filter keys that must be included in + output dictionary. For example ['energy'] will include 'energy' key if + available in properties to_dict. By default all the keys will be + included. To exclude all the keys from extensions use an empty list. + """ + base = {'type': 'Room2D'} + base['identifier'] = self.identifier + base['display_name'] = self.display_name + if abridged and self._abridged_properties is not None: + base['properties'] = self._abridged_properties + else: + base['properties'] = self.properties.to_dict(abridged, included_prop) + base['floor_boundary'] = [(pt.x, pt.y) for pt in self._floor_geometry.boundary] + if self._floor_geometry.has_holes: + base['floor_holes'] = \ + [[(pt.x, pt.y) for pt in hole] for hole in self._floor_geometry.holes] + base['floor_height'] = self._floor_geometry[0].z + base['floor_to_ceiling_height'] = self._floor_to_ceiling_height + base['is_ground_contact'] = self._is_ground_contact + base['is_top_exposed'] = self._is_top_exposed + + bc_dicts = [] + for bc in self._boundary_conditions: + if isinstance(bc, Outdoors) and 'energy' in base['properties']: + bc_dicts.append(bc.to_dict(full=True)) + else: + bc_dicts.append(bc.to_dict()) + base['boundary_conditions'] = bc_dicts + + if not all((param is None for param in self._window_parameters)): + base['window_parameters'] = [] + for glz in self._window_parameters: + val = glz.to_dict() if glz is not None else None + base['window_parameters'].append(val) + + if not all((param is None for param in self._shading_parameters)): + base['shading_parameters'] = [] + for shd in self._shading_parameters: + val = shd.to_dict() if shd is not None else None + base['shading_parameters'].append(val) + + if self._air_boundaries is not None: + if not all((not param for param in self._air_boundaries)): + base['air_boundaries'] = self._air_boundaries + + if self._skylight_parameters is not None: + base['skylight_parameters'] = self.skylight_parameters.to_dict() + + if self.user_data is not None: + base['user_data'] = self.user_data + + return base
+ + @property + def to(self): + """Room2D writer object. + + Use this method to access Writer class to write the room2d in other formats. + """ + return writer + +
[docs] @staticmethod + def solve_adjacency(room_2ds, tolerance=0.01, resolve_window_conflicts=True): + """Solve for all adjacencies between a list of input Room2Ds. + + Args: + room_2ds: A list of Room2Ds for which adjacencies will be solved. + tolerance: The minimum difference between the coordinate values of two + faces at which they can be considered adjacent. (Default: 0.01, + suitable for objects in meters). + resolve_window_conflicts: Boolean to note whether conflicts between + window parameters of adjacent segments should be resolved during + adjacency setting or an error should be raised about the mismatch. + Resolving conflicts will default to the window parameters with the + larger are and assign them to the other segment. (Default: True). + + Returns: + A list of tuples with each tuple containing 2 sub-tuples for wall + segments paired in the process of solving adjacency. Sub-tuples have + the Room2D as the first item and the index of the adjacent wall as the + second item. This data can be used to assign custom properties to the + new adjacent walls (like assigning custom window parameters for + interior windows, assigning air boundaries, or custom boundary + conditions). + """ + rwc = resolve_window_conflicts + adj_info = [] + for i, room_1 in enumerate(room_2ds): + try: + for room_2 in room_2ds[i + 1:]: + if not Polygon2D.overlapping_bounding_rect( + room_1._floor_geometry.boundary_polygon2d, + room_2._floor_geometry.boundary_polygon2d, tolerance): + continue # no overlap in bounding rect; adjacency impossible + for j, seg_1 in enumerate(room_1.floor_segments_2d): + for k, seg_2 in enumerate(room_2.floor_segments_2d): + if not isinstance(room_2._boundary_conditions[k], Surface): + if seg_1.distance_to_point(seg_2.p1) <= tolerance and \ + seg_1.distance_to_point(seg_2.p2) <= tolerance: + # set the boundary conditions of the segments + room_1.set_adjacency(room_2, j, k, rwc) + adj_info.append(((room_1, j), (room_2, k))) + break + except IndexError: + pass # we have reached the end of the list of rooms + return adj_info
+ +
[docs] @staticmethod + def find_adjacency(room_2ds, tolerance=0.01): + """Get a list with all adjacent pairs of segments between input Room2Ds. + + Note that this method does not change any boundary conditions of the input + Room2Ds or mutate them in any way. It's purely a geometric analysis of the + segments between Room2Ds. + + Args: + room_2ds: A list of Room2Ds for which adjacencies will be solved. + tolerance: The minimum difference between the coordinate values of two + faces at which they can be considered adjacent. (Default: 0.01, + suitable for objects in meters). + + Returns: + A list of tuples for each discovered adjacency. Each tuple contains + 2 sub-tuples with two elements. The first element is the Room2D and + the second is the index of the wall segment that is adjacent. + """ + adj_info = [] # lists of adjacencies to track + for i, room_1 in enumerate(room_2ds): + try: + for room_2 in room_2ds[i + 1:]: + if not Polygon2D.overlapping_bounding_rect( + room_1._floor_geometry.boundary_polygon2d, + room_2._floor_geometry.boundary_polygon2d, tolerance): + continue # no overlap in bounding rect; adjacency impossible + for j, seg_1 in enumerate(room_1.floor_segments_2d): + for k, seg_2 in enumerate(room_2.floor_segments_2d): + if seg_1.distance_to_point(seg_2.p1) <= tolerance and \ + seg_1.distance_to_point(seg_2.p2) <= tolerance: + adj_info.append(((room_1, j), (room_2, k))) + break + except IndexError: + pass # we have reached the end of the list of rooms + return adj_info
+ +
[docs] @staticmethod + def intersect_adjacency(room_2ds, tolerance=0.01, preserve_wall_props=True): + """Intersect the line segments of an array of Room2Ds to ensure matching walls. + + Also note that this method does not actually set the walls that are next to one + another to be adjacent. The solve_adjacency method must be used for this after + running this method. + + Args: + room_2ds: A list of Room2Ds for which adjacent segments will be + intersected. + tolerance: The minimum difference between the coordinate values of two + faces at which they can be considered adjacent. Default: 0.01, + suitable for objects in meters. + preserve_wall_props: Boolean to note whether existing window parameters, + shading parameters and boundary conditions should be preserved as + vertices are added during intersection. If False, all boundary + conditions are replaced with Outdoors, all window parameters are + erased, and this method will execute quickly. If True, an attempt + will be made to split window parameters new across colinear segments. + Existing boundary conditions will also be kept. (Default: True). + + Returns: + An array of Room2Ds that have been intersected with one another. + """ + # keep track of all data needed to map between 2D and 3D space + master_plane = room_2ds[0].floor_geometry.plane + move_dists = [] + is_holes = [] + polygon_2ds = [] + tol = tolerance + + # map all Room geometry into the same 2D space + for room in room_2ds: + # ensure all starting room heights match + dist = master_plane.o.z - room.floor_height + move_dists.append(dist) # record all distances moved + is_holes.append(False) # record that first Polygon doesn't have holes + polygon_2ds.append(room._floor_geometry.boundary_polygon2d) + # of there are holes in the face, add them as their own polygons + if room._floor_geometry.has_holes: + for hole in room._floor_geometry.hole_polygon2d: + move_dists.append(dist) # record all distances moved + is_holes.append(True) # record that first Polygon doesn't have holes + polygon_2ds.append(hole) + + # intersect the Room2D polygons within the 2D space + int_poly = Polygon2D.intersect_polygon_segments(polygon_2ds, tol) + + # convert the resulting coordinates back to 3D space + face_pts = [] + for poly, dist, is_hole in zip(int_poly, move_dists, is_holes): + pt_3d = [master_plane.xy_to_xyz(pt) for pt in poly] + if dist != 0: + pt_3d = [Point3D(pt.x, pt.y, pt.z - dist) for pt in pt_3d] + if not is_hole: + face_pts.append((pt_3d, [])) + else: + face_pts[-1][1].append(pt_3d) + + # rebuild all of the floor geometries to the input Room2Ds + intersected_rooms = [] + for i, face_loops in enumerate(face_pts): + if len(face_loops[1]) == 0: # no holes + new_geo = Face3D(face_loops[0], room_2ds[i].floor_geometry.plane) + else: # ensure holes are included + new_geo = Face3D(face_loops[0], room_2ds[i].floor_geometry.plane, + face_loops[1]) + rebuilt_room = Room2D( + room_2ds[i].identifier, new_geo, room_2ds[i].floor_to_ceiling_height, + is_ground_contact=room_2ds[i].is_ground_contact, + is_top_exposed=room_2ds[i].is_top_exposed) + rebuilt_room._skylight_parameters = room_2ds[i].skylight_parameters + rebuilt_room._display_name = room_2ds[i]._display_name + rebuilt_room._user_data = None if room_2ds[i].user_data is None else \ + room_2ds[i].user_data.copy() + rebuilt_room._parent = room_2ds[i]._parent + rebuilt_room._abridged_properties = room_2ds[i]._abridged_properties + rebuilt_room._properties._duplicate_extension_attr(room_2ds[i]._properties) + intersected_rooms.append(rebuilt_room) + + # transfer the wall properties if requested + if preserve_wall_props: + for orig_r, new_r in zip(room_2ds, intersected_rooms): + orig_r._match_and_transfer_wall_props(new_r, tolerance) + + return tuple(intersected_rooms)
+ +
[docs] @staticmethod + def group_by_adjacency(rooms): + """Group Room2Ds together that are connected by adjacencies. + + This is useful for separating rooms in the case where a Story contains + multiple towers or sections that are separated by outdoor boundary conditions. + + Args: + rooms: A list of Room2Ds to be grouped by their adjacency. + + Returns: + A list of list with each sub-list containing rooms that share adjacencies. + """ + return Room2D._adjacency_grouping(rooms, Room2D._find_adjacent_rooms)
+ +
[docs] @staticmethod + def group_by_air_boundary_adjacency(rooms): + """Group Room2Ds together that share air boundaries. + + This is useful for understanding the radiant enclosures that will exist + when a model is exported to EnergyPlus. + + Args: + rooms: A list of Room2Ds to be grouped by their air boundary adjacency. + + Returns: + A list of list with each sub-list containing Room2Ds that share adjacent + air boundaries. If a Room has no air boundaries it will the the only + item within its sub-list. + """ + return Room2D._adjacency_grouping( + rooms, Room2D._find_adjacent_air_boundary_rooms)
+ +
[docs] @staticmethod + def join_room_2ds(room_2ds, min_separation=0, tolerance=0.01): + """Join Room2Ds together that are touching one another within a min_separation. + + When the min_separation is less than or equal to the tolerance, all + properties of segments for the input Room2Ds will be preserved. When + the min_separation is larger than the tolerance, an attempt is made to + preserve all wall properties but there is a risk of losing some windows + just in the region where two Room2Ds are joined together across a gap + between them. This risk can be overcome by inserting Room2D vertices + around where the gap will be crossed between that Room2D and the + other Room2D. + + The largest Room2D that is identified within each connected group will + determine the extension properties of the resulting Room2D. Skylights + will be merged across rooms if they are of the same type or if they are None. + + Args: + room_2ds: A list of Room2Ds which will be joined together where they + touch one another. + min_separation: A number for the minimum distance between Room2Ds that + is considered a meaningful separation. Gaps between Room2Ds that + are less than this distance will result in the Room2Ds being + joined across the gap. When the input Room2Ds have floor_geometry + representing the boundaries defined by the interior wall finishes, + this input can be thought of as the maximum interior wall thickness. + When Room2Ds are perfectly touching one another within the tolerance + (with Room2D floor_geometry drawn to the center lines of interior + walls), this value can be set to zero or anything less than or + equal to the tolerance. Doing so will yield a cleaner result for the + boundary, which will be faster and more reliable. Note that care + should be taken not to set this value higher than the length of any + meaningful exterior wall segments. Otherwise, the exterior segments + will be ignored in the result. This can be particularly dangerous + around curved exterior walls that have been planarized through + subdivision into small segments. (Default: 0). + tolerance: The minimum distance between a vertex and the polygon + boundary at which point the vertex is considered to lie on the + polygon. (Default: 0.01, suitable for objects in meters). + """ + # get the horizontal boundaries around the input Room2Ds + h_bnds = Room2D.grouped_horizontal_boundary(room_2ds, min_separation, tolerance) + if len(h_bnds) == len(room_2ds): # no Room2Ds to join; return them as they are + return room_2ds + + # ensure Room2D vertices at the boundary exist + if min_separation <= tolerance: + room_2ds = Room2D.intersect_adjacency(room_2ds, tolerance) + else: # we have to figure out if new vertices were added to cross the boundary + # gather all vertices across the horizontal boundaries + bnd_verts = [] + for h_bnd in h_bnds: + bnd_verts.extend([Point2D(pt.x, pt.y) for pt in h_bnd.boundary]) + if h_bnd.has_holes: + for hole in h_bnd.holes: + bnd_verts.extend([Point2D(pt.x, pt.y) for pt in hole]) + # loop through rooms and identify vertices to insert + inter_rooms = [] + search_dist = tolerance * 2 + for room in room_2ds: + floor_segs = [room.floor_geometry.boundary_polygon2d.segments] + if room.floor_geometry.has_holes: + for hole in room.floor_geometry.hole_polygon2d: + floor_segs.append(hole.segments) + pts_2d, edit_code = [], [] + for loop in floor_segs: + loop_pts_2d = [] + for seg in loop: + loop_pts_2d.append(seg.p1) + edit_code.append('K') + for bnd_pt in bnd_verts: + if seg.distance_to_point(bnd_pt) <= search_dist: + if not seg.p1.is_equivalent(bnd_pt, tolerance) and \ + not seg.p2.is_equivalent(bnd_pt, tolerance): + loop_pts_2d.append(bnd_pt) # vertex to insert ! + edit_code.append('A') + pts_2d.append(loop_pts_2d) + edit_code = ''.join(edit_code) + if 'A' in edit_code: # room geometry must be updated + room = room.duplicate() # duplicate to avoid editing original geo + z_v = room.floor_height + pts_3d = [] + for loop in pts_2d: + pts_3d.append([Point3D(pt.x, pt.y, z_v) for pt in loop]) + new_geo = Face3D(pts_3d[0]) if len(pts_3d) == 1 else \ + Face3D(pts_3d[0], holes=pts_3d[1:]) + room.update_floor_geometry(new_geo, edit_code, search_dist) + inter_rooms.append(room) + room_2ds = inter_rooms + + # join the Room2Ds according to the horizontal boundaries that were found + joined_rooms = [] + for h_bnd in h_bnds: + bnd_p_gon = Polygon2D([Point2D(pt.x, pt.y) for pt in h_bnd.boundary]) + h_p = None + if h_bnd.has_holes: + h_p = [] + for hole in h_bnd.holes: + h_p.append(Polygon2D([Point2D(pt.x, pt.y) for pt in hole])) + new_room = Room2D.join_by_boundary( + room_2ds, bnd_p_gon, h_p, tolerance=tolerance) + joined_rooms.append(new_room) + return joined_rooms
+ +
[docs] @staticmethod + def join_by_boundary( + room_2ds, polygon, hole_polygons=None, floor_to_ceiling_height=None, + identifier=None, display_name=None, tolerance=0.01): + """Join several Room2D together using a boundary Polygon as a guide. + + All properties of segments along the boundary polygon will be preserved. + The largest Room2D that is identified within the boundary polygon will + determine the extension properties of the resulting Room unless the supplied + identifier matches an existing Room2D inside the polygon. Skylights + will be merged if they are of the same type or if they are None. + + It is recommended that the Room2Ds be aligned to the boundaries + of the polygon and duplicate vertices be removed before passing them + through this method. However, colinear vertices should not be removed + where possible. This helps ensure that relevant Room2D segments + are colinear with the polygon and so they can influence the result. + + Args: + room_2ds: A list of Room2Ds which will be joined together using the polygon. + polygon: A ladybug_geometry Polygon2D which will become the boundary + of the output joined Room2D. + hole_polygons: An optional list of hole polygons, which will add + holes into the output joined Room2D polygon. (Default: None). + floor_to_ceiling_height: An optional number to set the floor-to-ceiling + height of the resulting Room2D. If None, it will be the maximum + of the Room2Ds that are found inside the polygon, which ensures + that all window geometries are included in the output. If specified + and it is lower than the maximum Room2D height, any detailed + windows will be automatically trimmed to accommodate the new + floor-to-ceiling height. (Default: None). + identifier: An optional text string for the identifier of the new + joined Room2D. If this matches an existing Room2D inside of the + polygon, the existing Room2D will be used to set the extension + properties of the output Room2D. If None, the identifier + and extension properties of the output Room2D will be those of + the largest Room2D found inside of the polygon. (Default: None). + display_name: An optional text string for the display_name of the new + joined Room2D. If None, the display_name will be taken from the + largest existing Room2D inside the polygon or the existing + Room2D matching the identifier above. (Default: None). + tolerance: The minimum distance between a vertex and the polygon + boundary at which point the vertex is considered to lie on the + polygon. (Default: 0.01, suitable for objects in meters). + """ + tol = tolerance + # ensure that all polygons are counterclockwise + polygon = polygon.reverse() if polygon.is_clockwise else polygon + if hole_polygons is not None: + cc_hole_polygons = [] + for p in hole_polygons: + p = p.reverse() if p.is_clockwise else p + cc_hole_polygons.append(p) + hole_polygons = cc_hole_polygons + + # identify all Room2Ds inside of the polygon + rel_rooms, rel_ids, rel_a, rel_fh, rel_ch = [], [], [], [], [] + test_vec = Vector2D(0.99, 0.01) + for room in room_2ds: + if room.floor_geometry.is_convex: + rm_pt = room.center + else: + rm_pt_3d = room.floor_geometry._point_on_face(tol) + rm_pt = Point2D(rm_pt_3d.x, rm_pt_3d.y) + if polygon.is_point_inside_bound_rect(rm_pt, test_vec): + rel_rooms.append(room) + rel_ids.append(room.identifier) + rel_a.append(room.floor_area) + rel_fh.append(room.floor_height) + rel_ch.append(room.floor_to_ceiling_height) + + # if no rooms are inside the polygon, just return a dummy room from the polygon + if len(rel_rooms) == 0: + fh = sum([r.floor_height for r in room_2ds]) / len(room_2ds) + ftc = sum([r.floor_to_ceiling_height for r in room_2ds]) / len(room_2ds) \ + if floor_to_ceiling_height is None else floor_to_ceiling_height + bound_verts = [Point3D(p.x, p.y, fh) for p in polygon.vertices] + all_hole_verts = None + if hole_polygons is not None and len(hole_polygons) != 0: + all_hole_verts = [] + for hole in hole_polygons: + all_hole_verts.append([Point3D(p.x, p.y, fh) for p in hole.vertices]) + new_geo = Face3D(bound_verts, holes=all_hole_verts) + r_id = clean_and_id_string('Room') if identifier is None else identifier + return Room2D(r_id, new_geo, ftc) + + # determine the new floor heights using max/average across relevant rooms + new_flr_height = sum(rel_fh) / len(rel_fh) + max_ftc = max(rel_ch) + new_ftc = max_ftc if floor_to_ceiling_height is None else floor_to_ceiling_height + + # determine a primary room to set help set properties or the resulting room + if identifier is None or identifier not in rel_ids: + # find the largest room of the relevant rooms + sort_inds = [i for _, i in sorted(zip(rel_a, range(len(rel_a))))] + primary_room = rel_rooms[sort_inds[-1]] + if identifier is None: + identifier = primary_room.identifier + else: # draw properties from the room with the matching identifier + for r_id, rm in zip(rel_ids, rel_rooms): + if r_id == identifier: + primary_room = rm + break + if display_name is None: + display_name = primary_room.display_name + + # gather all segments and properties of relevant rooms + rel_segs, rel_bcs, rel_win, rel_shd, rel_abs = [], [], [], [], [] + for room in rel_rooms: + rel_segs.extend(room.floor_segments_2d) + rel_bcs.extend(room.boundary_conditions) + rel_shd.extend(room.shading_parameters) + rel_abs.extend(room.air_boundaries) + w_par = room.window_parameters + in_range = new_ftc - tol < room.floor_to_ceiling_height < new_ftc + tol + if not in_range: # adjust window ratios to preserve area + new_w_par = [] + for i, wp in enumerate(w_par): + if isinstance(wp, SimpleWindowRatio): + w_area = wp.area_from_segment( + rel_segs[i], room.floor_to_ceiling_height) + new_ratio = w_area / (new_ftc * rel_segs[i].length) + new_wp = wp.duplicate() + new_wp._window_ratio = new_ratio if new_ratio <= 0.99 else 0.99 + new_w_par.append(new_wp) + else: + new_w_par.append(wp) + w_par = new_w_par + rel_win.extend(w_par) + + # find all of the Room2Ds segments that lie on each polygon segment + new_bcs, new_win, new_shd, new_abs = [], [], [], [] + bound_verts = Room2D._segments_along_polygon( + polygon, rel_segs, rel_bcs, rel_win, rel_shd, rel_abs, + new_bcs, new_win, new_shd, new_abs, new_flr_height, tol) + if hole_polygons is not None and len(hole_polygons) != 0: + all_hole_verts = [] + for hole in hole_polygons: + hole_verts = Room2D._segments_along_polygon( + hole, rel_segs, rel_bcs, rel_win, rel_shd, rel_abs, + new_bcs, new_win, new_shd, new_abs, new_flr_height, tol) + all_hole_verts.append(hole_verts) + new_geo = Face3D(bound_verts, holes=all_hole_verts) + else: + new_geo = Face3D(bound_verts) + + # merge skylights across the input rooms if they are of the same type + new_sky_lights, new_areas = [], [] + for room in rel_rooms: + if room.skylight_parameters is not None: + new_sky_lights.append(room.skylight_parameters) + new_areas.append(room.floor_area) + new_sky_light = None + if all(isinstance(sl, DetailedSkylights) for sl in new_sky_lights): + try: + new_polys = new_sky_lights[0].polygons + new_is_dr = new_sky_lights[0].are_doors + for sl in new_sky_lights[1:]: + new_polys += sl.polygons + new_is_dr += sl.are_doors + new_sky_light = DetailedSkylights(new_polys, new_is_dr) + except IndexError: + pass # skylight with no polygons + elif all(isinstance(sl, GriddedSkylightArea) for sl in new_sky_lights): + new_area = sum(sl.skylight_area for sl in new_sky_lights) + new_sky_light = GriddedSkylightArea(new_area) + elif all(isinstance(sl, GriddedSkylightRatio) for sl in new_sky_lights): + zip_obj = zip(new_sky_lights, new_areas) + new_area = sum(sl.skylight_ratio * fa for sl, fa in zip_obj) + new_ratio = new_area / sum(room.floor_area for room in rel_rooms) + new_sky_light = GriddedSkylightRatio(new_ratio) + + # merge all segments and properties into a single Room2D + new_room = Room2D( + identifier, new_geo, new_ftc, new_bcs, new_win, new_shd, + primary_room.is_ground_contact, primary_room.is_top_exposed, tol) + new_room.skylight_parameters = new_sky_light + new_room.air_boundaries = new_abs + new_room.display_name = display_name + new_room._properties._duplicate_extension_attr(primary_room._properties) + + # if the floor-to-ceiling height is lower than the max, re-trim windows + if new_ftc < max_ftc: + new_w_pars = [] + for w_par, seg in zip(new_room._window_parameters, new_room.floor_segments): + if isinstance(w_par, DetailedWindows): + new_w_par = w_par.adjust_for_segment(seg, new_ftc, tolerance) + else: + new_w_par = w_par + new_w_pars.append(new_w_par) + new_room._window_parameters = new_w_pars + + return new_room
+ +
[docs] @staticmethod + def grouped_horizontal_boundary(room_2ds, min_separation=0, tolerance=0.01): + """Get a list of Face3D for the horizontal boundary around several Room2Ds. + + This method will attempt to produce a boundary that follows along the + walls of the Room2Ds and it is not suitable for groups of Room2Ds that + overlap one another in plan. This method may return an empty list if the + min_separation is so large that a continuous boundary could not be determined + or if overlaps between input Room2Ds result in failure. + + Args: + room_2ds: A list of Room2Ds for which the horizontal boundary will + be computed. + min_separation: A number for the minimum distance between Room2Ds that + is considered a meaningful separation. Gaps between Room2Ds that + are less than this distance will be ignored and the boundary + will continue across the gap. When the input Room2Ds have floor_geometry + representing the boundaries defined by the interior wall finishes, + this input can be thought of as the maximum interior wall thickness, + which should be ignored in the calculation of the overall boundary + of the Room2Ds. When Room2Ds are touching one another (with Room2D + floor_geometry drawn to the center lines of interior walls), this + value can be set to zero or anything less than or equal to the + tolerance. Doing so will yield a cleaner result for the + boundary, which will be faster. Note that care should be taken + not to set this value higher than the length of any meaningful + exterior wall segments. Otherwise, the exterior segments + will be ignored in the result. This can be particularly dangerous + around curved exterior walls that have been planarized through + subdivision into small segments. (Default: 0). + tolerance: The maximum difference between coordinate values of two + vertices at which they can be considered equivalent. (Default: 0.01, + suitable for objects in meters). + """ + # get the floor geometry of the rooms + floor_geos = [room.floor_geometry for room in room_2ds] + + # remove colinear vertices and degenerate rooms + clean_floor_geos = [] + for geo in floor_geos: + try: + clean_floor_geos.append(geo.remove_colinear_vertices(tolerance)) + except AssertionError: # degenerate geometry to ignore + pass + if len(clean_floor_geos) == 0: + return [] # no Room boundary to be found + + # convert the floor Face3Ds into counterclockwise Polygon2Ds + floor_polys, z_vals = [], [] + for flr_geo in clean_floor_geos: + z_vals.append(flr_geo.min.z) + b_poly = Polygon2D([Point2D(pt.x, pt.y) for pt in flr_geo.boundary]) + floor_polys.append(b_poly) + if flr_geo.has_holes: + for hole in flr_geo.holes: + h_poly = Polygon2D([Point2D(pt.x, pt.y) for pt in hole]) + floor_polys.append(h_poly) + z_min = min(z_vals) + + # if the min_separation is small, use the more reliable intersection method + if min_separation <= tolerance: + closed_polys = Polygon2D.joined_intersected_boundary(floor_polys, tolerance) + else: # otherwise, use the more intense and less reliable gap crossing method + closed_polys = Polygon2D.gap_crossing_boundary( + floor_polys, min_separation, tolerance) + + # remove colinear vertices from the resulting polygons + clean_polys = [] + for poly in closed_polys: + try: + clean_polys.append(poly.remove_colinear_vertices(tolerance)) + except AssertionError: + pass # degenerate polygon to ignore + + # figure out if polygons represent holes in the others and make Face3D + if len(clean_polys) == 0: + return [] + elif len(clean_polys) == 1: # can be represented with a single Face3D + pts3d = [Point3D(pt.x, pt.y, z_min) for pt in clean_polys[0]] + return [Face3D(pts3d)] + else: # need to separate holes from distinct Face3Ds + bound_faces = [] + for poly in clean_polys: + pts3d = tuple(Point3D(pt.x, pt.y, z_min) for pt in poly) + bound_faces.append(Face3D(pts3d)) + return Face3D.merge_faces_to_holes(bound_faces, tolerance)
+ +
[docs] @staticmethod + def generate_alignment_axes(room_2ds, distance, direction=Vector2D(0, 1), + angle_tolerance=1.0): + """Get suggested LineSegment2Ds for the Room2D.align method. + + This method will return the most common axes across the input Room2D + geometry along with the number of Room2D segments that correspond to + each axis. The latter can be used to filter the suggested alignment axes + to get only the most common ones across the input Room2Ds. + + Args: + room_2ds: A list of Room2D objects for which common axes will be evaluated. + distance: A number for the distance that will be used in the alignment + operation. This will be used to determine the resolution at which + alignment axes are generated and evaluated. Smaller alignment + distances will result in the generation of more common_axes since + a finer resolution can differentiate common that would typically be + grouped together. For typical building geometry, an alignment distance + of 0.3 meters or 1 foot is typically suitable for eliminating + unwanted details while not changing the geometry too much from + its original location. + direction: A Vector2D object to represent the direction in which the + common axes will be evaluated and generated. + angle_tolerance: The max angle difference in radians that the Room2D + segment direction can differ from the input direction before the + segments are not factored into this calculation of common axes. + + Returns: + A tuple with two elements. + + - common_axes: A list of LineSegment2D objects for the common + axes across the input Room2Ds. + + - axis_values: A list of integers that aligns with the common_axes + and denotes how many segments of the input Room2D each axis + relates to. Higher numbers indicate that that the axis is more + commonly aligned across the Room2Ds. + """ + # process the inputs + min_distance, merge_distance = distance / 3, distance + ang_tol = math.radians(angle_tolerance) + polygons = [] + for room in room_2ds: + polygons.append(room.floor_geometry.boundary_polygon2d) + if room.floor_geometry.has_holes: + for hole in room.floor_geometry.hole_polygon2d: + polygons.append(hole) + # return the common axes and values + return Polygon2D.common_axes( + polygons, direction, min_distance, merge_distance, ang_tol)
+ +
[docs] @staticmethod + def floor_segment_by_index(geometry, segment_index): + """Get a particular LineSegment3D from a Face3D object. + + The logic applied by this method to select the segment is the same that is + used to assign lists of values to the floor_geometry (eg. boundary conditions). + + Args: + geometry: A Face3D representing floor geometry. + segment_index: An integer for the index of the segment to return. + """ + segs = geometry.boundary_segments if geometry.holes is \ + None else geometry.boundary_segments + \ + tuple(seg for hole in geometry.hole_segments for seg in hole) + return segs[segment_index]
+ + def _room_volume_with_roof(self, roof_spec, tolerance): + """Get a Polyface3D for the Room volume given a roof_spec above the room. + + Args: + roof_spec: A Dragonfly RoofSpecification that describes the Roof + above the room geometry. + tolerance: The minimum distance from roof polygon edges at which a + point is considered to lie on the edge. + + Returns: + A tuple with the two items below. + + * room_polyface -- A Polyface3D object for the Room volume. This will + be None whenever the Room has no Roof geometries above it or there + are gaps or overlaps in the Roof geometries above the room. + + * roof_face_i -- A list of integers for the indices of the faces in + the Polyface3D that correspond to the roof. Will be None whenever + the roof is not successfully applied to the Room. + + * shade_geometry -- An list of Face3D for roof geometries that overlapped + with the Room2D but could not be easily incorporated into the Room + volume. Typical examples include roof geometries representing dormers + or roof elements that projected out and overhung into the Room2D + geometry. These should be translated to Room-assigned shades. Will + be None whenever the roof is not successfully applied to the Room. + """ + # get the roof polygons and the bounding Room2D polygon + roof_polys = roof_spec.boundary_geometry_2d + roof_planes = roof_spec.planes + room_poly = Polygon2D( + [Point2D(pt.x, pt.y) for pt in self.floor_geometry.boundary]) + + # gather all of the relevant roof polygons for the Room2D + rel_rf_polys, rel_rf_planes, is_full_bound = [], [], False + for rf_py, rf_pl in zip(roof_polys, roof_planes): + poly_rel = rf_py.polygon_relationship(room_poly, tolerance) + if poly_rel >= 0: + rel_rf_polys.append(rf_py) + rel_rf_planes.append(rf_pl) + if poly_rel == 1: # simple solution of one roof + is_full_bound = True + rel_rf_polys = [rel_rf_polys[-1]] + rel_rf_planes = [rel_rf_planes[-1]] + break + + # make the room volume + p_faces = [self.floor_geometry.flip()] # a list of Room volume faces + proj_dir = Vector3D(0, 0, 1) # direction to project onto Roof planes + + # when fully bounded, simply project the segments onto the single Roof face + if is_full_bound: + roof_plane = rel_rf_planes[0] + roof_verts = [] + for seg in self.floor_segments: + p1, p2 = seg.p1, seg.p2 + p3 = roof_plane.project_point(p2, proj_dir) + p4 = roof_plane.project_point(p1, proj_dir) + p_faces.append(Face3D((p1, p2, p3, p4))) + roof_verts.append(p4) + if not self.floor_geometry.has_holes: + p_faces.append(Face3D(roof_verts)) + else: + v_count = len(self.floor_geometry.boundary) + part_roof_verts = [roof_verts[:v_count]] + for hole in self.floor_geometry.holes: + part_roof_verts.append(roof_verts[v_count:v_count + len(hole)]) + v_count += len(hole) + p_faces.append(Face3D(part_roof_verts[0], holes=part_roof_verts[1:])) + return Polyface3D.from_faces(p_faces, tolerance), [-1], [] + + # when multiple roofs, each segment must be intersected with the roof polygons + # gather polygons that account for all of the Room2D holes + all_room_poly = [room_poly] + flr_segs = self.floor_segments + if self.floor_geometry.has_holes: + v_count = len(room_poly) + all_segments = [flr_segs[:v_count]] + for hole in self.floor_geometry.holes: + hole_poly = Polygon2D([Point2D(pt.x, pt.y) for pt in hole]) + all_room_poly.append(hole_poly) + all_segments.append(flr_segs[v_count:v_count + len(hole)]) + v_count += len(hole) + else: + all_segments = [flr_segs] + + # create the walls from the segments by intersecting them with the roof + walls = self._wall_faces_with_roof( + all_room_poly, all_segments, rel_rf_polys, rel_rf_planes, tolerance) + if walls is None: # invalid roof geometry + return None, None, None + p_faces.extend(walls) + + # add the roof faces using polygon boolean operations + roof_faces = self._roof_faces( + all_room_poly, rel_rf_polys, rel_rf_planes, tolerance) + if roof_faces is None: # invalid roof geometry + return None, None, None + roof_face_i = list(range(-1, -len(roof_faces) - 1, -1)) + p_faces.extend(roof_faces) + + # create the Polyface3D and try to repair it if it is not solid + room_polyface = Polyface3D.from_faces(p_faces, tolerance) + shade_geometry = [] + ang_tol = math.radians(1) + + if not room_polyface.is_solid: + # make sure that overlapping edges are merged so we don't get false readings + room_polyface = room_polyface.merge_overlapping_edges(tolerance, ang_tol) + + if not room_polyface.is_solid: + # try to patch any vertical gaps between roofs with new walls + room_polyface, roof_face_i = \ + self._patch_vertical_gaps(room_polyface, roof_face_i, tolerance) + if not room_polyface.is_solid: + room_polyface = room_polyface.merge_overlapping_edges(tolerance, ang_tol) + + if not room_polyface.is_solid: + # remove disconnected roof geometries from the Polyface (eg. dormers) + room_polyface, roof_face_i, shade_geometry = \ + self._separate_disconnected_faces(room_polyface, roof_face_i, tolerance) + if not room_polyface.is_solid: + room_polyface = room_polyface.merge_overlapping_edges(tolerance, ang_tol) + + return room_polyface, roof_face_i, shade_geometry + + def _wall_faces_with_roof(self, all_room_poly, all_segments, + rel_rf_polys, rel_rf_planes, tolerance): + """Generate Face3D for the Room Walls when there are multiple Roof Polygons. + + Args: + all_room_poly: A list of Polygon2D where each polygon represents either + the boundary of the room or a hole. + all_segments: A list of lists where each sub-list contains LineSegment2D + objects for each polygon in all_room_poly. + rel_rf_polys: A list of Polygon2D for the Roof geometries that are + relevant to the Room2D. + rel_rf_planes: A list of Plane objects for each Roof geometry that + is relevant to the Room2D. + tolerance: The distance value for absolute tolerance. + + Returns: + A list of Face3D for the Walls of the Room. Will be None if the Roof + geometries are invalid. + """ + wall_faces = [] + proj_dir = Vector3D(0, 0, 1) # direction to project onto Roof planes + + # loop through holes and boundary polygons and generate walls from them + for rm_poly, rm_segs in zip(all_room_poly, all_segments): + # find the polygon that the first room vertex is located in + current_poly, current_plane = None, None + other_poly, other_planes = rel_rf_polys[:], rel_rf_planes[:] # copy lists + pt1 = rm_poly[0] + for i, (rf_py, rf_pl) in enumerate(zip(rel_rf_polys, rel_rf_planes)): + if rf_py.point_relationship(pt1, tolerance) >= 0: + current_poly, current_plane = rf_py, rf_pl + other_poly.pop(i) + other_planes.pop(i) + break + if current_poly is None: # first point not inside a roof, invalid roof + return None + + # loop through segments and add vertices if they cross outside the roof face + rot_poly = rm_poly.vertices[1:] + (pt1,) + for pt2, seg in zip(rot_poly, rm_segs): + face_pts = [seg.p1, seg.p2] + # see if the segment ends in the same face it starts in + if current_poly.point_relationship(pt2, tolerance) >= 0: # project seg + face_pts.append(current_plane.project_point(seg.p2, proj_dir)) + face_pts.append(current_plane.project_point(seg.p1, proj_dir)) + else: + int_pts, int_pls = [(seg.p1, 0)], [current_plane] + # find where the segment leaves the polygon + seg_2d = LineSegment2D.from_array(((pt1.x, pt1.y), (pt2.x, pt2.y))) + for rf_seg in current_poly.segments: + int_pt = seg_2d.intersect_line_ray(rf_seg) + if int_pt is None: + dist, cls_pts = closest_point2d_between_line2d( + seg_2d, rf_seg) + if dist <= tolerance: + int_pt = cls_pts[0] + if int_pt is not None: + int_pts.append((int_pt, 0)) + int_pls.append(current_plane) + # find where it intersects the other relevant polygons + for o_poly, o_pl in zip(other_poly, other_planes): + for o_seg in o_poly.segments: + int_pt = seg_2d.intersect_line_ray(o_seg) + if int_pt is None: + d, cls_pts = closest_point2d_between_line2d( + seg_2d, o_seg) + if d <= tolerance: + int_pt = cls_pts[0] + if int_pt is not None: + int_pts.append((int_pt, 1)) + int_pls.append(o_pl) + # sort the intersections points along the segment + pt_dists = [(ipt[1], seg_2d.p1.distance_to_point(ipt[0])) + for ipt in int_pts] + pts_pls = [(i_pt[0], i_pl) for i_pt, i_pl in zip(int_pts, int_pls)] + sort_obj = sorted(zip(pt_dists, pts_pls), key=lambda pair: pair[0]) + sort_pts_pls = [x for _, x in sort_obj] + # if two points are equivalent, reorder with the previous point plane + ord_pts = [x[0] for x in sort_pts_pls] + ord_pls = [x[1] for x in sort_pts_pls] + for i, (pt, pln) in enumerate(sort_pts_pls[1:]): + if pt.distance_to_point(ord_pts[i]) < tolerance: + prev_pl = ord_pls[i - 1] + if pln == prev_pl and ord_pls[i] != prev_pl: # reorder + ord_pts[i], ord_pts[i + 1] = ord_pts[i + 1], ord_pts[i] + ord_pls[i], ord_pls[i + 1] = ord_pls[i + 1], ord_pls[i] + # project the points onto the planes + rf_pts = [ipl.project_point(Point3D.from_point2d(ipt), proj_dir) + for ipt, ipl in zip(ord_pts, ord_pls)] + # add a vertex for where the segment ends in the polygon + for i, (rf_py, rf_pl) in enumerate(zip(other_poly, other_planes)): + if rf_py.point_relationship(pt2, tolerance) >= 0: + other_poly.pop(i) + other_poly.append(current_poly) + other_planes.pop(i) + other_planes.append(current_plane) + current_poly, current_plane = rf_py, rf_pl + rf_pts.append( + rf_pl.project_point(Point3D.from_point2d(pt2), proj_dir)) + break + # remove duplicated vertices from the list + rf_pts = [pt for i, pt in enumerate(rf_pts) + if not pt.is_equivalent(rf_pts[i - 1], tolerance)] + if current_poly is None or len(rf_pts) < 2: + return None # point not inside a roof; invalid roof + # check that the first two vertices are not a sliver + if abs(rf_pts[0].x - rf_pts[1].x) < tolerance and \ + abs(rf_pts[0].y - rf_pts[1].y) < tolerance: + rf_pts.pop(0) + # add the points to the Face3D vertices + rf_pts.reverse() + face_pts.extend(rf_pts) + # make the final Face3D + if len(face_pts) == 2: # second point not inside a roof, invalid roof + return None + wall_faces.append(Face3D(face_pts)) + pt1 = pt2 # increment for next segment + + return wall_faces + + def _roof_faces(self, all_room_poly, rel_rf_polys, rel_rf_planes, tolerance): + """Generate Face3D for the Room Roofs when there are multiple Roof Polygons. + + Args: + all_room_poly: A list of Polygon2D where each polygon represents either + the boundary of the room or a hole. + rel_rf_polys: A list of Polygon2D for the Roof geometries that are + relevant to the Room2D. + rel_rf_planes: A list of Plane objects for each Roof geometry that + is relevant to the Room2D. + tolerance: The distance value for absolute tolerance. + + Returns: + A list of Face3D for the Roofs of the Room. Will be None if the Roof + geometries are invalid. + """ + roof_faces = [] + proj_dir = Vector3D(0, 0, 1) # direction to project onto Roof planes + + # create a BooleanPolygon for the Room2D + room_polys = [] + for rom_poly in all_room_poly: + rom_poly = rom_poly.remove_colinear_vertices(tolerance) + room_polys.append((pb.BooleanPoint(pt.x, pt.y) for pt in rom_poly.vertices)) + b_room_poly = pb.BooleanPolygon(room_polys) + # find the boolean intersection with each roof polygon and project the result + int_tol = tolerance / 100 # intersection tolerance must be finer + + for rf_poly, rf_plane in zip(rel_rf_polys, rel_rf_planes): + # snap the polygons to one another to avoid tolerance issues + rf_poly = rf_poly.remove_colinear_vertices(tolerance) + for rom_poly in all_room_poly: + rf_poly = rom_poly.snap_to_polygon(rf_poly, tolerance) + rf_pts = (pb.BooleanPoint(pt.x, pt.y) for pt in rf_poly.vertices) + b_rf_poly = pb.BooleanPolygon([rf_pts]) + try: + int_result = pb.intersect(b_room_poly, b_rf_poly, int_tol) + except Exception: # intersection failed for some reason + return None + polys = [Polygon2D(tuple(Point2D(pt.x, pt.y) for pt in new_poly)) + for new_poly in int_result.regions] + if self.floor_geometry.has_holes and len(polys) > 1: + # sort the polygons by area and check if any are inside the others + polys.sort(key=lambda x: x.area, reverse=True) + poly_groups = [[polys[0]]] + for sub_poly in polys[1:]: + for i, pg in enumerate(poly_groups): + if pg[0].is_polygon_inside(sub_poly): # it's a hole + poly_groups[i].append(sub_poly) + break + else: # it's a separate Face3D + poly_groups.append([sub_poly]) + # convert all vertices to 3D and append the roof Face3D + for pg in poly_groups: + pg_3d = [] + for shp in pg: + pt3s = tuple( + rf_plane.project_point(Point3D.from_point2d(pt2), proj_dir) + for pt2 in shp.vertices) + pg_3d.append(pt3s) + roof_faces.append(Face3D(pg_3d[0], rf_plane, holes=pg_3d[1:])) + else: # no holes are possible in the result; project all polygons directly + for sub_poly in polys: + pt3s = tuple( + rf_plane.project_point(Point3D.from_point2d(pt2), proj_dir) + for pt2 in sub_poly.vertices) + roof_faces.append(Face3D(pt3s, rf_plane)) + + return roof_faces + + def _patch_vertical_gaps(self, room_polyface, roof_face_i, tolerance): + """Patch any vertical gaps in a room_polyface. + + Args: + room_polyface: The non-solid Polyface3D to be patched with planar + vertical Faces. + roof_face_i: The indices of the polyface that correspond to the roof. + tolerance: The distance value for absolute tolerance. + + Returns: + The patched Room Polyface3D followed by an updated list of face indices + that should become Roofs. + """ + # get the faces and naked edges + p_faces = list(room_polyface.faces) + edges = [ed for ed in room_polyface.naked_edges + if not ed.is_vertical(tolerance)] + vertical_faces = [] + + # loop through the naked edges and try to match them + matched_segs = set() + edge_indices = list(range(len(edges))) + for i, edge_1 in enumerate(edges): + edge_1_2d = LineSegment2D.from_end_points( + Point2D(edge_1.p1.x, edge_1.p1.y), Point2D(edge_1.p2.x, edge_1.p2.y)) + other_edges = edges[:i] + edges[i + 1:] + other_is = edge_indices[:i] + edge_indices[i + 1:] + for oi, edge_2 in zip(other_is, other_edges): + e2p1 = Point2D(edge_2.p1.x, edge_2.p1.y) + e2p2 = Point2D(edge_2.p2.x, edge_2.p2.y) + if edge_1_2d.distance_to_point(e2p1) <= tolerance and \ + edge_1_2d.distance_to_point(e2p2) <= tolerance: + # check to be sure that the segments have not been aired already + edge_pair_1, edge_pair_2 = (i, oi), (oi, i) + if edge_pair_1 in matched_segs: + continue + matched_segs.add(edge_pair_1) + matched_segs.add(edge_pair_2) + # build the points of the vertical face + norm = Vector3D(edge_1.v.x, edge_1.v.y, 0) + int_pl_1 = Plane(n=norm, o=edge_2.p1) + int_pl_2 = Plane(n=norm, o=edge_2.p2) + edge_1_1 = intersect_line3d_plane_infinite(edge_1, int_pl_1) + edge_1_2 = intersect_line3d_plane_infinite(edge_1, int_pl_2) + new_face3d = Face3D((edge_1_1, edge_1_2, edge_2.p1, edge_2.p2)) + # find the grouping of points that is not self intersecting + if not new_face3d.is_self_intersecting and \ + new_face3d.area > tolerance: + vertical_faces.append(new_face3d) + else: + new_face3d = Face3D((edge_1_1, edge_1_2, edge_2.p2, edge_2.p1)) + if not new_face3d.is_self_intersecting and \ + new_face3d.area > tolerance: + vertical_faces.append(new_face3d) + else: + f_poly = new_face3d.polygon2d + fs1, fs2 = f_poly.segments[0], f_poly.segments[2] + int_pt2d = fs1.intersect_line_ray(fs2) + if int_pt2d is not None: + int_pt = new_face3d.plane.xy_to_xyz(int_pt2d) + new_face3d1 = Face3D((edge_2.p1, int_pt, edge_1_1)) + new_face3d2 = Face3D((edge_2.p2, int_pt, edge_1_2)) + if new_face3d1.area > tolerance: + vertical_faces.append(new_face3d1) + if new_face3d2.area > tolerance: + vertical_faces.append(new_face3d2) + + # remove duplicated vertices in the resulting vertical faces + clean_vert_faces = [] + for f in vertical_faces: + try: + clean_vert_faces.append(f.remove_duplicate_vertices(tolerance)) + except AssertionError: + pass # invalid sliver face + + # rebuild the room polyface + st_v = -len(roof_face_i) - 1 + roof_face_i = roof_face_i + list(range(st_v, st_v - len(clean_vert_faces), -1)) + p_faces.extend(clean_vert_faces) + room_polyface = Polyface3D.from_faces(p_faces, tolerance) + return room_polyface, roof_face_i + + def _separate_disconnected_faces(self, room_polyface, roof_face_i, tolerance): + """Separate Face3Ds from a room_polyface, with are not connected to the solid. + + Args: + room_polyface: The non-solid Polyface3D for which disconnected faces + will be separated out. + roof_face_i: The indices of the polyface that correspond to the roof. + tolerance: The distance value for absolute tolerance. + + Returns: + The new Room Polyface3D, followed by an updated list of roof face indices, + followed by a list of shade geometries. + """ + # remove disconnected roof geometries from the Polyface (eg. dormers) + shade_geometry, room_ind, shade_ind = [], [], [] + edge_i, edge_t = room_polyface.edge_indices, room_polyface.edge_types + for f_ind, face in enumerate(room_polyface.face_indices): + fe_types = [] + for fi in face: + for i, vi in enumerate(fi): + try: + ind = edge_i.index((vi, fi[i - 1])) + et = edge_t[ind] + except ValueError: # make sure reversed edge isn't there + try: + ind = edge_i.index((fi[i - 1], vi)) + et = edge_t[ind] + except ValueError: # an edge that was merged in overlapping + et = 1 + fe_types.append(et) + if sum(fe_types) <= 1: # disconnected face found! + shade_ind.append(f_ind) + else: + room_ind.append(f_ind) + if len(shade_ind) != 0: # rebuild the Polyface3D + p_faces = [room_polyface.faces[f_ind] for f_ind in room_ind] + shade_geometry = [room_polyface.faces[f_ind] for f_ind in shade_ind] + room_polyface = Polyface3D.from_faces(p_faces, tolerance) + new_roof_len = len(roof_face_i) - len(shade_ind) + roof_face_i = list(range(-1, -new_roof_len - 1, -1)) + return room_polyface, roof_face_i, shade_geometry + + def _honeybee_plenums(self, hb_room, tolerance=0.01): + """Get ceiling and/or floor plenums for the Room2D as a Honeybee Room. + + This method will check if there is a gap between the Room2D's ceiling and + floor, and the top and bottom of it's corresponding Story, respectively. + If there is a gap along the z axis larger then the specified tolerance, + it will compute the necessary ceiling and/or floor plenum to fill the gap. + + Args: + hb_room: A honeybee Room representing the dragonfly Room2D. + tolerance: The minimum distance in z values to check if the Room ceiling + and floor is adjacent to the upper and lower floor of the Story, + respectively. If not adjacent, the corresponding ceiling or floor + plenum is generated. Default: 0.01, suitable for objects in meters. + + Returns: + A list of Honeybee Rooms with two items: + + * ceil_plenum -- A honeybee-core Room representing the ceiling + plenum. If there isn't enough space between the Story + floor_to_floor_height and the Room2D floor_to_ceiling height, + this item will be None. + + * floor_plenum -- A honeybee-core Room representing the floor plenum. + If there isn't enough space between the Story floor_height and + the Room2D floor_height, this item will be None. + """ + # check to be sure that the room2d has a parent story + hb_rooms = [] + if not self.has_parent: + raise AttributeError( + 'Cannot add plenums to the "{}" Room because the parent Story has ' + 'not been set. This is required to derive the plenum ' + 'height.'.format(self.identifier)) + + parent = self.parent + parent_ceiling = parent.floor_height + parent.floor_to_floor_height + ceil_plenum_height = parent_ceiling - self.ceiling_height + floor_plenum_height = self.floor_height - parent.floor_height + + if ceil_plenum_height > tolerance: + ceil_plenum = self._honeybee_plenum( + ceil_plenum_height, plenum_type="ceiling") + # Set the plenum and the rooms to be adjacent to one another + hb_room[-1].set_adjacency(ceil_plenum[0], tolerance) + hb_rooms.append(ceil_plenum) + + if floor_plenum_height > tolerance: + floor_plenum = self._honeybee_plenum( + floor_plenum_height, plenum_type="floor") + # Set the plenum and the rooms to be adjacent to one another + hb_room[0].set_adjacency(floor_plenum[-1], tolerance) + try: + hb_room[0].boundary_condition = bcs.adiabatic + except AttributeError: + pass + hb_rooms.append(floor_plenum) + + return hb_rooms + + def _honeybee_plenum(self, plenum_height, plenum_type='ceiling'): + """Get a ceiling or floor plenum for the Room2D as a Honeybee Room. + + The boundary condition for all plenum faces is adiabatic except for the + ceiling and floor surfaces between the room, and any outdoor walls. + + Args: + hb_room: A honeybee Room representing the dragonfly Room2D. + plenum_height: The height of the plenum Room. + plenum_type: Text for the type of plenum to be constructed. + Choose from the following: + + * ceiling + * floor + + Returns: + A honeybee Room representing a plenum zone. + """ + plenum_id = self.identifier + '_{}_plenum'.format(plenum_type) + + # create reference 2d geometry for plenums + ref_face3d = self.floor_geometry.duplicate() + if plenum_type == 'ceiling': + ref_face3d = ref_face3d.move(Vector3D(0, 0, self.floor_to_ceiling_height)) + else: + ref_face3d = ref_face3d.move(Vector3D(0, 0, -plenum_height)) + + # create the honeybee Room + plenum_hb_room = Room.from_polyface3d( + plenum_id, Polyface3D.from_offset_face(ref_face3d, plenum_height)) + + # get the boundary condition that will be used for interior surfaces + try: + interior_bc = bcs.adiabatic + except AttributeError: # honeybee_energy is not loaded; no Adiabatic BC + interior_bc = bcs.outdoors + + # assign wall BCs based on self + for i, bc in enumerate(self._boundary_conditions): + if not isinstance(bc, Surface): + plenum_hb_room[i + 1].boundary_condition = bc + else: # assign boundary conditions for the roof and floor + plenum_hb_room[i + 1].boundary_condition = interior_bc + + if plenum_type == 'ceiling': # assign ceiling BCs + if self._is_top_exposed: + plenum_hb_room[-1].boundary_condition = bcs.outdoors + else: + plenum_hb_room[-1].boundary_condition = interior_bc + else: # assign floor BCss + if self._is_ground_contact: + plenum_hb_room[0].boundary_condition = bcs.ground + else: + plenum_hb_room[0].boundary_condition = interior_bc + + return plenum_hb_room + + def _check_wall_assigned_object(self, value, obj_name=''): + """Check an input that gets assigned to all of the walls of the Room.""" + try: + value = list(value) if not isinstance(value, list) else value + except (ValueError, TypeError): + raise TypeError('Input {} must be a list or a tuple'.format(obj_name)) + assert len(value) == len(self), 'Input {} length must be the ' \ + 'same as the number of floor_segments. {} != {}'.format( + obj_name, len(value), len(self)) + return value + + @staticmethod + def _flip_wall_assigned_objects(original_geo, bcs, win_pars, shd_pars): + """Get arrays of wall-assigned parameters that are flipped/reversed. + + This method accounts for the case that a floor geometry has holes in it. + """ + # go through the boundary and ensure detailed parameters are flipped + new_bcs = [] + new_win_pars = [] + new_shd_pars = [] + for i, seg in enumerate(original_geo.boundary_segments): + new_bcs.append(bcs[i]) + win_par = win_pars[i] + if isinstance(win_par, _AsymmetricBase): + new_win_pars.append(win_par.flip(seg.length)) + else: + new_win_pars.append(win_par) + new_shd_pars.append(shd_pars[i]) + + # reverse the lists of wall-assigned objects on the floor boundary + new_bcs.reverse() + new_win_pars.reverse() + new_shd_pars.reverse() + + # add any objects related to the holes + if original_geo.has_holes: + bound_len = len(original_geo.boundary) + new_bcs = new_bcs + bcs[bound_len:] + new_win_pars = new_win_pars + win_pars[bound_len:] + new_shd_pars = new_shd_pars + shd_pars[bound_len:] + + # return the flipped lists + return new_bcs, new_win_pars, new_shd_pars + + def _split_walls_along_height(self, hb_room, tolerance, plenums=False): + """Split adjacent walls to ensure matching surface areas in to_honeybee workflow. + + Args: + hb_room: A non-split Honeybee Room representation of this Room2D. + tolerance: The minimum distance in z values of floor_height and + floor_to_ceiling_height at which adjacent Faces will be split. + plenums: A boolean to note whether the resulting model has auto-generated + plenums, which will determine the default boundary condition of + any split wall segments. (Default: False). + """ + new_faces = [hb_room[0]] + for i, bc in enumerate(self._boundary_conditions): + face = hb_room[i + 1] + if not isinstance(bc, Surface): + new_faces.append(face) + else: + try: + adj_rm = self._parent.room_by_identifier( + bc.boundary_condition_objects[-1]) + except ValueError: # missing adjacency in Story; just pass invalid BC + new_faces.append(face) + continue + flr_diff = adj_rm.floor_height - self.floor_height + ciel_diff = self.ceiling_height - adj_rm.ceiling_height + if flr_diff <= tolerance and ciel_diff <= tolerance: + # No need to split the surface along its height + new_faces.append(face) + elif flr_diff > tolerance and ciel_diff > tolerance: + # split the face into to 3 smaller faces along its height + lseg = LineSegment3D.from_end_points(face.geometry[0], + face.geometry[1]) + mid_dist = self.floor_to_ceiling_height - ciel_diff - flr_diff + vec1 = Vector3D(0, 0, flr_diff) + vec2 = Vector3D(0, 0, self.floor_to_ceiling_height - ciel_diff) + below = Face3D.from_extrusion(lseg, vec1) + mid = Face3D.from_extrusion( + lseg.move(vec1), Vector3D(0, 0, mid_dist)) + above = Face3D.from_extrusion( + lseg.move(vec2), Vector3D(0, 0, ciel_diff)) + mid_face = face.duplicate() + mid_face._geometry = mid + self._reassign_split_windows(mid_face, i, tolerance) + below_face = Face('{}_Below'.format(face.identifier), below) + above_face = Face('{}_Above'.format(face.identifier), above) + try: + below_face.boundary_condition = bcs.ground \ + if self.is_ground_contact and not plenums else bcs.adiabatic + except AttributeError: + pass # honeybee_energy is not loaded; no adiabatic BC + try: + below_face.boundary_condition = bcs.outdoors \ + if adj_rm.is_top_exposed and not plenums else bcs.adiabatic + except AttributeError: + pass # honeybee_energy is not loaded; no adiabatic BC + new_faces.extend([below_face, mid_face, above_face]) + elif flr_diff > tolerance: + # split the face into to 2 smaller faces along its height + lseg = LineSegment3D.from_end_points(face.geometry[0], + face.geometry[1]) + mid_dist = self.floor_to_ceiling_height - flr_diff + vec1 = Vector3D(0, 0, flr_diff) + below = Face3D.from_extrusion(lseg, vec1) + mid = Face3D.from_extrusion( + lseg.move(vec1), Vector3D(0, 0, mid_dist)) + mid_face = face.duplicate() + mid_face._geometry = mid + self._reassign_split_windows(mid_face, i, tolerance) + below_face = Face('{}_Below'.format(face.identifier), below) + try: + below_face.boundary_condition = bcs.ground \ + if self.is_ground_contact and not plenums else bcs.adiabatic + except AttributeError: + pass # honeybee_energy is not loaded; no adiabatic BC + new_faces.extend([below_face, mid_face]) + elif ciel_diff > tolerance: + # split the face into to 2 smaller faces along its height + lseg = LineSegment3D.from_end_points(face.geometry[0], + face.geometry[1]) + mid_dist = self.floor_to_ceiling_height - ciel_diff + vec1 = Vector3D(0, 0, mid_dist) + mid = Face3D.from_extrusion(lseg, vec1) + above = Face3D.from_extrusion( + lseg.move(vec1), Vector3D(0, 0, ciel_diff)) + mid_face = face.duplicate() + mid_face._geometry = mid + self._reassign_split_windows(mid_face, i, tolerance) + above_face = Face('{}_Above'.format(face.identifier), above) + try: + above_face.boundary_condition = bcs.outdoors \ + if adj_rm.is_top_exposed and not plenums else bcs.adiabatic + except AttributeError: + pass # honeybee_energy is not loaded; no adiabatic BC + new_faces.extend([mid_face, above_face]) + new_faces.append(hb_room[-1]) + return new_faces + + def _reassign_split_windows(self, face, i, tolerance): + """Re-assign WindowParameters to any base surface that has been split. + + Args: + face: Honeybee Face to which windows will be re-assigned. + i: The index of the window_parameters that correspond to the face + tolerance: The tolerance, which will be used to re-assign windows. + """ + glz_par = self._window_parameters[i] + if glz_par is not None: + face.remove_sub_faces() + glz_par.add_window_to_face(face, tolerance) + + @staticmethod + def _segment_wall_face(room, segment, tolerance): + """Get a Wall Face that corresponds with a certain wall segment. + + Args: + room: A Honeybee Room from which a wall Face will be returned. + segment: A LineSegment3D along one of the walls of the room. + tolerance: The maximum difference between values at which point vertices + are considered to be the same. + """ + for face in room.faces: + if isinstance(face.type, (Wall, AirBoundary)): + fg = face.geometry + try: + verts = fg._remove_colinear( + fg._boundary, fg.boundary_polygon2d, tolerance) + except AssertionError: + return None + for v1 in verts: + if segment.p1.is_equivalent(v1, tolerance): + p2 = segment.p2 + for v2 in verts: + if p2.is_equivalent(v2, tolerance): + return face + + def _match_and_transfer_wall_props(self, new_room, tolerance): + """Transfer wall properties of matching segments between this room and a new one. + + All wall properties are transferred exactly as they are when segments + are perfectly equal between this room and the new room. When segments + are colinear/overlapping but the segment on the new_room is shorter than + that on this room, the wall properties on this room will be split in + order to assign them correctly to the new room. When a given segment + of the new_room is not overlapping/colinear with any segment of this + room, it will be given default properties with an outdoor boundary + condition. + + This all makes this method suitable for preserving properties across + operations that trim or split the original room to make the new_room. + + Args: + new_room: An new Room2D to which wall properties will be transferred. + """ + # get the relevant original segments by copying the lists on this Room2D + rel_segs = self.floor_segments + rel_win = self._window_parameters + rel_shd = self._shading_parameters + rel_abs = self.air_boundaries + rel_bcs = [] + for bc in self._boundary_conditions: + if not isinstance(bc, Surface): + rel_bcs.append(bc) + else: # Surface boundary conditions can mess up window splitting + rel_bcs.append(bcs.outdoors) + # build up new lists of parameters if the segments match + new_bcs, new_win, new_shd, new_abs = {}, {}, {}, {} + for k, seg1 in enumerate(rel_segs): + m_win_segs, m_i = [], [] + for i, seg2 in enumerate(new_room.floor_segments): + if seg1.distance_to_point(seg2.p1) <= tolerance and \ + seg1.distance_to_point(seg2.p2) <= tolerance: # colinear + new_bcs[i] = rel_bcs[k] + new_shd[i] = rel_shd[k] + new_abs[i] = rel_abs[k] + m_win_segs.append(seg2) + m_i.append(i) + # split the window parameters across the matched segments + wp_par_to_split = rel_win[k] + if wp_par_to_split is None: + for i in m_i: + new_win[i] = None + else: + full_len = sum(sg.length for sg in m_win_segs) + if abs(seg1.length - full_len) <= tolerance: # all segments accounted + if len(m_i) == 1: # no change to the segment + new_win[m_i[0]] = wp_par_to_split + else: # windows to be split + split_par = wp_par_to_split.split(m_win_segs, tolerance) + for i, w_par in zip(m_i, split_par): + new_win[i] = w_par + else: # not all segment accounted; trim each window par from original + for i, n_seg in zip(m_i, m_win_segs): + new_win[i] = wp_par_to_split.trim(seg1, n_seg, tolerance) + + # assign the matched properties to the new room + final_bcs, final_win, final_shd, final_abs = [], [], [], [] + for i in range(len(new_room)): + try: + final_bcs.append(new_bcs[i]) + final_win.append(new_win[i]) + final_shd.append(new_shd[i]) + final_abs.append(new_abs[i]) + except KeyError: # segment not matched to any in existing room + final_bcs.append(bcs.outdoors) + final_win.append(None) + final_shd.append(None) + final_abs.append(False) + new_room.boundary_conditions = final_bcs + new_room.window_parameters = final_win + new_room.shading_parameters = final_shd + new_room.air_boundaries = final_abs + + @staticmethod + def _remove_colinear_props( + pts_3d, pts_2d, segs_2d, bound_cds, win_pars, ftc_height, tolerance): + """Remove colinear vertices across a boundary while merging window properties.""" + new_vertices, new_bcs, new_w_par = [], [], [] + skip = 0 # track the number of vertices being skipped/removed + m_segs, m_bcs, m_w_par = [], [], [] + # loop through vertices and remove all cases of colinear verts + for i, _v in enumerate(pts_2d): + m_segs.append(segs_2d[i - 2]) + m_bcs.append(bound_cds[i - 2]) + m_w_par.append(win_pars[i - 2]) + _a = pts_2d[i - 2 - skip].determinant(pts_2d[i - 1]) + \ + pts_2d[i - 1].determinant(_v) + _v.determinant(pts_2d[i - 2 - skip]) + if abs(_a) >= tolerance: # vertex is not colinear; add vertex and merge + new_vertices.append(pts_3d[i - 1]) + if all(not isinstance(bc, Ground) for bc in m_bcs): + new_bcs.append(bcs.outdoors) + if all(wp is None for wp in m_w_par): + new_w_par.append(None) + elif len(m_w_par) == 1: + new_w_par.append(m_w_par[0]) + else: + new_wp = DetailedWindows.merge(m_w_par, m_segs, ftc_height) + new_w_par.append(new_wp) + else: + new_bcs.append(bcs.ground) + new_w_par.append(None) + skip = 0 + m_bcs, m_w_par, m_segs = [], [], [] + else: # vertex is colinear; continue + skip += 1 + # catch case of last two vertices being equal but distinct from first point + if skip != 0 and pts_3d[-2].is_equivalent(pts_3d[-1], tolerance): + _a = pts_2d[-3].determinant(pts_2d[-1]) + \ + pts_2d[-1].determinant(pts_2d[0]) + pts_2d[0].determinant(pts_2d[-3]) + if abs(_a) >= tolerance: + new_vertices.append(pts_3d[-1]) + if not isinstance(bound_cds[-2], Ground): + new_bcs.append(bcs.outdoors) + new_w_par.append(win_pars[-2]) + else: + new_bcs.append(bcs.ground) + new_w_par.append(None) + elif skip != 0: + w_par_for_merge = m_w_par + [new_w_par[0]] + if not all(wp is None for wp in w_par_for_merge): + segs_for_merge = m_segs + [segs_2d[-1]] + new_w_par[0] = DetailedWindows.merge( + w_par_for_merge, segs_for_merge, ftc_height) + # move the first properties to the end to match with the vertices + new_bcs.append(new_bcs.pop(0)) + new_w_par.append(new_w_par.pop(0)) + return new_vertices, new_bcs, new_w_par + + @staticmethod + def _adjacency_grouping(rooms, adj_finding_function): + """Group Room2Ds together according to an adjacency finding function. + + Args: + rooms: A list of Room2Ds to be grouped by their adjacency. + adj_finding_function: A function that denotes which rooms are adjacent + to another. + + Returns: + A list of list with each sub-list containing rooms that share adjacencies. + """ + # create a room lookup table and duplicate the list of rooms + room_lookup = {rm.identifier: rm for rm in rooms} + all_rooms = list(rooms) + adj_network = [] + + # loop through the rooms and find air boundary adjacencies + for room in all_rooms: + adj_ids = adj_finding_function(room) + if len(adj_ids) == 0: # a room that is its own solar enclosure + adj_network.append([room]) + else: # there are other adjacent rooms to find + local_network = [room] + local_ids, first_id = set(adj_ids), room.identifier + while len(adj_ids) != 0: + # add the current rooms to the local network + adj_objs = [room_lookup[rm_id] for rm_id in adj_ids] + local_network.extend(adj_objs) + adj_ids = [] # reset the list of new adjacencies + # find any rooms that are adjacent to the adjacent rooms + for obj in adj_objs: + all_new_ids = adj_finding_function(obj) + new_ids = [rid for rid in all_new_ids + if rid not in local_ids and rid != first_id] + for rm_id in new_ids: + local_ids.add(rm_id) + adj_ids.extend(new_ids) + # after the local network is understood, clean up duplicated rooms + adj_network.append(local_network) + i_to_remove = [i for i, room_obj in enumerate(all_rooms) + if room_obj.identifier in local_ids] + for i in reversed(i_to_remove): + all_rooms.pop(i) + return adj_network + + @staticmethod + def _find_adjacent_rooms(room): + """Find the identifiers of all rooms with adjacency to a room.""" + adj_rooms = [] + for bc in room._boundary_conditions: + if isinstance(bc, Surface): + adj_rooms.append(bc.boundary_condition_objects[-1]) + return adj_rooms + + @staticmethod + def _find_adjacent_air_boundary_rooms(room): + """Find the identifiers of all rooms with air boundary adjacency to a room.""" + adj_rooms = [] + for bc, ab in zip(room._boundary_conditions, room.air_boundaries): + if ab and isinstance(bc, Surface): + adj_rooms.append(bc.boundary_condition_objects[-1]) + return adj_rooms + + @staticmethod + def _segments_along_polygon( + polygon, rel_segs, rel_bcs, rel_win, rel_shd, rel_abs, + new_bcs, new_win, new_shd, new_abs, new_flr_height, tol): + """Find the segments along a polygon and add their properties to new lists.""" + new_segs = [] + for seg in polygon.segments: + seg_segs, seg_bcs, seg_win, seg_shd, seg_abs = [], [], [], [], [] + # collect the room segments and properties along the boundary + for i, rs in enumerate(rel_segs): + if seg.distance_to_point(rs.p1) <= tol and \ + seg.distance_to_point(rs.p2) <= tol: # colinear + seg_segs.append(rs) + seg_bcs.append(rel_bcs[i]) + seg_win.append(rel_win[i]) + seg_shd.append(rel_shd[i]) + seg_abs.append(rel_abs[i]) + if len(seg_segs) == 0: + Room2D._add_dummy_segment( + seg.p1, seg.p2, new_segs, new_bcs, new_win, new_shd, new_abs) + continue + # sort the Room2D segments along the polygon segment + seg_dists = [seg.p1.distance_to_point(s.p1) for s in seg_segs] + sort_ind = [i for _, i in sorted(zip(seg_dists, range(len(seg_dists))))] + seg_segs = [seg_segs[i] for i in sort_ind] + seg_bcs = [seg_bcs[i] for i in sort_ind] + seg_win = [seg_win[i] for i in sort_ind] + seg_shd = [seg_shd[i] for i in sort_ind] + seg_abs = [seg_abs[i] for i in sort_ind] + # identify any gaps and add dummy segments + p1_dists = sorted(seg_dists) + p2_dists = [seg.p1.distance_to_point(s.p2) for s in seg_segs] + last_d, last_seg = 0, None + for i, (p1d, p2d) in enumerate(zip(p1_dists, p2_dists)): + if p1d < last_d - tol: # overlapping segment; ignore it + continue + elif p1d > last_d + tol: # add a dummy segment for the gap + st_pt = last_seg.p2 if last_seg is not None else seg.p1 + Room2D._add_dummy_segment( + st_pt, seg_segs[i].p1, new_segs, new_bcs, + new_win, new_shd, new_abs) + # add the segment + new_segs.append(seg_segs[i]) + new_bcs.append(seg_bcs[i]) + new_win.append(seg_win[i]) + new_shd.append(seg_shd[i]) + new_abs.append(seg_abs[i]) + last_d = p2d + last_seg = seg_segs[i] + return [Point3D(s.p1.x, s.p1.y, new_flr_height) for s in new_segs] + + @staticmethod + def _add_dummy_segment(p1, p2, new_segs, new_bcs, new_win, new_shd, new_abs): + """Add a dummy segment to lists of properties that are being built.""" + new_segs.append(LineSegment2D.from_end_points(p1, p2)) + new_bcs.append(bcs.outdoors) + new_win.append(None) + new_shd.append(None) + new_abs.append(False) + + @staticmethod + def _intersect_line2d_infinite(line_ray_a, line_ray_b): + """Get the intersection between a Ray2Ds extended infinitely. + + Args: + line_ray_a: A Ray2D object. + line_ray_b: Another Ray2D object. + + Returns: + Point2D of intersection if it exists. None if lines are parallel. + """ + d = line_ray_b.v.y * line_ray_a.v.x - line_ray_b.v.x * line_ray_a.v.y + if d == 0: + return None + dy = line_ray_a.p.y - line_ray_b.p.y + dx = line_ray_a.p.x - line_ray_b.p.x + ua = (line_ray_b.v.x * dy - line_ray_b.v.y * dx) / d + return Point2D(line_ray_a.p.x + ua * line_ray_a.v.x, + line_ray_a.p.y + ua * line_ray_a.v.y) + + def __copy__(self): + new_r = Room2D(self.identifier, self._floor_geometry, + self.floor_to_ceiling_height, + self._boundary_conditions[:]) # copy boundary condition list + new_r._display_name = self._display_name + new_r._user_data = None if self.user_data is None else self.user_data.copy() + new_r._parent = self._parent + new_wp = [] + for wp in self._window_parameters: + nwp = wp.duplicate() if wp is not None else None + new_wp.append(nwp) + new_r._window_parameters = new_wp + new_r._shading_parameters = self._shading_parameters[:] # copy shading list + new_r._air_boundaries = self._air_boundaries[:] \ + if self._air_boundaries is not None else None + new_r._is_ground_contact = self._is_ground_contact + new_r._is_top_exposed = self._is_top_exposed + new_r._skylight_parameters = self._skylight_parameters.duplicate() \ + if self._skylight_parameters is not None else None + new_r._abridged_properties = self._abridged_properties + new_r._properties._duplicate_extension_attr(self._properties) + return new_r + + def __len__(self): + return self._segment_count + + def __getitem__(self, key): + return self.floor_segments[key] + + def __iter__(self): + return iter(self.floor_segments) + + def __repr__(self): + return 'Room2D: %s' % self.display_name
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/shadingparameter.html b/docs/_modules/dragonfly/shadingparameter.html new file mode 100644 index 00000000..4eb88fba --- /dev/null +++ b/docs/_modules/dragonfly/shadingparameter.html @@ -0,0 +1,1551 @@ + + + + + + + dragonfly.shadingparameter — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.shadingparameter

+# coding: utf-8
+"""Shading Parameters with instructions for generating shades."""
+from __future__ import division
+
+from honeybee.typing import float_in_range, float_positive, int_positive
+
+from ladybug_geometry.geometry2d.pointvector import Vector2D
+
+
+class _ShadingParameterBase(object):
+    """Base object for all shading parameters.
+
+    This object records all of the methods that must be overwritten on a shading
+    parameter object for it to be successfully be applied in dragonfly workflows.
+    """
+    __slots__ = ()
+
+    def __init__(self):
+        pass
+
+    def add_shading_to_face(self, face, tolerance=0.01):
+        """Add Shades to a Honeybee Face using these Window Parameters."""
+        pass
+
+    def scale(self, factor):
+        """Get a scaled version of these ShadingParameters.
+
+        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
+
+    @classmethod
+    def from_dict(cls, data):
+        """Create ShadingParameterBase from a dictionary.
+
+        .. code-block:: python
+
+            {
+            "type": "ShadingParameterBase"
+            }
+        """
+        assert data['type'] == 'ShadingParameterBase', \
+            'Expected ShadingParameterBase dictionary. Got {}.'.format(data['type'])
+        return cls()
+
+    def to_dict(self):
+        """Get ShadingParameterBase as a dictionary."""
+        return {'type': 'ShadingParameterBase'}
+
+    def duplicate(self):
+        """Get a copy of this object."""
+        return self.__copy__()
+
+    def ToString(self):
+        return self.__repr__()
+
+    def __copy__(self):
+        return _ShadingParameterBase()
+
+    def __repr__(self):
+        return 'ShadingParameterBase'
+
+
+
[docs]class ExtrudedBorder(_ShadingParameterBase): + """Instructions for extruded borders over all windows in the wall. + + Args: + depth: A number for the depth of the border. + + Properties: + * depth + """ + __slots__ = ('_depth',) + + def __init__(self, depth): + """Instructions for extruded borders over all windows in the wall.""" + self._depth = float_positive(depth, 'overhang width') + + @property + def depth(self): + """Get a number for the depth of the border.""" + return self._depth + +
[docs] def add_shading_to_face(self, face, tolerance=0.01): + """Add Shades to a Honeybee Face using these Shading Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: An optional value to return None if the overhang has a length less + than the tolerance. Default: 0.01, suitable for objects in meters. + """ + if self.depth != 0: + for ap in face.apertures: + ap.extruded_border(self.depth)
+ +
[docs] def scale(self, factor): + """Get a scaled version of these ShadingParameters. + + This method is called within the scale methods of the Room2D. + + Args: + factor: A number representing how much the object should be scaled. + """ + return ExtrudedBorder(self.depth * factor)
+ +
[docs] @classmethod + def from_dict(cls, data): + """Create ExtrudedBorder from a dictionary. + + .. code-block:: json + + { + "type": "ExtrudedBorder", + "depth": 0.5 + } + """ + assert data['type'] == 'ExtrudedBorder', \ + 'Expected ExtrudedBorder dictionary. Got {}.'.format(data['type']) + return cls(data['depth'])
+ +
[docs] def to_dict(self): + """Get ExtrudedBorder as a dictionary.""" + return {'type': 'ExtrudedBorder', + 'depth': self.depth}
+ + def __copy__(self): + return ExtrudedBorder(self.depth) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return self.depth + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, ExtrudedBorder) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'ExtrudedBorder:\n depth: {}'.format(self.depth)
+ + +
[docs]class Overhang(_ShadingParameterBase): + """Instructions for a single overhang over an entire wall. + + Args: + depth: A number for the overhang depth. + angle: A number for the for an angle to rotate the overhang in degrees. + Positive values indicate a downward rotation. Negative values indicate + an upward rotation. Default is 0 for no rotation. + + Properties: + * depth + * angle + """ + __slots__ = ('_depth', '_angle') + + def __init__(self, depth, angle=0): + """Instructions for a single overhang over an entire wall.""" + self._depth = float_positive(depth, 'overhang width') + self._angle = float_in_range(angle, -90, 90, 'overhang angle') + + @property + def depth(self): + """Get a number for the overhang depth.""" + return self._depth + + @property + def angle(self): + """Get a number for the overhang angle.""" + return self._angle + +
[docs] def add_shading_to_face(self, face, tolerance=0.01): + """Add Shades to a Honeybee Face using these Shading Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: An optional value to return None if the overhang has a length less + than the tolerance. Default: 0.01, suitable for objects in meters. + """ + if self.depth != 0: + face.overhang(self.depth, self.angle, False, tolerance)
+ +
[docs] def scale(self, factor): + """Get a scaled version of these ShadingParameters. + + This method is called within the scale methods of the Room2D. + + Args: + factor: A number representing how much the object should be scaled. + """ + return Overhang(self.depth * factor, self.angle)
+ +
[docs] @classmethod + def from_dict(cls, data): + """Create Overhang from a dictionary. + + .. code-block:: python + + { + "type": "Overhang", + "depth": 1.5, + "angle": 0 + } + """ + assert data['type'] == 'Overhang', \ + 'Expected Overhang dictionary. Got {}.'.format(data['type']) + angle = data['angle'] if 'angle' in data else 0 + return cls(data['depth'], angle)
+ +
[docs] def to_dict(self): + """Get Overhang as a dictionary.""" + return {'type': 'Overhang', + 'depth': self.depth, + 'angle': self.angle}
+ + def __copy__(self): + return Overhang(self.depth, self.angle) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.depth, self.angle) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, Overhang) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'Overhang:\n depth: {}\n angle: {}'.format(self.depth, self.angle)
+ + +class _LouversBase(_ShadingParameterBase): + """Instructions for a series of louvered Shades over a Face. + + Args: + depth: A number for the depth to extrude the louvers. + offset: A number for the distance to louvers from this Face. + Default is 0 for no offset. + angle: A number for the for an angle to rotate the louvers in degrees. + Default is 0 for no rotation. + contour_vector: A Vector2D for the direction along which contours + are generated. This 2D vector will be interpreted into a 3D vector + within the plane of this Face. (0, 1) will usually generate + horizontal contours in 3D space, (1, 0) will generate vertical + contours, and (1, 1) will generate diagonal contours. Default: (0, 1). + flip_start_side: Boolean to note whether the side the louvers start from + should be flipped. Default is False to have contours on top or right. + Setting to True will start contours on the bottom or left. + + Properties: + * depth + * offset + * angle + * contour_vector + * flip_start_side + """ + __slots__ = ('_depth', '_offset', '_angle', '_contour_vector', '_flip_start_side') + + def __init__(self, depth, offset=0, angle=0, contour_vector=Vector2D(0, 1), + flip_start_side=False): + """Initialize LouversBase.""" + self._depth = float_positive(depth, 'louver depth') + self._offset = float_positive(offset, 'louver offset') + self._angle = float_in_range(angle, -90, 90, 'overhang angle') + assert isinstance(contour_vector, Vector2D), 'Expected Vector2D for ' \ + 'LouversByDistance contour_vector. Got {}.'.format(type(contour_vector)) + self._contour_vector = contour_vector + self._flip_start_side = bool(flip_start_side) + + @property + def depth(self): + """Get a number for the depth to extrude the louvers.""" + return self._depth + + @property + def offset(self): + """Get a number for the distance to louvers from this Face.""" + return self._offset + + @property + def angle(self): + """Get a number for an angle to rotate the louvers in degrees.""" + return self._angle + + @property + def contour_vector(self): + """Get a Vector2D for the direction along which contours are generated.""" + return self._contour_vector + + @property + def flip_start_side(self): + """Get a boolean to note whether the side the louvers start from is flipped.""" + return self._flip_start_side + + @staticmethod + def _default_dict_parameters(data): + """Get defaulted parameters from a base dictionary.""" + offset = data['offset'] if 'offset' in data else 0 + angle = data['angle'] if 'angle' in data else 0 + contr = Vector2D.from_array(data['contour_vector']) if 'contour_vector' in data \ + else Vector2D(0, 1) + flip = data['flip_start_side'] if 'flip_start_side' in data else False + return offset, angle, contr, flip + + def __copy__(self): + return _LouversBase(self.depth, self.offset, self.angle, + self.contour_vector, self.flip_start_side) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.depth, self.offset, self.angle, + self.contour_vector, self.flip_start_side) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, _LouversBase) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'LouversBase:' + + +
[docs]class LouversByDistance(_LouversBase): + """Instructions for a series of louvered Shades at a given distance between. + + Args: + distance: A number for the approximate distance between each louver. + depth: A number for the depth to extrude the louvers. + offset: A number for the distance to louvers from the wall. + Default is 0 for no offset. + angle: A number for the for an angle to rotate the louvers in degrees. + Default is 0 for no rotation. + contour_vector: A Vector2D for the direction along which contours + are generated. This 2D vector will be interpreted into a 3D vector + within the plane of the wall. (0, 1) will usually generate + horizontal contours in 3D space, (1, 0) will generate vertical + contours, and (1, 1) will generate diagonal contours. Default: (0, 1). + flip_start_side: Boolean to note whether the side the louvers start from + should be flipped. Default is False to have contours on top or right. + Setting to True will start contours on the bottom or left. + + Properties: + * distance + * depth + * offset + * angle + * contour_vector + * flip_start_side + """ + __slots__ = ('_distance',) + + def __init__(self, distance, depth, offset=0, angle=0, + contour_vector=Vector2D(0, 1), flip_start_side=False): + """Initialize LouversByDistance.""" + self._distance = float_positive(distance, 'louver separation distance') + _LouversBase.__init__(self, depth, offset, angle, + contour_vector, flip_start_side) + + @property + def distance(self): + """Get a number for the approximate distance between each louver.""" + return self._distance + +
[docs] def add_shading_to_face(self, face, tolerance=0.01): + """Add Shades to a Honeybee Face using these Shading Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: An optional value to remove any louvers with a length less + than the tolerance. Default: 0.01, suitable for objects in meters. + """ + if self.depth != 0: + face.louvers_by_distance_between( + self.distance, self.depth, self.offset, self.angle, self.contour_vector, + self.flip_start_side, False, tolerance)
+ +
[docs] def scale(self, factor): + """Get a scaled version of these ShadingParameters. + + This method is called within the scale methods of the Room2D. + + Args: + factor: A number representing how much the object should be scaled. + """ + return LouversByDistance( + self.distance * factor, self.depth * factor, self.offset * factor, + self.angle, self.contour_vector, self.flip_start_side)
+ +
[docs] @classmethod + def from_dict(cls, data): + """Create LouversByDistance from a dictionary. + + .. code-block:: python + + { + "type": "LouversByDistance", + "distance": 0.3, + "depth": 0.1, + "offset": 0.3, + "angle": 0, + "contour_vector": [0, 1], # array of (x, y) values + "flip_start_side": False + } + """ + assert data['type'] == 'LouversByDistance', \ + 'Expected LouversByDistance dictionary. Got {}.'.format(data['type']) + offset, angle, contr, flip = cls._default_dict_parameters(data) + return cls(data['distance'], data['depth'], offset, angle, contr, flip)
+ +
[docs] def to_dict(self): + """Get LouversByDistance as a dictionary.""" + return {'type': 'LouversByDistance', + 'distance': self.distance, + 'depth': self.depth, + 'offset': self.offset, + 'angle': self.angle, + 'contour_vector': self.contour_vector.to_array(), + 'flip_start_side': self.flip_start_side}
+ + def __copy__(self): + return LouversByDistance(self.distance, self.depth, self.offset, self.angle, + self.contour_vector, self.flip_start_side) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.distance, self.depth, self.offset, self.angle, + self.contour_vector, self.flip_start_side) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, LouversByDistance) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'LouversByDistance:\n distance: {}\n depth: {}\n offset: {}\n angle: {}' \ + '\n contour: {}\n flip: {}'.format( + self.distance, self.depth, self.offset, self.angle, + self.contour_vector, self.flip_start_side)
+ + +
[docs]class LouversByCount(_LouversBase): + """Instructions for a specific number of louvered Shades over a wall. + + Args: + louver_count: A positive integer for the number of louvers to generate. + depth: A number for the depth to extrude the louvers. + offset: A number for the distance to louvers from the wall. + Default is 0 for no offset. + angle: A number for the for an angle to rotate the louvers in degrees. + Default is 0 for no rotation. + contour_vector: A Vector2D for the direction along which contours + are generated. This 2D vector will be interpreted into a 3D vector + within the plane of the wall. (0, 1) will usually generate + horizontal contours in 3D space, (1, 0) will generate vertical + contours, and (1, 1) will generate diagonal contours. Default: (0, 1). + flip_start_side: Boolean to note whether the side the louvers start from + should be flipped. Default is False to have contours on top or right. + Setting to True will start contours on the bottom or left. + + Properties: + * louver_count + * depth + * offset + * angle + * contour_vector + * flip_start_side + """ + __slots__ = ('_louver_count',) + + def __init__(self, louver_count, depth, offset=0, angle=0, + contour_vector=Vector2D(0, 1), flip_start_side=False): + """Initialize LouversByCount.""" + self._louver_count = int_positive(louver_count, 'louver count') + _LouversBase.__init__(self, depth, offset, angle, + contour_vector, flip_start_side) + + @property + def louver_count(self): + """Get a integer for the number of louvers to generate.""" + return self._louver_count + +
[docs] def add_shading_to_face(self, face, tolerance=0.01): + """Add Shades to a Honeybee Face using these Shading Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: An optional value to remove any louvers with a length less + than the tolerance. Default: 0.01, suitable for objects in meters. + """ + if self.depth != 0: + face.louvers_by_count( + self.louver_count, self.depth, self.offset, self.angle, + self.contour_vector, self.flip_start_side, False, tolerance)
+ +
[docs] def scale(self, factor): + """Get a scaled version of these ShadingParameters. + + This method is called within the scale methods of the Room2D. + + Args: + factor: A number representing how much the object should be scaled. + """ + return LouversByCount( + self.louver_count, self.depth * factor, self.offset * factor, + self.angle, self.contour_vector, self.flip_start_side)
+ +
[docs] @classmethod + def from_dict(cls, data): + """Create LouversByCount from a dictionary. + + .. code-block:: python + + { + "type": "LouversByCount", + "louver_count": 10, + "depth": 0.1, + "offset": 0.3, + "angle": 0, + "contour_vector": [0, 1], # array of (x, y) values + "flip_start_side": False + } + """ + assert data['type'] == 'LouversByCount', \ + 'Expected LouversByCount dictionary. Got {}.'.format(data['type']) + offset, angle, contr, flip = cls._default_dict_parameters(data) + return cls(data['louver_count'], data['depth'], offset, angle, contr, flip)
+ +
[docs] def to_dict(self): + """Get LouversByCount as a dictionary.""" + return {'type': 'LouversByCount', + 'louver_count': self.louver_count, + 'depth': self.depth, + 'offset': self.offset, + 'angle': self.angle, + 'contour_vector': self.contour_vector.to_array(), + 'flip_start_side': self.flip_start_side}
+ + def __copy__(self): + return LouversByCount(self.louver_count, self.depth, self.offset, self.angle, + self.contour_vector, self.flip_start_side) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.louver_count, self.depth, self.offset, self.angle, + self.contour_vector, self.flip_start_side) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, LouversByCount) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'LouversByCount:\n count: {}\n depth: {}\n offset: {}\n angle: {}' \ + '\n contour: {}\n flip: {}'.format( + self.louver_count, self.depth, self.offset, self.angle, + self.contour_vector, self.flip_start_side)
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/skylightparameter.html b/docs/_modules/dragonfly/skylightparameter.html new file mode 100644 index 00000000..374004fd --- /dev/null +++ b/docs/_modules/dragonfly/skylightparameter.html @@ -0,0 +1,1850 @@ + + + + + + + dragonfly.skylightparameter — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

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: + new_geo = polygon.remove_colinear_vertices(tolerance) + if new_geo.is_self_intersecting: + 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 + p_dir = Vector3D(0, 0, 1) + for i, (polygon, isd) in enumerate(zip(self.polygons, self.are_doors)): + pt_in_poly = polygon.center if polygon.is_convex else \ + polygon.pole_of_inaccessibility(tolerance) + if not self._is_sub_point(pt_in_poly, parent_poly, parent_holes): + continue + pt3d = tuple( + face.geometry.plane.project_point(Point3D(p.x, p.y, 0), p_dir) + for p in polygon) + s_geo = Face3D(pt3d) + if isd: + sub_f = Door('{}_Door{}'.format(face.identifier, i + 1), s_geo) + face.add_door(sub_f) + else: + sub_f = Aperture('{}_Glz{}'.format(face.identifier, i + 1), s_geo) + face.add_aperture(sub_f)
+ +
[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_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 face + 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] @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 _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))
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/story.html b/docs/_modules/dragonfly/story.html new file mode 100644 index 00000000..3c4ec31d --- /dev/null +++ b/docs/_modules/dragonfly/story.html @@ -0,0 +1,2717 @@ + + + + + + + dragonfly.story — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.story

+# coding: utf-8
+"""Dragonfly Story."""
+from __future__ import division
+import math
+
+from ladybug_geometry.geometry2d import Vector2D, Polygon2D
+from ladybug_geometry.geometry3d import Vector3D, Ray3D, Polyline3D, Face3D, Polyface3D
+
+from honeybee.typing import float_positive, int_in_range, clean_string, \
+    invalid_dict_error
+from honeybee.boundarycondition import boundary_conditions as bcs
+from honeybee.boundarycondition import Outdoors, Surface
+from honeybee.facetype import AirBoundary
+from honeybee.facetype import face_types as ftyp
+from honeybee.altnumber import autocalculate
+from honeybee.shade import Shade
+from honeybee.room import Room
+
+from ._base import _BaseGeometry
+from .room2d import Room2D
+from .roof import RoofSpecification
+from .windowparameter import DetailedWindows
+from .properties import StoryProperties
+import dragonfly.writer.story as writer
+
+
+
[docs]class Story(_BaseGeometry): + """A Story of a building defined by an extruded Room2Ds. + + Args: + identifier: Text string for a unique Story ID. Must be < 100 characters + and not contain any spaces or special characters. + room_2ds: An array of dragonfly Room2D objects that together form an + entire story of a building. + floor_to_floor_height: A number for the distance from the floor plate of + this Story to the floor of the story above this one (if it exists). + This should be in the same units system as the input room_2d geometry. + If None, this value will be the maximum floor_to_ceiling_height of the + input room_2ds plus any difference between the Story floor height + and the room floor heights. (Default: None) + floor_height: A number for the absolute floor height of the Story. + If None, this will be the minimum floor height of all the Story's + room_2ds, which is suitable for cases where there are no floor + plenums. (Default: None). + multiplier: An integer that denotes the number of times that this + Story is repeated over the height of the building. (Default: 1). + roof: An optional RoofSpecification object containing geometry and instructions + for generating sloped roofs over a Story. The RoofSpecification will only + affect the child Room2Ds that have a True is_top_exposed property + and it will only be utilized in translation to Honeybee when the Story + multiplier is 1. If None, all Room2D ceilings will be flat. (Default: None). + + Properties: + * identifier + * display_name + * full_id + * room_2ds + * floor_to_floor_height + * multiplier + * roof + * parent + * has_parent + * floor_height + * floor_area + * exterior_wall_area + * exterior_aperture_area + * volume + * is_above_ground + * min + * max + * user_data + """ + __slots__ = ('_room_2ds', '_floor_to_floor_height', '_floor_height', + '_multiplier', '_roof', '_parent') + + def __init__(self, identifier, room_2ds, floor_to_floor_height=None, + floor_height=None, multiplier=1, roof=None): + """A Story of a building defined by an extruded Floor2Ds.""" + _BaseGeometry.__init__(self, identifier) # process the identifier + + # process the Room2Ds and story geometry + self.room_2ds = room_2ds + + # process the input properties + self.floor_height = floor_height + self.floor_to_floor_height = floor_to_floor_height + self.multiplier = multiplier + self.roof = roof + + self._parent = None # _parent will be set when Story is added to a Building + self._properties = StoryProperties(self) # properties for extensions + +
[docs] @classmethod + def from_dict(cls, data, tolerance=0): + """Initialize a Story from a dictionary. + + Args: + data: A dictionary representation of a Story object. + tolerance: The maximum difference between z values at which point vertices + are considered to be in the same horizontal plane. This is used to check + that all vertices of the input floor_geometry lie in the same horizontal + floor plane. Default is 0, which will not perform any check. + """ + # check the type of dictionary + assert data['type'] == 'Story', 'Expected Story dictionary. ' \ + 'Got {}.'.format(data['type']) + + # serialize the rooms + rooms = [] + for r_dict in data['room_2ds']: + try: + rooms.append(Room2D.from_dict(r_dict, tolerance)) + except Exception as e: + invalid_dict_error(r_dict, e) + + # check if any room boundaries were reversed + dict_pts = [tuple(room['floor_boundary'][0]) for room in data['room_2ds']] + room_pts = [(rm.floor_geometry[0].x, rm.floor_geometry[0].y) for rm in rooms] + not_reversed = [dpt == rpt for dpt, rpt in zip(dict_pts, room_pts)] + + # ensure Surface boundary conditions are correct if floors were reversed + if not all(not_reversed): # some room floors have been reversed + bcs_to_update = [] + for not_revd, room in zip(not_reversed, rooms): + if not not_revd: # double negative! reversed room boundary + for i, bc in enumerate(room._boundary_conditions): + if isinstance(bc, Surface): # segment must be updated + newid = '{}..Face{}'.format(room.identifier, i + 1) + bc_room = bc.boundary_condition_objects[1] + bc_f_i = bc.boundary_condition_objects[0].split('..Face')[-1] + bc_tup = (bc_room, int(bc_f_i) - 1, (newid, room.identifier)) + bcs_to_update.append(bc_tup) + for bc_tup in bcs_to_update: # update any reversed boundary conditions + adj_room = bc_tup[0] + for not_revd, room in zip(not_reversed, rooms): # find adjacent room + if room.identifier == adj_room: + bc_to_update = room._boundary_conditions[bc_tup[1]] if not_revd \ + else room._boundary_conditions[-1 - bc_tup[1]] + bc_to_update._boundary_condition_objects = bc_tup[2] + + # process the floor_to_floor_height and the multiplier + f2fh = data['floor_to_floor_height'] if 'floor_to_floor_height' in data \ + and data['floor_to_floor_height'] != autocalculate.to_dict() else None + fh = data['floor_height'] if 'floor_height' in data \ + and data['floor_height'] != autocalculate.to_dict() else None + mult = data['multiplier'] if 'multiplier' in data else 1 + + # process the roof specification if it exists + roof = RoofSpecification.from_dict(data['roof']) if 'roof' in data \ + and data['roof'] is not None else None + + # create the story object + story = Story(data['identifier'], rooms, f2fh, fh, mult, roof) + if 'display_name' in data and data['display_name'] is not None: + story._display_name = data['display_name'] + if 'user_data' in data and data['user_data'] is not None: + story.user_data = data['user_data'] + + # load any extension attributes assigned to the story + if data['properties']['type'] == 'StoryProperties': + story.properties._load_extension_attr_from_dict(data['properties']) + return story
+ +
[docs] @classmethod + def from_honeybee(cls, identifier, rooms, tolerance): + """Initialize a Story from a list of Honeybee Rooms. + + Args: + identifier: Text string for a unique Story ID. Must be < 100 characters + and not contain any spaces or special characters. + rooms: A list of Honeybee Room objects. + tolerance: The maximum difference between values at which point vertices + are considered to be the same. + """ + # create the Room2Ds from the Honeybee Rooms + room_2ds = [] + for hb_room in rooms: + if not hb_room.exclude_floor_area: + try: + room_2ds.append(Room2D.from_honeybee(hb_room, tolerance)) + except Exception: # invalid Honeybee Room that is not a closed solid + msg = 'Room "{}" is not a closed solid and cannot be converted to ' \ + 'a Room2D.\nTry using the "ExtrudedOnly" option to convert ' \ + 'the Honeybee Model to Dragonfly'.format(hb_room.display_name) + raise ValueError(msg) + + room_2ds = [room for room in room_2ds if room is not None] + # re-set the adjacencies in relation to the Room2D segments + room_2ds = cls._reset_adjacencies_from_honeybee(room_2ds, tolerance) + return cls(identifier, room_2ds)
+ + @staticmethod + def _reset_adjacencies_from_honeybee(room_2ds, tolerance): + """Re-set the adjacencies in relation to the Room2D segments. + + It is customary to run this method after converting the Story from Honeybee. + This will ensure that any Surface boundary conditions from the Honeybee + translation survive the translation process to the Dragonfly conventions + of Surface boundary conditions. + + Args: + room_2ds: A list of Room2Ds of the same Story for which adjacencies + will be reset. + tolerance: The maximum difference between values at which point vertices + are considered to be the same. + """ + all_adj_faces = [[x for x, bc in enumerate(room_1._boundary_conditions) + if isinstance(bc, Surface)] for room_1 in room_2ds] + for i, room_1 in enumerate(room_2ds): + try: + for x, room_2 in enumerate(room_2ds[i + 1:]): + if not Polygon2D.overlapping_bounding_rect( + room_1._floor_geometry.boundary_polygon2d, + room_2._floor_geometry.boundary_polygon2d, tolerance): + continue # no overlap in bounding rect; adjacency impossible + for j, seg_1 in enumerate(room_1.floor_segments_2d): + for k, seg_2 in enumerate(room_2.floor_segments_2d): + if isinstance(room_2._boundary_conditions[k], Surface): + if seg_1.distance_to_point(seg_2.p1) <= tolerance and \ + seg_1.distance_to_point(seg_2.p2) <= tolerance: + if abs(seg_1.length - seg_2.length) <= tolerance: + # set the boundary conditions of the segments + room_1.set_adjacency(room_2, j, k) + try: + adj_f_1 = all_adj_faces[i] + adj_f_2 = all_adj_faces[i + x + 1] + adj_f_1.pop(adj_f_1.index(j)) + adj_f_2.pop(adj_f_2.index(k)) + except ValueError: + pass # from honeybee broke adjacency + break + except IndexError: + pass # we have reached the end of the list of zones + # set any adjacencies to default that were not set + try: + default_adj_bc = bcs.adiabatic + remove_win = True + except AttributeError: + default_adj_bc = bcs.outdoors + remove_win = False + for r_i, adj_faces in enumerate(all_adj_faces): + for seg_i in adj_faces: + room_2ds[r_i]._boundary_conditions[seg_i] = default_adj_bc + room_2ds[r_i]._air_boundaries[seg_i] = False + if remove_win: + room_2ds[r_i]._window_parameters[seg_i] = None + return room_2ds + + @property + def room_2ds(self): + """Get or set a tuple of Room2D objects that form the Story.""" + return self._room_2ds + + @room_2ds.setter + def room_2ds(self, value): + if not isinstance(value, tuple): + value = tuple(value) + assert len(value) > 0, 'Story must have at least one Room2D.' + for room in value: + assert isinstance(room, Room2D), \ + 'Expected dragonfly Room2D. Got {}'.format(type(room)) + room._parent = self + self._room_2ds = value + if not self.room_2d_story_geometry_valid(value): + # prepare to give an exception message + msg1 = 'Story "{}" has Room floor elevations that are too different from ' \ + 'one another to be a part of the same Story.'.format(self.display_name) + msg2t = 'The following Rooms have an elevation {} the others:\n{}' + # determine if are more offending rooms above or below average floor height + flr_hts = [rm.floor_height for rm in self.room_2ds] + rm_ids = [rm.display_name for rm in self.room_2ds] + flr_hts, rm_ids = zip(*sorted(zip(flr_hts, rm_ids))) + min_flr_to_ceil = min([rm.floor_to_ceiling_height for rm in self.room_2ds]) + rms_below = [] + for flr_ht, rid in zip(flr_hts, rm_ids): + if flr_hts[-1] - flr_ht > min_flr_to_ceil: + rms_below.append(rid) + else: + break + rms_above = [] + for flr_ht, rid in zip(reversed(flr_hts), reversed(rm_ids)): + if flr_ht - flr_hts[0] > min_flr_to_ceil: + rms_above.append(rid) + else: + break + msg2 = msg2t.format('below', '\n'.join(rms_below)) \ + if len(rms_below) < len(rms_above) else \ + msg2t.format('above', '\n'.join(rms_above)) + msg = '{}\n{}'.format(msg1, msg2) + raise ValueError(msg) + + @property + def floor_to_floor_height(self): + """Get or set a number for the distance from this floor plate to the next one.""" + return self._floor_to_floor_height + + @floor_to_floor_height.setter + def floor_to_floor_height(self, value): + if value is None: + ciel_hgt = max([room.ceiling_height for room in self._room_2ds]) + value = ciel_hgt - self.floor_height + self._floor_to_floor_height = float_positive(value, 'floor-to-floor height') + + @property + def floor_height(self): + """Get or set a number for the absolute floor height of the Story. + + This will be the minimum floor height of all the Story's room_2ds unless + specified otherwise. + """ + return self._floor_height + + @floor_height.setter + def floor_height(self, value): + if value is None: + value = min([room.floor_height for room in self._room_2ds]) + self._floor_height = float(value) + + @property + def multiplier(self): + """Get or set an integer noting how many times this Story is repeated. + + Multipliers are used to speed up the calculation when similar Stories are + repeated more than once. Essentially, a given simulation with the + Story is run once and then the result is multiplied by the multiplier. + This comes with some inaccuracy. However, this error might not be too large + if the Stories are similar enough and it can often be worth it since it can + greatly speed up the calculation. + + For more information on multipliers in EnergyPlus see EnergyPlus Tips and Tricks: + https://bigladdersoftware.com/epx/docs/9-1/tips-and-tricks-using-energyplus/\ +using-multipliers-zone-and-or-window.html + """ + return self._multiplier + + @multiplier.setter + def multiplier(self, value): + self._multiplier = int_in_range(value, 1, input_name='room multiplier') + + @property + def roof(self): + """Get or set a RoofSpecification with instructions for generating sloped roofs. + + The RoofSpecification will only affect the child Room2Ds that have a True + is_top_exposed property and it will only be utilized in translation to + Honeybee when the Story multiplier is 1. + """ + return self._roof + + @roof.setter + def roof(self, value): + if value is not None: + assert isinstance(value, RoofSpecification), \ + 'Expected dragonfly RoofSpecification. Got {}'.format(type(value)) + value._parent = self + self._roof = value + + @property + def parent(self): + """Parent Building if assigned. None if not assigned.""" + return self._parent + + @property + def has_parent(self): + """Boolean noting whether this Story has a parent Building.""" + return self._parent is not None + + @property + def floor_area(self): + """Get a number for the total floor area in the Story. + + Note that this property is for one Story and does NOT use the multiplier. + However, if this Story is assigned to a parent Building with room_3ds, + it will include the floor area of these 3D Rooms (without the room multiplier). + """ + flr_area = sum([room.floor_area for room in self._room_2ds]) + if self.has_parent and self.parent.has_room_3ds: + for r in self.parent.room_3ds_by_story(self.display_name): + if not r.exclude_floor_area: + flr_area += r.floor_area + return flr_area + + @property + def exterior_wall_area(self): + """Get a number for the total exterior wall area in the Story. + + Note that this property is for one story and does NOT use the multiplier. + However, if this Story is assigned to a parent Building with room_3ds, + it will include the wall area of these 3D Rooms (without the room multiplier). + """ + ewa = sum([room.exterior_wall_area for room in self._room_2ds]) + if self.has_parent and self.parent.has_room_3ds: + for r in self.parent.room_3ds_by_story(self.display_name): + ewa += r.exterior_wall_area + return ewa + + @property + def exterior_aperture_area(self): + """Get a number for the total exterior aperture area in the Story. + + Note that this property is for one story and does NOT use the multiplier. + However, if this Story is assigned to a parent Building with room_3ds, + it will include the exterior wall aperture area of these 3D Rooms (without + the room multiplier). + """ + eaa = sum([room.exterior_aperture_area for room in self._room_2ds]) + if self.has_parent and self.parent.has_room_3ds: + for r in self.parent.room_3ds_by_story(self.display_name): + eaa += r.exterior_wall_aperture_area + return eaa + + @property + def volume(self): + """Get a number for the volume of all the Rooms in the Story. + + Note that this property is for one story and does NOT use the multiplier. + However, if this Story is assigned to a parent Building with room_3ds, + it will include the volume of these 3D Rooms (without the room multiplier). + """ + vol = sum([room.volume for room in self._room_2ds]) + if self.has_parent and self.parent.has_room_3ds: + for r in self.parent.room_3ds_by_story(self.display_name): + vol += r.volume + return vol + + @property + def is_above_ground(self): + """Get a boolean to note if this Story is above the ground. + + The story is considered above the ground if at least one of its Room2Ds + has an outdoor boundary condition for its walls. + """ + for room in self._room_2ds: + for bc in room._boundary_conditions: + if isinstance(bc, Outdoors): + return True + return False + + @property + def min(self): + """Get a Point2D for the min bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this Story is in proximity + to others. + """ + return self._calculate_min(self._room_2ds) + + @property + def max(self): + """Get a Point2D for the max bounding rectangle vertex in the XY plane. + + This is useful in calculations to determine if this Story is in proximity + to others. + """ + return self._calculate_max(self._room_2ds) + +
[docs] def floor_geometry(self, tolerance=0.01): + """Get a ladybug_geometry Polyface3D object representing the floor plate. + + Args: + tolerance: The minimum distance between points at which they are + not considered touching. Default: 0.01, suitable for objects + in meters. + """ + story_height = self.floor_height + room_floors = [] + for room in self.room_2ds: + diff = story_height - room.floor_height + if abs(diff) <= tolerance: + room_floors.append(room.floor_geometry) + else: + room_floors.append(room.floor_geometry.move(Vector3D(0, 0, diff))) + # TODO: consider returning a list of polyfaces if input rooms are disjointed + return Polyface3D.from_faces(room_floors, tolerance)
+ +
[docs] def outline_segments(self, tolerance=0.01): + """Get a list of LineSegment3D objects for the outline of the floor plate. + + Note that these segments include both the boundary surrounding the floor + and any holes for courtyards that exist within the floor. + + Args: + tolerance: The minimum distance between points at which they are + not considered touching. Default: 0.01, suitable for objects + in meters. + """ + return self.floor_geometry(tolerance).naked_edges
+ +
[docs] def outline_polylines(self, tolerance=0.01): + """Get a list of Polyline3D objects for the outline of the floor plate. + + Note that these segments include both the boundary surrounding the floor + and any holes for courtyards that exist within the floor. + + Args: + tolerance: The minimum distance between points at which they are + not considered touching. Default: 0.01, suitable for objects + in meters. + """ + return Polyline3D.join_segments(self.outline_segments(tolerance), tolerance)
+ +
[docs] def footprint(self, tolerance=0.01): + """Get a list of Face3D objects for the minimum floor plate representation. + + Args: + tolerance: The minimum distance between points at which they are + not considered touching. (Default: 0.01, suitable for objects + in meters). + """ + plines = self.outline_polylines(tolerance) + if len(plines) == 1: # can be represented with a single Face3D + return [Face3D(plines[0].vertices[:-1])] + else: # need to separate holes from distinct Face3Ds + faces = [Face3D(pl.vertices[:-1]) for pl in plines] + return Face3D.merge_faces_to_holes(faces, tolerance)
+ +
[docs] def shade_representation(self, cap=False, tolerance=0.01): + """A list of honeybee Shade objects representing the story geometry. + + This accounts for the story multiplier and can be used to account for + this Story's shade in the simulation of another nearby Story. + + Args: + cap: Boolean to note whether the shade representation should be capped + with a top face. Usually, this is not necessary to account for + blocked sun and is only needed when it's important to account for + reflected sun off of roofs. (Default: False). + tolerance: The minimum distance between points at which they are + not considered touching. Default: 0.01, suitable for objects + in meters. + """ + context_shades = [] + extru_vec = Vector3D(0, 0, self.floor_to_floor_height * self.multiplier) + for i, seg in enumerate(self.outline_segments(tolerance)): + try: + extru_geo = Face3D.from_extrusion(seg, extru_vec) + shd_id = '{}_{}'.format(self.identifier, i) + context_shades.append(Shade(shd_id, extru_geo)) + except ZeroDivisionError: + pass # duplicate vertex resulting in a segment of length 0 + if cap: + for i, s in enumerate(self.footprint(tolerance)): + shd_id = '{}_Top_{}'.format(self.identifier, i) + context_shades.append(Shade(shd_id, s.move(extru_vec))) + return context_shades
+ +
[docs] def shade_representation_multiplier(self, exclude_index=0, cap=False, + tolerance=0.01): + """A list of honeybee Shade objects for just the "multiplier" part of the story. + + This includes all of the geometry along the height of the multiplier except + for one of the floors (represented by the exclude_index). This will be an + empty list if the story has a multiplier of 1. + + Args: + exclude_index: An optional index for a story along the multiplier to + be excluded from the shade representation. For example, if 0, + the bottom geometry along the multiplier is excluded. (Default: 0). + cap: Boolean to note whether the shade representation should be capped + with a top face. Usually, this is not necessary to account for + blocked sun and is only needed when it's important to account for + reflected sun off of roofs. (Default: False). + tolerance: The minimum distance between points at which they are + not considered touching. Default: 0.01, suitable for objects + in meters. + """ + if self.multiplier == 1: + return [] + # get the extrusion and moving vectors + ftf, mult = self.floor_to_floor_height, self.multiplier + context_shades, ceil_vecs, extru_vecs = [], [], [] + if exclude_index != 0: # insert vectors for the bottom shade + ceil_vecs.append(Vector3D(0, 0, 0)) + extru_vecs.append(Vector3D(0, 0, ftf * exclude_index)) + if exclude_index < mult: # insert vectors for the top shade + ceil_vecs.append(Vector3D(0, 0, ftf * (exclude_index + 1))) + extru_vecs.append(Vector3D(0, 0, ftf * (mult - exclude_index - 1))) + # loop through the segments and build up the shades + for i, seg in enumerate(self.outline_segments(tolerance)): + for ceil_vec, extru_vec in zip(ceil_vecs, extru_vecs): + seg = seg.move(ceil_vec) + try: + extru_geo = Face3D.from_extrusion(seg, extru_vec) + shd_id = '{}_{}'.format(self.identifier, i) + context_shades.append(Shade(shd_id, extru_geo)) + except ZeroDivisionError: + pass # duplicate vertex resulting in a segment of length 0 + # cap the extrusions if requested + if cap and exclude_index < mult: + full_vec = Vector3D(0, 0, ftf * mult) + for i, s in enumerate(self.footprint(tolerance)): + shd_id = '{}_Top_{}'.format(self.identifier, i) + context_shades.append(Shade(shd_id, s.move(full_vec))) + return context_shades
+ +
[docs] def room_by_identifier(self, room_identifier): + """Get a Room2D from this Story using its identifier. + + Result will be None if the Room2D is not found in the Story. + + Args: + room_identifier: String for the identifier of the Room2D to be + retrieved from this story. + """ + for room in self._room_2ds: + if room.identifier == room_identifier: + return room + else: + raise ValueError('Room2D "{}" was not found in the story "{}"' + '.'.format(room_identifier, self.identifier))
+ +
[docs] def rooms_by_identifier(self, room_identifiers): + """Get a list of Room2D objects in this story given Room2D identifiers. + + Args: + room_identifier: Array of strings for the identifiers of the Room2D + to be retrieved from this Story. + """ + room_2ds = [] + for identifier in room_identifiers: + for room in self._room_2ds: + if room.identifier == identifier: + room_2ds.append(room) + break + else: + raise ValueError('Room2D "{}" was not found in the story ' + '"{}".'.format(identifier, self.identifier)) + return room_2ds
+ +
[docs] def add_prefix(self, prefix): + """Change the identifier and all child Room2D ids by inserting a prefix. + + This is particularly useful in workflows where you duplicate and edit + a starting object and then want to combine it with the original object + into one Model (like making a model of repeated stories) since all objects + within a Model must have unique identifiers. + + This method is used internally to convert from a Story with a multiplier + to fully-detailed Stories with unique identifiers. + + Args: + prefix: Text that will be inserted at the start of this object's + (and child segments') identifier and display_name. It is recommended + that this prefix be short to avoid maxing out the 100 allowable + characters for dragonfly identifiers. + """ + self._identifier = clean_string('{}_{}'.format(prefix, self.identifier)) + if self._display_name is not None: + self.display_name = '{}_{}'.format(prefix, self.display_name) + self.properties.add_prefix(prefix) + for room in self.room_2ds: + room.add_prefix(prefix)
+ +
[docs] def add_room_2d(self, room_2d): + """Add a Room2D to this Story. + + No check will be performed for whether the input room_2d's identifier + matches one in the current Story. + + Args: + room_2d: A Room2D object to be added to this Story. + """ + assert isinstance(room_2d, Room2D), \ + 'Expected dragonfly Room2D. Got {}'.format(type(room_2d)) + room_2d._parent = self + self._room_2ds = self._room_2ds + (room_2d,)
+ +
[docs] def add_room_2ds(self, rooms_2ds, add_duplicate_ids=False): + """Add a list of Room2Ds to this Story with checks for duplicate identifiers. + + Args: + room_2d: A list of Room2D objects to be added to this Story. + add_duplicate_ids: A boolean to note whether added Room2Ds that + have matching identifiers within the current Story should be + ignored (False) or they should be added to the Story creating + an ID collision that can be resolved later (True). (Default: False). + """ + # check to be sure that the input is composed of Room2Ds + for o_room_2d in rooms_2ds: + assert isinstance(o_room_2d, Room2D), \ + 'Expected dragonfly Room2D. Got {}'.format(type(o_room_2d)) + # add the rooms and deal with duplicated IDs appropriately + new_room_2ds = list(self._room_2ds) + if add_duplicate_ids: + for o_room_2d in rooms_2ds: + o_room_2d._parent = self + new_room_2ds.append(o_room_2d) + else: + exist_set = {rm.identifier for rm in self._room_2ds} + for o_room_2d in rooms_2ds: + if o_room_2d.identifier not in exist_set: + o_room_2d._parent = self + new_room_2ds.append(o_room_2d) + # assign the new Room2Ds to this Story + self._room_2ds = tuple(new_room_2ds)
+ +
[docs] def reset_room_2d_boundaries( + self, polygons, identifiers=None, display_names=None, + floor_to_ceiling_heights=None, tolerance=0.01): + """Rebuild the Room2Ds of the Story using boundary Polygons. + + All existing properties of segments along the boundary polygons will be + preserved, including all window geometries. By default, the largest room + that is identified within each of the boundary polygons will determine the + extension properties of the resulting Room2D. + + It is recommended that the Room2Ds be aligned to the boundaries of the + polygon and duplicate vertices be removed before using this method. + + Args: + polygons: A list of ladybug_geometry Polygon2D, which will become + the new boundaries of the Story's Room2Ds. Note that it is + acceptable to include hole polygons in this list and they will + automatically be sensed by their relationship to the other + polygons. + identifiers: An optional list of text that align with the polygons + and will dictate the identifiers of the Story's Rooms. If this + matches an existing Room2D inside of the polygon, the existing + Room2D will be used to set the extension properties of the output + Room2D. If None, the identifier and extension properties of the + output Room2D will be those of the largest Room2D found inside + of the polygon. (Default: None). + display_names: An optional list of text that align with the + polygons and will dictate the display_names of the Story's Rooms. + If None, the display_name will be taken from the + largest existing Room2D inside the polygon or the existing + Room2D matching the identifier above. (Default: None). + floor_to_ceiling_heights: An optional list of numbers that align with the + polygons and will dictate the the floor-to-ceiling heights of the + resulting Room2Ds. If None, it will be the maximum of the Room2Ds + that are found inside each of the polygon, which ensures + that all window geometries are included in the output. If specified + and it is lower than the maximum Room2D height, any detailed + windows will be automatically trimmed to accommodate the new + floor-to-ceiling height. (Default: None). + tolerance: The minimum distance between a vertex and the polygon + boundary at which point the vertex is considered to lie on the + polygon. (Default: 0.01, suitable for objects in meters). + """ + # set defaults for identifiers and display_names + if identifiers is None: + identifiers = [None] * len(polygons) + if display_names is None: + display_names = [None] * len(polygons) + if floor_to_ceiling_heights is None: + floor_to_ceiling_heights = [None] * len(polygons) + # sort the polygons so they can be correctly interpreted as holes + p_areas = [p.area for p in polygons] + sort_ind = [i for _, i in sorted(zip(p_areas, range(len(p_areas))))] + sort_ind.reverse() + sort_poly = [polygons[i] for i in sort_ind] + sort_ids = [identifiers[i] for i in sort_ind] + sort_names = [display_names[i] for i in sort_ind] + sort_ftcs = [floor_to_ceiling_heights[i] for i in sort_ind] + # loop through the polygons and make the Room2Ds + new_room_2ds = [] + skip_i = [] # list to track hole polygons to be skipped + zip_obj = zip(sort_poly, sort_ids, sort_names, sort_ftcs) + for i, (poly, r_id, r_nm, ftc) in enumerate(zip_obj): + if i in skip_i: + continue + holes = [] + for j, o_poly in enumerate(sort_poly[i + 1:]): + if poly.is_polygon_inside(o_poly): + holes.append(o_poly) + skip_i.append(i + j + 1) + new_room = Room2D.join_by_boundary( + self._room_2ds, poly, holes, ftc, r_id, r_nm, tolerance=tolerance) + new_room_2ds.append(new_room) + self._room_2ds = tuple(new_room_2ds)
+ +
[docs] def suggested_alignment_axes( + self, distance, direction=Vector2D(0, 1), angle_tolerance=1.0): + """Get suggested LineSegment2Ds to be used for this Story in the align methods. + + This method will return the most common axes across the Story geometry + along with the number of Room2D segments that correspond to each axis. + The latter can be used to filter the suggested alignment axes to get + only the most common ones across the input Room2Ds. + + Args: + distance: A number for the distance that will be used in the alignment + operation. This will be used to determine the resolution at which + alignment axes are generated and evaluated. Smaller alignment + distances will result in the generation of more common_axes since + a finer resolution can differentiate common that would typically be + grouped together. For typical building geometry, an alignment distance + of 0.3 meters or 1 foot is typically suitable for eliminating + unwanted details while not changing the geometry too much from + its original location. + direction: A Vector2D object to represent the direction in which the + common axes will be evaluated and generated. + angle_tolerance: The max angle difference in radians that the Room2D + segment direction can differ from the input direction before the + segments are not factored into this calculation of common axes. + + Returns: + A tuple with two elements. + + - common_axes: A list of LineSegment2D objects for the common + axes across the input Room2Ds. + + - axis_values: A list of integers that aligns with the common_axes + and denotes how many segments of the input Room2D each axis + relates to. Higher numbers indicate that that the axis is more + commonly aligned across the Room2Ds. + """ + return Room2D.generate_alignment_axes( + self._room_2ds, distance, direction, angle_tolerance)
+ +
[docs] def align_room_2ds(self, line_ray, distance): + """Move Room2D vertices within a given distance of a line to be on that line. + + Note that, when there are small Room2Ds next to the input line_ray, + this method can create degenerate Room2Ds and so it may be wise to run + the delete_degenerate_room_2ds method after running this one. + + Args: + line_ray: A ladybug_geometry Ray2D or LineSegment2D to which the Room2D + vertices will be aligned. Ray2Ds will be interpreted as being infinite + in both directions while LineSegment2Ds will be interpreted as only + existing between two points. + distance: The maximum distance between a vertex and the line_ray where + the vertex will be moved to lie on the line_ray. Vertices beyond + this distance will be left as they are. + """ + for room in self.room_2ds: + room.align(line_ray, distance)
+ +
[docs] def align(self, line_ray, distance, tolerance=0.01): + """Move Room2D and Roof vertices within a distance of a line to be on that line. + + This method differs from the align_room_2ds method in that it will also + align any Roof geometry (if it is present). + + Args: + line_ray: A ladybug_geometry Ray2D or LineSegment2D to which the Room2D + and Roof vertices will be aligned. Ray2Ds will be interpreted as being + infinite in both directions while LineSegment2Ds will be interpreted + as only existing between two points. + distance: The maximum distance between a vertex and the line_ray where + the vertex will be moved to lie on the line_ray. Vertices beyond + this distance will be left as they are. + tolerance: The minimum distance between vertices below which they are + considered co-located. This is used to ensure that the alignment process + does not create new overlaps in the roof geometry. (Default: 0.01, + suitable for objects in meters). + """ + self.align_room_2ds(line_ray, distance) + if self.roof is not None: + self.roof.align(line_ray, distance, tolerance)
+ +
[docs] def remove_room_2d_duplicate_vertices(self, tolerance=0.01, delete_degenerate=False): + """Remove duplicate vertices from all Room2Ds in this Story. + + All properties assigned to the Room2D will be preserved and any changed + Surface boundary conditions will be automatically updated based on the + removed wall segment indices. + + Args: + tolerance: The minimum distance between a vertex and the line it lies + upon at which point the vertex is considered duplicated. Default: 0.01, + suitable for objects in meters). + delete_degenerate: Boolean to note whether degenerate Room2Ds (with floor + geometries that evaluate to less than 3 vertices at the tolerance) + should be deleted from the Story instead of raising a ValueError. + Note that using this option frequently creates invalid missing + adjacencies, requiring the run of reset_adjacencies followed + by re-running solve_adjacency. (Default: False). + + Returns: + A list of all degenerate Room2Ds that were removed if delete_degenerate + is True. Will be None if delete_degenerate is False. + """ + # remove vertices from the rooms and track the removed indices + removed_dict, removed_rooms = {}, None + if delete_degenerate: + new_room_2ds, removed_rooms = [], [] + for room in self.room_2ds: + try: + removed_dict[room.identifier] = \ + room.remove_duplicate_vertices(tolerance) + new_room_2ds.append(room) + except ValueError: # degenerate room found! + removed_rooms.append(room) + assert len(new_room_2ds) > 0, 'All Room2Ds of Story "{}" are '\ + 'degenerate.'.format(self.display_name) + self._room_2ds = tuple(new_room_2ds) + else: + for room in self.room_2ds: + removed_dict[room.identifier] = \ + room.remove_duplicate_vertices(tolerance) + + # go through the rooms and update any changed Surface boundary conditions + if len(self.room_2ds) != 1: + for room in self.room_2ds: + for j, bc in enumerate(room._boundary_conditions): + if isinstance(bc, Surface): + adj_wall, adj_room = bc.boundary_condition_objects + try: + removed_i = removed_dict[adj_room] + except KeyError: # illegal boundary condition; just ignore + continue + if len(removed_i) == 0: # no removed vertices in room + continue + current_i = int(adj_wall.split('..Face')[-1]) - 1 + if removed_i[0] <= current_i: # surface bc to be updated + bef_count = len([k for k in removed_i if k <= current_i]) + new_i = current_i - bef_count + new_bc = Surface( + ('{}..Face{}'.format(adj_room, new_i + 1), adj_room) + ) + room._boundary_conditions[j] = new_bc + return removed_rooms
+ +
[docs] def remove_room_2d_colinear_vertices( + self, tolerance=0.01, preserve_wall_props=True, delete_degenerate=False): + """Automatically remove colinear or duplicate vertices for the Story's Room2Ds. + + Args: + tolerance: The minimum difference between the coordinate values at + which they are considered co-located. Default: 0.01, + suitable for objects in meters. + preserve_wall_props: Boolean to note whether existing window parameters + and Ground boundary conditions should be preserved as vertices are + removed. If False, all boundary conditions are replaced with Outdoors, + all window parameters are erased, and this method will execute quickly. + If True, an attempt will be made to merge window parameters together + across colinear segments, translating simple window parameters to + rectangular ones if necessary. Also, existing Ground boundary + conditions will be kept. (Default: True). + delete_degenerate: Boolean to note whether degenerate Room2Ds (with + floor geometries that evaluate to less than 3 vertices at the + tolerance) should be deleted from the Story instead of raising + a ValueError. (Default: False). + + Returns: + A list of all degenerate Room2Ds that were removed if delete_degenerate + is True. Will be None if delete_degenerate is False. + """ + # remove vertices from the rooms and track the removed indices + if delete_degenerate: + new_room_2ds, removed_rooms = [], [] + for room in self.room_2ds: + try: + new_r = room.remove_colinear_vertices(tolerance, preserve_wall_props) + new_room_2ds.append(new_r) + except ValueError: # degenerate room found! + removed_rooms.append(room) + assert len(new_room_2ds) > 0, 'All Room2Ds of Story "{}" are '\ + 'degenerate.'.format(self.display_name) + self._room_2ds = tuple(new_room_2ds) + return removed_rooms + else: + new_room_2ds = [] + for room in self.room_2ds: + new_room = room.remove_colinear_vertices(tolerance, preserve_wall_props) + new_room_2ds.append(new_room) + self._room_2ds = tuple(new_room_2ds)
+ +
[docs] def remove_room_2d_short_segments(self, distance, angle_tolerance=1.0): + """Remove consecutive short segments on this Story's Room2Ds. + + To patch over the removed segments, an attempt will first be made to find the + intersection of the two neighboring segments. If these two lines are parallel, + they will simply be connected with a segment. + + Properties assigned to the Room2Ds will be preserved for the segments that + are not removed. Room2Ds that have all of their walls shorter than the + distance will be removed from the Story. + + Args: + distance: The maximum length of a segment below which the segment + will be considered for removal. + angle_tolerance: The max angle difference in degrees that vertices + are allowed to differ from one another in order to consider them + colinear. (Default: 1). + + Returns: + A list of all small Room2Ds that were removed. + """ + # remove vertices from the rooms and track the removed indices + new_room_2ds, removed_rooms = [], [] + for room in self.room_2ds: + nr = room.remove_short_segments(distance, angle_tolerance) + if nr is not None: + new_room_2ds.append(nr) + else: + removed_rooms.append(room) + assert len(new_room_2ds) > 0, 'All Room2Ds of Story "{}" are '\ + 'are shorter than the distance {}.'.format(self.display_name, distance) + self._room_2ds = tuple(new_room_2ds) + return removed_rooms
+ +
[docs] def delete_degenerate_room_2ds(self, tolerance=0.01): + """Remove all Room2Ds with a floor_area of zero from this Story. + + This method will also automatically remove any degenerate holes in Room2D + floor geometries, which have an area less than zero. + + Args: + tolerance: The minimum difference between the coordinate values at + which they are considered co-located. Default: 0.01, + suitable for objects in meters. + + Returns: + A list of all degenerate Room2Ds that were removed. + """ + new_room_2ds, removed_rooms = [], [] + for room in self.room_2ds: + max_dim = max((room.max.x - room.min.x, room.max.y - room.min.y)) + if room.floor_geometry.area < max_dim * tolerance: + removed_rooms.append(room) + else: + room.remove_degenerate_holes(tolerance) + new_room_2ds.append(room) + assert len(new_room_2ds) > 0, 'All Room2Ds of Story "{}" are '\ + 'degenerate.'.format(self.display_name) + self._room_2ds = tuple(new_room_2ds) + return removed_rooms
+ +
[docs] def rebuild_detailed_windows( + self, tolerance=0.01, match_adjacency=False, rebuild_skylights=True): + """Rebuild all detailed windows such that they are bounded by their parent walls. + + This method will also ensure that all interior windows on adjacent wall + segments are matched correctly with one another. + + This is useful to run after situations where Room2D vertices have been moved, + which can otherwise disrupt the pattern of detailed windows. + + Args: + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + match_adjacency: A boolean to note whether this method should ensure + that all interior windows on adjacent wall segments are matched + correctly with one another. This is desirable when the existing + adjacencies across the model are correct but it can create several + unwanted cases when the adjacencies are not correct. (Default: False). + rebuild_skylights: A boolean to note whether skylights should be offset + and rebuilt if they lie outside their parent Room2D. + """ + adj_dict = {} + for room in self.room_2ds: + new_w_pars = [] + zip_items = zip( + room._window_parameters, room.floor_segments, room._boundary_conditions) + for i, (w_par, seg, bc) in enumerate(zip_items): + if isinstance(w_par, DetailedWindows): + new_w_par = w_par.adjust_for_segment( + seg, room.floor_to_ceiling_height, tolerance) + if match_adjacency and isinstance(bc, Surface): + try: + adj_seg = bc.boundary_condition_objects[0] + new_w_par = adj_dict[adj_seg].flip(seg.length) + except KeyError: # first of the two adjacencies + this_seg = '{}..Face{}'.format(room.identifier, i + 1) + adj_dict[this_seg] = new_w_par + except AttributeError: # all windows were removed from adjacency + new_w_par = None + else: + new_w_par = w_par + new_w_pars.append(new_w_par) + room._window_parameters = new_w_pars + if rebuild_skylights: + room.offset_skylight_parameters(tolerance * 2, tolerance)
+ +
[docs] def reset_adjacency(self): + """Set all Surface boundary conditions on the Story to be Outdoors.""" + for room in self.room_2ds: + room.reset_adjacency()
+ +
[docs] def intersect_room_2d_adjacency(self, tolerance=0.01): + """Automatically intersect the line segments of the Story's Room2Ds. + + Note that this method effectively erases window parameters and shading + parameters for any intersected segments as the original segments are + subdivided. As such, it is recommended that this method be used before + assigning window or shading parameters. + + Args: + tolerance: The minimum difference between the coordinate values of two + at which they can be considered adjacent. (Default: 0.01, + suitable for objects in meters). + """ + self._room_2ds = Room2D.intersect_adjacency(self._room_2ds, tolerance)
+ +
[docs] def solve_room_2d_adjacency( + self, tolerance=0.01, intersect=False, resolve_window_conflicts=True): + """Automatically solve adjacencies across the Room2Ds in this Story. + + Args: + tolerance: The minimum difference between the coordinate values of two + faces at which they can be considered adjacent. (Default: 0.01, + suitable for objects in meters). + intersect: Boolean to note wether the Room2Ds should be intersected + to obtain matching wall segments before solving adjacency. Note + that setting this to True will result in the loss of windows and + shades assigned to intersected segments. (Default: False). + resolve_window_conflicts: Boolean to note whether conflicts between + window parameters of adjacent segments should be resolved during + adjacency setting or an error should be raised about the mismatch. + Resolving conflicts will default to the window parameters with the + larger are and assign them to the other segment. (Default: True). + """ + if intersect: + self._room_2ds = Room2D.intersect_adjacency(self._room_2ds, tolerance) + Room2D.solve_adjacency(self._room_2ds, tolerance, resolve_window_conflicts)
+ +
[docs] def set_outdoor_window_parameters(self, window_parameter): + """Set all of the outdoor walls to have the same window parameters. + + Args: + window_parameter: A WindowParameter object that will be assigned to + all wall segments of this story's rooms that have an Outdoors + boundary conditions. This can also be None, to remove all + windows from the story. + """ + for room in self._room_2ds: + room.set_outdoor_window_parameters(window_parameter)
+ +
[docs] def set_outdoor_shading_parameters(self, shading_parameter): + """Set all of the outdoor walls to have the same shading parameters. + + Args: + shading_parameter: A ShadingParameter object that will be assigned to + all wall segments of this story's rooms that have an Outdoors + boundary conditions. This can also be None, to remove all + shades from the story. + """ + for room in self._room_2ds: + room.set_outdoor_shading_parameters(shading_parameter)
+ +
[docs] def to_rectangular_windows(self): + """Convert all of the windows of the Story to the RectangularWindows format.""" + for room in self._room_2ds: + room.to_rectangular_windows()
+ +
[docs] def set_ground_contact(self, is_ground_contact=True): + """Set all child Room2Ds of this object to have floors with ground contact. + + Args: + is_ground_contact: A boolean noting whether all the Story's room_2ds + have floors in contact with the ground. Default: True. + """ + for room in self._room_2ds: + room.is_ground_contact = is_ground_contact
+ +
[docs] def set_top_exposed(self, is_top_exposed=True): + """Set all child Room2Ds of this object to have ceilings exposed to the outdoors. + + Args: + is_top_exposed: A boolean noting whether all the Story's room_2ds + have ceilings exposed to the outdoors. Default: True. + """ + for room in self._room_2ds: + room.is_top_exposed = is_top_exposed
+ +
[docs] def split_with_story_above(self, story_above, tolerance=0.01): + """Split the child Room2Ds of this object with the footprint of the Story above. + + This is useful as a pre-step before running set_top_exposed_by_story_above + as it ensures all top-exposed areas of this Story have a Room2D that can + be set to exposed. + + Args: + story_above: A Story object that sits above this Story. Each Room2D + of this Story will be checked to see if it intersects the Story + above and it will be split based on this. + tolerance: The tolerance with which the splitting intersection will be + computed. Default: 0.01, suitable for objects in meters. + """ + # get the footprint geometry of the story above + above_geos = story_above.footprint(tolerance) + # loop through the rooms and split them + split_rooms = [] + for room in self._room_2ds: + # split the floor with all geometries above + room_height = room.floor_height + split_geo = [room.floor_geometry] + for i, a_geo in enumerate(above_geos): + # make sure all above geometries are at the room floor_height + if abs(a_geo[0].z - room_height) > tolerance: + a_geo = a_geo.move(Vector3D(0, 0, room_height - a_geo[0].z)) + above_geos[i] = a_geo # set it so we hopefully don't move next time + # split the geometries with one another + new_geo = [] + for r_geo in split_geo: + floor_split, above_split = Face3D.coplanar_split( + r_geo, a_geo, tolerance, 1) + new_geo.extend(floor_split) + split_geo = new_geo + # create the new Room2D if necessary + if len(split_geo) == 1: # no room splitting needed + split_rooms.append(room) + else: # the Room2D has been split + for j, s_geo in enumerate(split_geo): + # check to make sure the split geometry is not degenerate + max_dim = max((s_geo.max.x - s_geo.min.x, s_geo.max.y - s_geo.min.y)) + if s_geo.area < max_dim * tolerance: # degenerate geometry found + continue + new_id = '{}_{}'.format(room.identifier, j) + new_room = Room2D( + new_id, s_geo, room.floor_to_ceiling_height, + is_ground_contact=room.is_ground_contact, + is_top_exposed=room.is_top_exposed) + room._match_and_transfer_wall_props(new_room, tolerance) + new_room._display_name = room._display_name + new_room._user_data = None if room.user_data is None \ + else room.user_data.copy() + new_room._skylight_parameters = room._skylight_parameters + new_room._properties._duplicate_extension_attr(room._properties) + split_rooms.append(new_room) + # set the split rooms to this story + self.room_2ds = split_rooms
+ +
[docs] def set_top_exposed_by_story_above(self, story_above, tolerance=0.01): + """Set the child Room2Ds of this object to have ceilings exposed to the outdoors. + + Args: + story_above: A Story object that sits above this Story. Each Room2D + of this Story will be checked to see if the story_above geometry + lies above the room and, if not, the top exposure will be set to True. + tolerance: The tolerance that will be used to compute the point within + the floor boundary that is used to check whether there is geometry + above each Room2D. It is recommended that this number not be less + than 1 centimeter to avoid long computation times. Default: 0.01, + suitable for objects in meters. + """ + up_vec = Vector3D(0, 0, 1) + for room in self._room_2ds: + rm_pt = room.floor_geometry.center if room.floor_geometry.is_convex else \ + room.floor_geometry.pole_of_inaccessibility(tolerance) + face_ray = Ray3D(rm_pt, up_vec) + for other_room in story_above._room_2ds: + if other_room._floor_geometry.intersect_line_ray(face_ray) is not None: + room.is_top_exposed = False + break + else: + room.is_top_exposed = True
+ +
[docs] def make_underground(self): + """Make this Story underground by setting all Room2D segments to have Ground BCs. + + Note that this method only changes the outdoor walls of the Room2Ds to have + Ground boundary conditions and, if the floors of the story are also in + contact with the ground, the set_ground_contact should be used in addition + to this method. + + Also note that this method will throw an exception if any of the Room2Ds have + WindowParameters assigned to them (since Ground boundary conditions are) + not compatible with windows. So using the set_outdoor_window_parameters + method and passing None to remove all windows is often recommended + before running this method. + """ + for room in self._room_2ds: + for i, bc in enumerate(room._boundary_conditions): + if isinstance(bc, Outdoors): + room.set_boundary_condition(i, bcs.ground)
+ +
[docs] def generate_grid(self, x_dim, y_dim=None, offset=1.0): + """Get a list of gridded Mesh3D objects offset from the floors of this story. + + Args: + x_dim: The x dimension of the grid cells as a number. + y_dim: The y dimension of the grid cells as a number. Default is None, + which will assume the same cell dimension for y as is set for x. + offset: A number for how far to offset the grid from the base face. + Default is 1.0, which will not offset the grid to be 1 unit above + the floor. + """ + return [room.generate_grid(x_dim, y_dim, offset) for room in self._room_2ds]
+ +
[docs] def move(self, moving_vec): + """Move this Story along a vector. + + Args: + moving_vec: A ladybug_geometry Vector3D with the direction and distance + to move the object. + """ + for room in self._room_2ds: + room.move(moving_vec) + self._floor_height = self._floor_height + moving_vec.z + if self._roof is not None: + self._roof.move(moving_vec) + self.properties.move(moving_vec)
+ +
[docs] def rotate_xy(self, angle, origin): + """Rotate this Story counterclockwise in the XY plane by a certain angle. + + Args: + angle: An angle in degrees. + origin: A ladybug_geometry Point3D for the origin around which the + object will be rotated. + """ + for room in self._room_2ds: + room.rotate_xy(angle, origin) + if self._roof is not None: + self._roof.rotate_xy(angle, origin) + self.properties.rotate_xy(angle, origin)
+ +
[docs] def reflect(self, plane): + """Reflect this Story across a plane. + + Args: + plane: A ladybug_geometry Plane across which the object will be reflected. + """ + for room in self._room_2ds: + room.reflect(plane) + if self._roof is not None: + self._roof.reflect(plane) + self.properties.reflect(plane)
+ +
[docs] def scale(self, factor, origin=None): + """Scale this Story by a factor from an origin point. + + 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). + """ + for room in self._room_2ds: + room.scale(factor, origin) + self._floor_to_floor_height = self._floor_to_floor_height * factor + self._floor_height = self._floor_height * factor + if self._roof is not None: + self._roof.scale(factor, origin) + self.properties.scale(factor, origin)
+ +
[docs] def check_missing_adjacencies(self, raise_exception=True, detailed=False): + """Check that all Room2Ds have adjacent objects that exist within this Story. + + Args: + raise_exception: Boolean to note whether a ValueError should be raised + if missing or invalid adjacencies are found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + detailed = False if raise_exception else detailed + # gather all of the Surface boundary conditions + srf_bc_dict, rid_map = {}, {} + for room in self._room_2ds: + for bc, w_par in zip(room._boundary_conditions, room._window_parameters): + if isinstance(bc, Surface): + bc_objs = bc.boundary_condition_objects + try: + bc_ind = int(bc_objs[0].split('..Face')[-1]) - 1 + srf_bc_dict[(bc_objs[-1], bc_ind)] = \ + (room.identifier, bc_objs[0], w_par, room) + except ValueError: # Surface BC not following dragonfly convention + # this will be reported as a missing adjacency later + srf_bc_dict[(bc_objs[-1], 10000)] = \ + (room.identifier, bc_objs[0], w_par, room) + rid_map[room.identifier] = room.full_id + # check the adjacencies for all Surface boundary conditions + msgs = [] + for key, val in srf_bc_dict.items(): + rm_id = key[0] + for room in self._room_2ds: + if room.identifier == rm_id: + try: + rm_bc = room._boundary_conditions[key[1]] + rm_w_par = room._window_parameters[key[1]] + except IndexError: # referenced wall segment does not exist + try: + r1, r2 = rid_map[val[0]], rid_map[rm_id] + except KeyError: # completely missing from the model + r1, r2 = val[0], rm_id + msg = 'Room2D "{}" has an adjacency referencing a missing ' \ + 'wall segment on Room2D "{}".'.format(r1, r2) + msg = self._validation_message_child( + msg, val[3], detailed, '100203', + error_type='Missing Adjacency') + if detailed: + msg['element_id'].append(room.identifier) + msg['element_name'].append(room.display_name) + msg['parents'].append(msg['parents'][0]) + msgs.append(msg) + break + if not isinstance(rm_bc, Surface): + try: + r1, r2 = rid_map[rm_id], rid_map[val[1]] + except KeyError: # completely missing from the model + r1, r2 = rm_id, val[1] + msg = 'Room2D "{}" does not have a Surface boundary condition ' \ + 'at "{}" but its adjacent object does.'.format(r1, r2) + msg = self._validation_message_child( + msg, room, detailed, '100201', + error_type='Mismatched Adjacency') + if detailed: + msg['element_id'].append(val[3].identifier) + msg['element_name'].append(val[3].display_name) + msg['parents'].append(msg['parents'][0]) + msgs.append(msg) + if val[2] != rm_w_par: + try: + r1, r2 = rid_map[val[0]], rid_map[rm_id] + except KeyError: # completely missing from the model + r1, r2 = val[0], rm_id + msg = 'Window parameters do not match between ' \ + 'adjacent Room2Ds "{}" and "{}".'.format(r1, r2) + msg = self._validation_message_child( + msg, room, detailed, '100202', + error_type='Mismatched WindowParameter Adjacency') + if detailed: + msg['element_id'].append(val[3].identifier) + msg['element_name'].append(val[3].display_name) + msg['parents'].append(msg['parents'][0]) + msgs.append(msg) + break + else: + try: + r1, r2 = rid_map[val[0]], rid_map[rm_id] + except KeyError: # completely missing from the model + r1, r2 = val[0], rm_id + msg = 'Room2D "{}" has a missing adjacency for Room2D "{}".'.format( + r1, r2) + msg = self._validation_message_child( + msg, val[3], detailed, '100203', error_type='Missing Adjacency') + msgs.append(msg) + # report any errors + if detailed: + return msgs + full_msg = '\n '.join(msgs) + if raise_exception and len(msgs) != 0: + raise ValueError(full_msg) + return full_msg
+ +
[docs] def check_no_room2d_overlaps( + self, tolerance=0.01, raise_exception=True, detailed=False): + """Check that geometries of Room2Ds do not overlap with one another. + + Overlaps in Room2Ds mean that the Room volumes will collide with one + another during translation to Honeybee. + + Args: + tolerance: The minimum distance that two Room2Ds geometries can overlap + with one another and still be considered valid. (Default: 0.01, + suitable for objects in meters). + raise_exception: Boolean to note whether a ValueError should be raised + if overlapping geometries are found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + # find the number of overlaps across the Room2Ds + msgs = [] + rooms = self.room_2ds + for i, room_1 in enumerate(rooms): + poly_1 = room_1.floor_geometry.polygon2d + try: + for room_2 in rooms[i + 1:]: + poly_2 = room_2.floor_geometry.polygon2d + if poly_1.polygon_relationship(poly_2, tolerance) >= 0: + msg = 'Room2D "{}" overlaps with Room2D "{}" more than the '\ + 'tolerance ({}) on Story "{}".'.format( + room_1.display_name, room_2.display_name, + tolerance, self.display_name) + msg = self._validation_message_child( + msg, room_1, detailed, '100104', + error_type='Overlapping Room Geometries') + if detailed: + msg['element_id'].append(room_2.identifier) + msg['element_name'].append(room_2.display_name) + msg['parents'].append(msg['parents'][0]) + msgs.append(msg) + except IndexError: + pass # we have reached the end of the list + # report any errors + if detailed: + return msgs + full_msg = '\n '.join(msgs) + if raise_exception and len(msgs) != 0: + raise ValueError(full_msg) + return full_msg
+ +
[docs] def check_no_roof_overlaps( + self, tolerance=0.01, raise_exception=True, detailed=False): + """Check that geometries of RoofSpecifications do not overlap with one another. + + Overlaps make the Roof geometry unusable for translation to Honeybee. + + Args: + tolerance: The minimum distance that two Roof geometries can overlap + with one another and still be considered valid. Default: 0.01, + suitable for objects in meters. + raise_exception: Boolean to note whether a ValueError should be raised + if overlapping geometries are found. (Default: True). + detailed: Boolean for whether the returned object is a detailed list of + dicts with error info or a string with a message. (Default: False). + + Returns: + A string with the message or a list with a dictionary if detailed is True. + """ + # find the number of overlaps in the Roof specification + msgs = [] + if self.roof is not None: + over_count = self.roof.overlap_count(tolerance) + if over_count > 0: + msg = 'Story "{}" has RoofSpecification geometry with {} overlaps ' \ + 'in it.'.format(self.display_name, over_count) + msg = self._validation_message_child( + msg, self.roof, detailed, '100105', error_type='Invalid Roof') + msgs.append(msg) + # report any errors + if detailed: + return msgs + full_msg = '\n '.join(msgs) + if raise_exception and len(msgs) != 0: + raise ValueError(full_msg) + return full_msg
+ +
[docs] def to_honeybee(self, use_multiplier=True, add_plenum=False, tolerance=0.01, + enforce_adj=True, enforce_solid=True): + """Convert Dragonfly Story to a list of Honeybee Rooms. + + Args: + use_multiplier: If True, this Story's multiplier will be passed along + to the generated Honeybee Room objects, indicating the simulation + will be run once for the Story and then results will be multiplied. + You will want to set this to False when exporting each Story as + full geometry. + add_plenum: Boolean to indicate whether ceiling/floor plenums should + be auto-generated for the Rooms. (Default: False). + tolerance: The minimum distance in z values of floor_height and + floor_to_ceiling_height at which adjacent Faces will be split. + If None, no splitting will occur. (Default: 0.01, suitable for + objects in meters). + enforce_adj: Boolean to note whether an exception should be raised if + an adjacency between two Room2Ds is invalid (True) or if the invalid + Surface boundary condition should be replaced with an Outdoor + boundary condition (False). If False, any Walls containing + WindowParameters and an illegal boundary condition will also + be replaced with an Outdoor boundary condition. (Default: True). + enforce_solid: Boolean to note whether rooms should be translated + as solid extrusions whenever translating them with custom + roof geometry produces a non-solid result (True) or the non-solid + room geometry should be allowed to remain in the result (False). + The latter is useful for understanding why a particular roof + geometry has produced a non-solid result. (Default: True). + + Returns: + A list of honeybee Rooms that represent the Story. + """ + # set up the multiplier + mult = self.multiplier if use_multiplier else 1 + + # convert all of the Room2Ds to honeybee Rooms + hb_rooms = [] + adjacencies = [] + for room in self._room_2ds: + hb_room, adj = room.to_honeybee( + mult, add_plenum=add_plenum, tolerance=tolerance, + enforce_bc=enforce_adj, enforce_solid=enforce_solid) + if isinstance(hb_room, Room): + hb_rooms.append(hb_room) + else: # list of rooms with plenums + hb_rooms.extend(hb_room) + adjacencies.extend(adj) + + # assign adjacent boundary conditions that could not be set on the room level + if len(adjacencies) != 0: + adj_set = set() + for adj in adjacencies: + if adj[0].identifier not in adj_set: + for room in hb_rooms: + adj_room = adj[1][-1] + if room.identifier == adj_room: + for face in room.faces: + adj_face = adj[1][-2] + if face.identifier == adj_face: + self._match_apertures(adj[0], face) + other_resolve = False + if self.roof is not None: # two roofs may meet + tol_area = math.sqrt(face.area) * tolerance + if abs(face.area - adj[0].area) > tol_area: + self._resolve_roof_adj( + face, adj[0], tolerance) + other_resolve = True + if not other_resolve: + try: + adj[0].set_adjacency(face, tolerance) + except (AssertionError, ValueError) as e: + if enforce_adj: + raise e + face.boundary_condition = bcs.outdoors + adj[0].boundary_condition = bcs.outdoors + adj_set.add(face.identifier) + break + break + return hb_rooms
+ +
[docs] def to_dict(self, abridged=False, included_prop=None): + """Return Story as a dictionary. + + Args: + abridged: Boolean to note whether the extension properties of the + object (ie. construction sets) should be included in detail + (False) or just referenced by identifier (True). Default: False. + included_prop: List of properties to filter keys that must be included in + output dictionary. For example ['energy'] will include 'energy' key if + available in properties to_dict. By default all the keys will be + included. To exclude all the keys from extensions use an empty list. + """ + base = {'type': 'Story'} + base['identifier'] = self.identifier + base['display_name'] = self.display_name + base['room_2ds'] = [r.to_dict(abridged, included_prop) for r in self._room_2ds] + base['floor_to_floor_height'] = self.floor_to_floor_height + base['floor_height'] = self.floor_height + base['multiplier'] = self.multiplier + if self.roof is not None: + base['roof'] = self.roof.to_dict() + if self.user_data is not None: + base['user_data'] = self.user_data + base['properties'] = self.properties.to_dict(abridged, included_prop) + return base
+ + @property + def to(self): + """Story writer object. + + Use this method to access Writer class to write the story in other formats. + """ + return writer + +
[docs] @staticmethod + def room_2d_story_geometry_valid(room_2ds): + """Check that a set of Room2Ds have geometry that makes a valid Story. + + This means that all of the floors of the Room2Ds are close enough to + one another in elevation that their walls could touch each other. + + Args: + room_2ds: An array of Room2Ds that will be checked to ensure their + geometry makes a valid Story. + + Returns: + True if the Room2D geometries make a valid Story. False if they do not. + """ + if len(room_2ds) == 1: + return True + flr_hts = sorted([rm.floor_height for rm in room_2ds]) + min_flr_to_ceil = min([rm.floor_to_ceiling_height for rm in room_2ds]) + return True if flr_hts[-1] - flr_hts[0] < min_flr_to_ceil else False
+ + @staticmethod + def _match_apertures(face_1, face2): + for ap1, ap2 in zip(face_1.apertures, face2.apertures): + ap1._is_operable, ap2._is_operable = False, False + try: + ap1.properties.energy.vent_opening = None + ap2.properties.energy.vent_opening = None + except AttributeError: + pass # honeybee-energy extension is not loaded + + @staticmethod + def _resolve_roof_adj(face_1, face_2, tol): + """Resolve incorrect adjacency where walls of two roofs meet.""" + # remove air boundary conditions so the split result is valid + use_ab = False + if isinstance(face_1.type, AirBoundary) or isinstance(face_2.type, AirBoundary): + face_1.type = ftyp.wall + face_2.type = ftyp.wall + use_ab = True + + # split the adjacent walls with one another to get a match + room_1, room_2 = face_1.parent, face_2.parent + new_faces1 = room_1.coplanar_split([face_2.geometry], tol) + new_faces2 = room_2.coplanar_split([face_1.geometry], tol) + new_faces1 = [face_1] if len(new_faces1) == 0 else new_faces1 + new_faces2 = [face_2] if len(new_faces2) == 0 else new_faces2 + + # find the adjacency and set it + adj_geo = None + for j, f_1 in enumerate(new_faces1): + for k, f_2 in enumerate(new_faces2): + if f_1.geometry.is_centered_adjacent(f_2.geometry, tol): + f_1.set_adjacency(f_2) + adj_geo = f_1.geometry + if use_ab: + f_1.type = ftyp.air_boundary + f_2.type = ftyp.air_boundary + new_faces1.pop(j) + new_faces2.pop(k) + break + + # set the boundary conditions of the other newly-created Faces + for nf in new_faces1: + for of in room_2.faces: + if nf.geometry.is_centered_adjacent(of.geometry, tol): + nf.set_adjacency(of) + if use_ab: + nf.type = ftyp.air_boundary + of.type = ftyp.air_boundary + break + else: + if adj_geo is not None: + if nf.center.z < adj_geo.center.z: + nf.boundary_condition = room_2[0].boundary_condition + elif nf.center.z > adj_geo.center.z: + nf.boundary_condition = room_2[-1].boundary_condition + for nf in new_faces2: + for of in room_1.faces: + if nf.geometry.is_centered_adjacent(of.geometry, tol): + nf.set_adjacency(of) + if use_ab: + nf.type = ftyp.air_boundary + of.type = ftyp.air_boundary + break + else: + if adj_geo is not None: + if nf.center.z < adj_geo.center.z: + nf.boundary_condition = room_1[0].boundary_condition + elif nf.center.z > adj_geo.center.z: + nf.boundary_condition = room_1[-1].boundary_condition + + def __copy__(self): + new_s = Story( + self.identifier, tuple(room.duplicate() for room in self._room_2ds), + self._floor_to_floor_height, self._floor_height, self._multiplier) + new_s._roof = None if self._roof is None else self._roof.duplicate() + new_s._display_name = self._display_name + new_s._user_data = None if self.user_data is None else self.user_data.copy() + new_s._parent = self._parent + new_s._properties._duplicate_extension_attr(self._properties) + return new_s + + def __len__(self): + return len(self._room_2ds) + + def __getitem__(self, key): + return self._room_2ds[key] + + def __iter__(self): + return iter(self._room_2ds) + + def __repr__(self): + return 'Story: %s' % self.display_name
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/subdivide.html b/docs/_modules/dragonfly/subdivide.html new file mode 100644 index 00000000..a867ff7d --- /dev/null +++ b/docs/_modules/dragonfly/subdivide.html @@ -0,0 +1,1089 @@ + + + + + + + dragonfly.subdivide — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.subdivide

+"""Utilities to interpret parameters for subdividing building masses."""
+from __future__ import division
+
+
+
[docs]def interpret_floor_height_subdivide(floor_to_floor_heights, max_height, + first_floor_height=0): + """Interpret a list of instructions for subdividing a building mass into floors. + + Args: + floor_to_floor_heights: An array of floor-to-floor height instructions + that describe how a building mass should be divided into floors. + The array should run from bottom floor to top floor. + Each item in the array can be either a single number for the + floor-to-floor height or a text string that codes for how many + floors of each height should be generated. For example, inputting + "2@4" will make two floors with a height of 4 units. Simply inputting + "@3" will make all floors at 3 units. Putting in sequential arrays + of these text strings will divide up floors accordingly. For example, + the list ["1@5", "2@4", "@3"] will make a ground floor of 5 units, + two floors above that at 4 units and all remaining floors at 3 units. + max_height: The maximum height of the building, noting the z-value + above which no new floor heights should be generated. + first_floor_height: The z-value of the first floor. + + Returns: + A tuple with two elements + + - floor_heights -- An array of float values for the floor heights, which + can be used to generate planes that subdivide a building mass. + + - interpreted_f2f -- An array of float values noting the distance between + each floor. Note that, unlike the input floor_to_floor_heights, + this array always has float values and is the same length as the + floor_heights. + """ + # generate the list of height float values + floor_heights = [first_floor_height] + interpreted_f2f = [] + for height in floor_to_floor_heights: + try: # single number for the floor + flr_h = float(height) + flr_count = 1 + except (TypeError, ValueError): # instructions for generating floors + flr_h = float(height.split('@')[1]) + try: + flr_count = int(height.split('@')[0]) + except ValueError: # no number of floors to generate (ie. '@3') + flr_count = int((max_height - floor_heights[-1]) / flr_h) + + if flr_h != 0: + for _ in range(flr_count): + floor_heights.append(floor_heights[-1] + flr_h) + interpreted_f2f.append(flr_h) + + # check to be sure no heights are above the max height + if floor_heights[-1] >= max_height: + floor_heights = [hgt for hgt in floor_heights if hgt < max_height] + interpreted_f2f = [interpreted_f2f[i] for i in range(len(floor_heights))] + + # remove last height if the difference between it and max height is too small + if len(floor_heights) != 1 and \ + max_height - floor_heights[-1] < interpreted_f2f[-1] - 1e-9: + del floor_heights[-1] + del interpreted_f2f[-1] + interpreted_f2f.append(max_height - floor_heights[-1]) + elif len(interpreted_f2f) < len(floor_heights): + interpreted_f2f.append(max_height - floor_heights[-1]) + + return floor_heights, interpreted_f2f
+ + +
[docs]def interpret_core_perimeter_subdivide(perimeter_depths, floor_count): + """Interpret a list of instructions for subdividing a building mass into floors. + + Args: + perimeter_depths: An array of perimeter depth instructions that describe + how a building floors should be divided into core/perimeter Rooms. + The array should run from bottom floor to top floor. + Each item in the array can be either a single number for the perimeter + depth or a text string that codes for over how many floors a given + perimeter depth should be applies. For example, inputting + "2@4" will offset the first 2 floors 4 units. Simply inputting + "@3" will make all floors offset at 3 units. Putting in sequential arrays + of these text strings will offset floors accordingly. For example, + the list ["1@5", "2@4", "@3"] will offset the ground floor at 5 units, + two floors above that at 4 units and all remaining floors at 3 units. + floor_count: An integer for the number of floors within the building. The + array output from this function will have a length equal to this number. + + Returns: + An array of float values for perimeter depths, which can be used to offset + perimeters over a building's stories. + """ + # generate the list of depth float values + interpreted_depths = [] + for depth in perimeter_depths: + try: # single number for the floor + flr_d = float(depth) + flr_count = 1 + except (TypeError, ValueError): # instructions for generating depths + flr_d = float(depth.split('@')[1]) + try: + flr_count = int(depth.split('@')[0]) + except ValueError: # no number of depths to generate (ie. '@3') + flr_count = int(floor_count - len(interpreted_depths)) + + for _ in range(flr_count): + interpreted_depths.append(flr_d) + + # check to be sure the length of the list is correct + if len(interpreted_depths) > floor_count: # cut the list short + interpreted_depths = [interpreted_depths[i] for i in range(floor_count)] + elif len(interpreted_depths) < floor_count: # repeat last floor + interpreted_depths.extend([interpreted_depths[-1] for i in + range(floor_count - len(interpreted_depths))]) + + return interpreted_depths
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/dragonfly/windowparameter.html b/docs/_modules/dragonfly/windowparameter.html new file mode 100644 index 00000000..54aef6f2 --- /dev/null +++ b/docs/_modules/dragonfly/windowparameter.html @@ -0,0 +1,3659 @@ + + + + + + + dragonfly.windowparameter — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +

Source code for dragonfly.windowparameter

+# coding: utf-8
+"""Window Parameters with instructions for generating windows."""
+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.pointvector import Vector3D, Point3D
+from ladybug_geometry.geometry3d import LineSegment3D, Plane, Face3D
+from ladybug_geometry.bounding import bounding_rectangle
+
+from honeybee.typing import float_in_range, float_positive
+from honeybee.boundarycondition import Surface
+from honeybee.aperture import Aperture
+from honeybee.door import Door
+
+
+class _WindowParameterBase(object):
+    """Base object for all window parameters.
+
+    This object records all of the methods that must be overwritten on a window
+    parameter object for it to be successfully be applied in dragonfly workflows.
+
+    Properties:
+        * user_data
+    """
+    __slots__ = ('_user_data',)
+
+    def __init__(self):
+        self._user_data = None
+
+    @property
+    def user_data(self):
+        """Get or set an optional dictionary for additional meta data for this object.
+
+        This will be None until it has been set. All keys and values of this
+        dictionary should be of a standard Python type to ensure correct
+        serialization of the object to/from JSON (eg. str, float, int, list dict)
+        """
+        return self._user_data
+
+    @user_data.setter
+    def user_data(self, value):
+        if value is not None:
+            assert isinstance(value, dict), 'Expected dictionary for honeybee ' \
+                'object user_data. Got {}.'.format(type(value))
+        self._user_data = value
+
+    def area_from_segment(self, segment, floor_to_ceiling_height):
+        """Get the window area generated by these parameters from a LineSegment3D."""
+        return 0
+
+    def add_window_to_face(self, face, tolerance=0.01):
+        """Add Apertures to a Honeybee Face using these Window Parameters."""
+        pass
+
+    def scale(self, factor):
+        """Get a scaled version of these WindowParameters.
+
+        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 trim(self, original_segment, sub_segment, tolerance=0.01):
+        """Trim window parameters for a sub segment given the original segment.
+
+        Args:
+            original_segment: The original LineSegment3D to which the window
+                parameters are assigned.
+            sub_segment: A LineSegment3D that is a sub-segment of the original_segment,
+                which will be used to trim the window parameters to fit this segment.
+                Note that this sub_segment should have the same orientation as
+                the original segment.
+            tolerance: The minimum distance between a vertex and the edge of the
+                wall segment that is considered not touching. (Default: 0.01, suitable
+                for objects in meters).
+        """
+        return self  # windows are assumed to repeat
+
+    @staticmethod
+    def merge_to_rectangular(window_parameters, segments, floor_to_ceiling_height):
+        """Merge any window parameters together into rectangular windows.
+
+        Args:
+            window_parameters: A list of WindowParameters to be merged.
+            segments: The segments to which the window parameters are assigned.
+                These should be in order as they appear on the parent Room2D.
+            floor_to_ceiling_height: The floor-to-ceiling height of the Room2D
+                to which the segment belongs.
+        """
+        base_x = 0
+        origins, widths, heights, doors = [], [], [], []
+        for wp, s in zip(window_parameters, segments):
+            if wp is not None:
+                rwp = wp.to_rectangular_windows(s, floor_to_ceiling_height)
+                zip_obj = zip(rwp.origins, rwp.widths, rwp.heights, rwp.are_doors)
+                for o, w, h, d in zip_obj:
+                    origins.append(Point2D(o.x + base_x, o.y))
+                    widths.append(w)
+                    heights.append(h)
+                    doors.append(d)
+            base_x += s.length
+        return RectangularWindows(origins, widths, heights, doors)
+
+    @classmethod
+    def from_dict(cls, data):
+        """Create WindowParameterBase from a dictionary.
+
+        .. code-block:: python
+
+            {
+            "type": "WindowParameterBase"
+            }
+        """
+        assert data['type'] == 'WindowParameterBase', \
+            'Expected WindowParameterBase dictionary. Got {}.'.format(data['type'])
+        new_w_par = cls()
+        if 'user_data' in data and data['user_data'] is not None:
+            new_w_par.user_data = data['user_data']
+        return new_w_par
+
+    def to_dict(self):
+        """Get WindowParameterBase as a dictionary."""
+        return {'type': 'WindowParameterBase'}
+
+    def duplicate(self):
+        """Get a copy of this object."""
+        return self.__copy__()
+
+    def ToString(self):
+        return self.__repr__()
+
+    def _add_user_data(self, new_win_par):
+        """Add copies of this object's user_data to new WindowParameters."""
+        if self.user_data is not None:
+            for w_par in new_win_par:
+                w_par.user_data = self.user_data.copy()
+
+    def _apply_user_data_to_honeybee(self, sub_faces, clean_data=None):
+        """Apply the WindowParameter user_data to generated honeybee objects.
+
+        Args:
+            sub_faces: An array of Honeybee Apertures or Doors to which user_data
+                will be applied from this WindowParameter.
+            clean_data: An optional dictionary of user_data to be used in place
+                of the currently assigned user_data. This is useful when not all
+                WindowParameters are able to be applied to the generated Honeybee
+                objects. When None, the self.user_data will be used. (Default: None).
+        """
+        app_data = self.user_data if clean_data is None else clean_data
+        if app_data is not None:
+            for i, sub_f in enumerate(sub_faces):
+                u_dict = {}
+                for key, val in app_data.items():
+                    if isinstance(val, (list, tuple)) and len(val) != 0:
+                        try:
+                            u_dict[key] = val[i]
+                        except IndexError:  # use longest list logic
+                            u_dict[key] = val[-1]
+                    else:
+                        u_dict[key] = val
+                sub_f.user_data = u_dict
+
+    def __copy__(self):
+        return _WindowParameterBase()
+
+    def __repr__(self):
+        return 'WindowParameterBase'
+
+
+
[docs]class SingleWindow(_WindowParameterBase): + """Instructions for a single window in the face center defined by a width x height. + + Note that, if these parameters are applied to a base face that is too short + or too narrow for the input width and/or height, the generated window will + automatically be shortened when it is applied to the face. In this way, + setting the width to be `float('inf')` will create parameters that always + generate a ribbon window of the input height. + + Args: + width: A number for the window width. + height: A number for the window height. + sill_height: A number for the window sill height. Default: 1. + + Properties: + * width + * height + * sill_height + * user_data + """ + __slots__ = ('_width', '_height', '_sill_height') + + def __init__(self, width, height, sill_height=1): + """Initialize SingleWindow.""" + _WindowParameterBase.__init__(self) # add the user_data + self._width = float_positive(width, 'window width') + self._height = float_positive(height, 'window height') + self._sill_height = float_positive(sill_height, 'window sill height') + + @property + def width(self): + """Get a number for the window width.""" + return self._width + + @property + def height(self): + """Get a number for the window height.""" + return self._height + + @property + def sill_height(self): + """Get a number for the sill height.""" + return self._sill_height + +
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): + """Get the window area generated by these parameters from a LineSegment3D. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + max_width = segment.length + max_height = floor_to_ceiling_height - self.sill_height + final_width = max_width if self.width > max_width else self.width + final_height = max_height if self.height > max_height else self.height + if final_height < 0: + return 0 + else: + return final_width * final_height
+ +
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): + """Get a version of these WindowParameters as RectangularWindows. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segments belong. + """ + max_width = segment.length + max_height = (floor_to_ceiling_height) - self.sill_height + final_width = max_width if self.width > max_width else self.width + final_height = max_height if self.height > max_height else self.height + if final_height < 0: + return None + else: + origin = Point2D((segment.length - final_width) / 2, self.sill_height) + new_w = RectangularWindows([origin], [final_width], [final_height]) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def add_window_to_face(self, face, tolerance=0.01): + """Add Apertures to a Honeybee Face using these Window Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: Optional tolerance value. Default: 0.01, suitable for + objects in meters. + """ + if self._width == 0 or self._height == 0: + return None + width_seg = LineSegment3D.from_end_points(face.geometry[0], face.geometry[1]) + height_seg = LineSegment3D.from_end_points(face.geometry[1], face.geometry[2]) + max_width = width_seg.length - tolerance + max_height = (height_seg.length - tolerance) - self.sill_height + final_width = max_width if self.width > max_width else self.width + final_height = max_height if self.height > max_height else self.height + if final_height > 0: + face.aperture_by_width_height(final_width, final_height, self.sill_height) + # if the Aperture is interior, set adjacent boundary condition + if isinstance(face._boundary_condition, Surface): + ids = face._boundary_condition.boundary_condition_objects + adj_ap_id = '{}_Glz1'.format(ids[0]) + final_ids = (adj_ap_id,) + ids + face.apertures[0].boundary_condition = Surface(final_ids, True) + self._apply_user_data_to_honeybee(face.apertures)
+ +
[docs] def scale(self, factor): + """Get a scaled version of these WindowParameters. + + This method is called within the scale methods of the Room2D. + + Args: + factor: A number representing how much the object should be scaled. + """ + new_w = SingleWindow( + self.width * factor, self.height * factor, self.sill_height * factor) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def split(self, segments, tolerance=0.01): + """Split SingleWindow parameters across a list of ordered segments. + + Args: + segments: The segments to which the window parameters are being + split across. These should be in order as they appear on the + parent Room2D. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + lengths = [s.length for s in segments] + total_len = sum(lengths) + new_w_par = [] + for length in lengths: + new_w = (length / total_len) * self.width + new_w_par.append(SingleWindow(new_w, self.height, self.sill_height)) + self._add_user_data(new_w_par) + return new_w_par
+ +
[docs] @staticmethod + def merge(window_parameters, segments, floor_to_ceiling_height): + """Merge SingleWindow parameters together using their assigned segments. + + Args: + window_parameters: A list of WindowParameters to be merged. + segments: The segments to which the window parameters are assigned. + These should be in order as they appear on the parent Room2D. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segments belong. + """ + width = 0 + weights, heights, sill_heights = [], [], [], [] + for wp, s in zip(window_parameters, segments): + if wp is not None: + if isinstance(wp, SingleWindow): + weights.append(s.length) + width += wp.width + heights.append(wp.height) + sill_heights.append(wp.length) + else: # not all windows are of the same type; convert all to rectangular + return _WindowParameterBase.merge_to_rectangular( + window_parameters, segments, floor_to_ceiling_height) + tw = sum(weights) + weights = [w / tw for w in weights] + height = sum([h * w for h, w in zip(heights, weights)]) + sill_height = sum([h * w for h, w in zip(sill_heights, weights)]) + new_w = SingleWindow(width, height, sill_height) + new_w._user_data = None if window_parameters[0].user_data is None else \ + window_parameters[0].user_data.copy() + return new_w
+ +
[docs] @classmethod + def from_dict(cls, data): + """Create SingleWindow from a dictionary. + + .. code-block:: python + + { + "type": "SingleWindow", + "width": 100, + "height": 1.5, + "sill_height": 0.8 + } + """ + assert data['type'] == 'SingleWindow', \ + 'Expected SingleWindow dictionary. Got {}.'.format(data['type']) + sill = data['sill_height'] if 'sill_height' in data else 1 + new_w_par = cls(data['width'], data['height'], sill) + if 'user_data' in data and data['user_data'] is not None: + new_w_par.user_data = data['user_data'] + return new_w_par
+ +
[docs] def to_dict(self): + """Get SingleWindow as a dictionary.""" + base = { + 'type': 'SingleWindow', + 'width': self.width, + 'height': self.height, + 'sill_height': self.sill_height + } + if self.user_data is not None: + base['user_data'] = self.user_data + return base
+ + def __copy__(self): + new_w = SingleWindow(self.width, self.height, self.sill_height) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.width, self.height, self.sill_height) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, SingleWindow) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'SingleWindow: [width: {}] [height: {}] [sill_height: {}]'.format( + self.width, self.height, self.sill_height)
+ + +
[docs]class SimpleWindowArea(_WindowParameterBase): + """Instructions for a single window defined by an absolute area. + + Properties: + * window_area + * rect_split + * user_data + + Args: + window_area: A number for the window area in current model units. + If this area is larger than the area of the Wall that it is applied + to, the window will fill the parent Wall at a 99% ratio. + rect_split: Boolean to note whether rectangular portions of base Face + should be extracted before scaling them to create apertures. For + pentagonal gabled geometries, the resulting apertures will consist + of one rectangle and one triangle, which can often look more realistic + and is a better input for engines like EnergyPlus that cannot + model windows with more than 4 vertices. However, if a single + pentagonal window is desired for such a gabled shape, this input can + be set to False to produce such a result. + """ + __slots__ = ('_window_area', '_rect_split') + + def __init__(self, window_area, rect_split=True): + """Initialize SimpleWindowArea.""" + _WindowParameterBase.__init__(self) # add the user_data + self._window_area = float_positive(window_area, 'window area') + self._rect_split = bool(rect_split) + + @property + def window_area(self): + """Get a number for the window area in current model units.""" + return self._window_area + + @property + def rect_split(self): + """Get a boolean for whether rectangular portions are extracted.""" + return self._rect_split + +
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): + """Get the window area generated by these parameters from a LineSegment3D. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + wall_area = segment.length * floor_to_ceiling_height * 0.99 + return self.window_area if self.window_area < wall_area else wall_area
+ +
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): + """Get a version of these WindowParameters as RectangularWindows. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + if self._window_area == 0: + return None + wall_area = segment.length * floor_to_ceiling_height * 0.99 + window_ratio = self.window_area / wall_area + window_ratio = 0.99 if window_ratio > 0.99 else window_ratio + scale_factor = window_ratio ** 0.5 + final_height = floor_to_ceiling_height * scale_factor + final_width = segment.length * scale_factor + origin = Point2D((segment.length - final_width) / 2, + (floor_to_ceiling_height - final_height) / 2) + new_w = RectangularWindows([origin], [final_width], [final_height]) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def add_window_to_face(self, face, tolerance=0.01): + """Add Apertures to a Honeybee Face using these Window Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: Optional tolerance value. Default: 0.01, suitable for + objects in meters. + """ + if self._window_area == 0: + return None + window_ratio = self.window_area / face.area + window_ratio = 0.99 if window_ratio > 0.99 else window_ratio + face.apertures_by_ratio(window_ratio, tolerance, self.rect_split) + # if the Aperture is interior, set adjacent boundary condition + if isinstance(face._boundary_condition, Surface): + num_aps = face.apertures + for i, ap in enumerate(face.apertures): + ids = face._boundary_condition.boundary_condition_objects + adj_ap_id = '{}_Glz{}'.format(ids[0], num_aps - i - 1) + final_ids = (adj_ap_id,) + ids + ap.boundary_condition = Surface(final_ids, True) + self._apply_user_data_to_honeybee(face.apertures)
+ +
[docs] def scale(self, factor): + """Get a scaled version of these WindowParameters. + + This method is called within the scale methods of the Room2D. + + Args: + factor: A number representing how much the object should be scaled. + """ + new_w = SimpleWindowArea(self.window_area * factor, self.rect_split) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def split(self, segments, tolerance=0.01): + """Split SimpleWindowArea parameters across a list of ordered segments. + + Args: + segments: The segments to which the window parameters are being + split across. These should be in order as they appear on the + parent Room2D. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + # if one of the segments is much larger than the others, add all windows to that + lengths = [s.length for s in segments] + new_wps = self._all_to_primary_segment(lengths) + if new_wps is not None: + return new_wps + # otherwise, just distribute the windows evenly + total_len = sum(lengths) + n_par = [SimpleWindowArea(self.window_area * (sl / total_len), self.rect_split) + for sl in lengths] + self._add_user_data(n_par) + return n_par
+ +
[docs] def trim(self, original_segment, sub_segment, tolerance=0.01): + """Trim window parameters for a sub segment given the original segment. + + Args: + original_segment: The original LineSegment3D to which the window + parameters are assigned. + sub_segment: A LineSegment3D that is a sub-segment of the original_segment, + which will be used to trim the window parameters to fit this segment. + Note that this sub_segment should have the same orientation as + the original segment. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + # evenly distribute the windows + len_ratio = sub_segment.length / original_segment.length + n_par = SimpleWindowArea(self.window_area * len_ratio, self.rect_split) + n_par._user_data = self._user_data + return n_par
+ +
[docs] @staticmethod + def merge(window_parameters, segments, floor_to_ceiling_height): + """Merge SimpleWindowArea parameters together using their assigned segments. + + Args: + window_parameters: A list of WindowParameters to be merged. + segments: The segments to which the window parameters are assigned. + These should be in order as they appear on the parent Room2D. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segments belong. + """ + win_area, rect_split = 0, True + for wp in window_parameters: + if wp is not None: + if isinstance(wp, SimpleWindowArea): + win_area += wp.window_area + rect_split = wp.rect_split + else: # not all windows are of the same type; convert all to rectangular + return _WindowParameterBase.merge_to_rectangular( + window_parameters, segments, floor_to_ceiling_height) + new_w = SimpleWindowArea(win_area, rect_split) + new_w._user_data = None if window_parameters[0].user_data is None else \ + window_parameters[0].user_data.copy() + return new_w
+ +
[docs] @classmethod + def from_dict(cls, data): + """Create SimpleWindowArea from a dictionary. + + .. code-block:: python + + { + "type": "SimpleWindowArea", + "window_area": 5.5, + "rect_split": False + } + """ + assert data['type'] == 'SimpleWindowArea', \ + 'Expected SimpleWindowArea dictionary. Got {}.'.format(data['type']) + rect_split = True if 'rect_split' not in data else data['rect_split'] + new_w_par = cls(data['window_area'], rect_split) + if 'user_data' in data and data['user_data'] is not None: + new_w_par.user_data = data['user_data'] + return new_w_par
+ +
[docs] def to_dict(self): + """Get SimpleWindowArea as a dictionary.""" + base = { + 'type': 'SimpleWindowArea', + 'window_area': self.window_area + } + if self.user_data is not None: + base['user_data'] = self.user_data + if not self.rect_split: + base['rect_split'] = False + return base
+ + def _all_to_primary_segment(self, lengths, prim_ratio=0.95): + """Determine if one segment is primary and should get all the windows.""" + total_len = sum(lengths) + prim_len = prim_ratio + 0.01 + all_to_one_i = None + for i, sl in enumerate(lengths): + if sl / total_len > prim_len: + all_to_one_i = i + break + if all_to_one_i is not None: + new_wps = [None] * len(lengths) + new_par = self.duplicate() + new_wps[all_to_one_i] = new_par + return new_wps + + def __copy__(self): + new_w = SimpleWindowArea(self.window_area, self.rect_split) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self._window_area, self._rect_split) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, SimpleWindowArea) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'SimpleWindowArea: [area: {}]'.format(self.window_area)
+ + +
[docs]class SimpleWindowRatio(_WindowParameterBase): + """Instructions for a single window defined by an area ratio with the base wall. + + Properties: + * window_ratio + * rect_split + * user_data + + Args: + window_ratio: A number between 0 and 1 for the ratio between the window + area and the parent wall surface area. + rect_split: Boolean to note whether rectangular portions of base Face + should be extracted before scaling them to create apertures. For + pentagonal gabled geometries, the resulting apertures will consist + of one rectangle and one triangle, which can often look more realistic + and is a better input for engines like EnergyPlus that cannot + model windows with more than 4 vertices. However, if a single + pentagonal window is desired for such a gabled shape, this input can + be set to False to produce such a result. + """ + __slots__ = ('_window_ratio', '_rect_split') + + def __init__(self, window_ratio, rect_split=True): + """Initialize SimpleWindowRatio.""" + _WindowParameterBase.__init__(self) # add the user_data + self._window_ratio = float_in_range(window_ratio, 0, 1, 'window ratio') + self._rect_split = bool(rect_split) + + @property + def window_ratio(self): + """Get a number between 0 and 1 for the window ratio.""" + return self._window_ratio + + @property + def rect_split(self): + """Get a boolean for whether rectangular portions are extracted.""" + return self._rect_split + +
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): + """Get the window area generated by these parameters from a LineSegment3D. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + return segment.length * floor_to_ceiling_height * self._window_ratio
+ +
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): + """Get a version of these WindowParameters as RectangularWindows. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + if self._window_ratio == 0: + return None + scale_factor = self.window_ratio ** 0.5 + final_height = floor_to_ceiling_height * scale_factor + final_width = segment.length * scale_factor + origin = Point2D((segment.length - final_width) / 2, + (floor_to_ceiling_height - final_height) / 2) + new_w = RectangularWindows([origin], [final_width], [final_height]) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def add_window_to_face(self, face, tolerance=0.01): + """Add Apertures to a Honeybee Face using these Window Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: Optional tolerance value. Default: 0.01, suitable for + objects in meters. + """ + if self._window_ratio == 0: + return None + face.apertures_by_ratio(self.window_ratio, tolerance, self.rect_split) + # if the Aperture is interior, set adjacent boundary condition + if isinstance(face._boundary_condition, Surface): + num_aps = face.apertures + for i, ap in enumerate(face.apertures): + ids = face._boundary_condition.boundary_condition_objects + adj_ap_id = '{}_Glz{}'.format(ids[0], num_aps - i - 1) + final_ids = (adj_ap_id,) + ids + ap.boundary_condition = Surface(final_ids, True) + self._apply_user_data_to_honeybee(face.apertures)
+ +
[docs] def split(self, segments, tolerance=0.01): + """Split SimpleWindowRatio parameters across a list of ordered segments. + + Args: + segments: The segments to which the window parameters are being + split across. These should be in order as they appear on the + parent Room2D. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + # if one of the segments is much larger than the others, add all windows to that + new_ratios = self._all_to_primary_segment(segments) + if new_ratios is not None: + return new_ratios + # otherwise, just distribute the windows evenly + new_w_par = [self] * len(segments) + self._add_user_data(new_w_par) + return new_w_par
+ +
[docs] @staticmethod + def merge(window_parameters, segments, floor_to_ceiling_height): + """Merge SimpleWindowRatio parameters together using their assigned segments. + + Args: + window_parameters: A list of WindowParameters to be merged. + segments: The segments to which the window parameters are assigned. + These should be in order as they appear on the parent Room2D. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segments belong. + """ + win_area, wall_area, rect_split = 0, 0, True + for wp, s in zip(window_parameters, segments): + if wp is not None: + wall_a = s.length * floor_to_ceiling_height + wall_area += wall_a + if isinstance(wp, SimpleWindowRatio): + win_area += (wall_area * wp.window_ratio) + rect_split = wp.rect_split + else: # not all windows are of the same type; convert all to rectangular + return _WindowParameterBase.merge_to_rectangular( + window_parameters, segments, floor_to_ceiling_height) + new_w = SimpleWindowRatio(win_area / wall_area, rect_split) + new_w._user_data = None if window_parameters[0].user_data is None else \ + window_parameters[0].user_data.copy() + return new_w
+ +
[docs] @classmethod + def from_dict(cls, data): + """Create SimpleWindowRatio from a dictionary. + + .. code-block:: python + + { + "type": "SimpleWindowRatio", + "window_ratio": 0.4, + "rect_split": False + } + """ + assert data['type'] == 'SimpleWindowRatio', \ + 'Expected SimpleWindowRatio dictionary. Got {}.'.format(data['type']) + rect_split = True if 'rect_split' not in data else data['rect_split'] + new_w_par = cls(data['window_ratio'], rect_split) + if 'user_data' in data and data['user_data'] is not None: + new_w_par.user_data = data['user_data'] + return new_w_par
+ +
[docs] def to_dict(self): + """Get SimpleWindowRatio as a dictionary.""" + base = { + 'type': 'SimpleWindowRatio', + 'window_ratio': self.window_ratio + } + if self.user_data is not None: + base['user_data'] = self.user_data + if not self.rect_split: + base['rect_split'] = False + return base
+ + def _all_to_primary_segment(self, segments, prim_ratio=0.95): + """Determine if one segment is primary and should get all the windows.""" + if self.window_ratio <= prim_ratio: + lengths = [s.length for s in segments] + total_len = sum(lengths) + prim_len = prim_ratio + 0.01 + all_to_one_i = None + for i, sl in enumerate(lengths): + if sl / total_len > prim_len: + all_to_one_i = i + all_to_one_ratio = self.window_ratio * (total_len / sl) + break + if all_to_one_i is not None: + new_ratios = [None] * len(segments) + new_par = self.duplicate() + new_par._window_ratio = all_to_one_ratio + new_ratios[all_to_one_i] = new_par + return new_ratios + + def __copy__(self): + new_w = SimpleWindowRatio(self.window_ratio, self.rect_split) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self._window_ratio, self.rect_split) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, SimpleWindowRatio) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'SimpleWindowRatio: [ratio: {}]'.format(self.window_ratio)
+ + +
[docs]class RepeatingWindowRatio(SimpleWindowRatio): + """Instructions for repeating windows derived from an area ratio with the base face. + + Args: + window_ratio: A number between 0 and 0.95 for the ratio between the window + area and the total facade area. + window_height: A number for the target height of the windows. + Note that, if the window ratio is too large for the height, the + ratio will take precedence and the actual window_height will + be larger than this value. + sill_height: A number for the target height above the bottom edge of + the rectangle to start the windows. Note that, if the + ratio is too large for the height, the ratio will take precedence + and the sill_height will be smaller than this value. + horizontal_separation: A number for the target separation between + individual window center lines. If this number is larger than + the parent rectangle base, only one window will be produced. + vertical_separation: An optional number to create a single vertical + separation between top and bottom windows. Default: 0. + + Properties: + * window_ratio + * window_height + * sill_height + * horizontal_separation + * vertical_separation + * user_data + """ + __slots__ = ('_window_height', '_sill_height', + '_horizontal_separation', '_vertical_separation') + + def __init__(self, window_ratio, window_height, sill_height, + horizontal_separation, vertical_separation=0): + """Initialize RepeatingWindowRatio.""" + _WindowParameterBase.__init__(self) # add the user_data + self._window_ratio = float_in_range(window_ratio, 0, 0.95, 'window ratio') + self._window_height = float_positive(window_height, 'window height') + self._sill_height = float_positive(sill_height, 'sill height') + self._horizontal_separation = \ + float_positive(horizontal_separation, 'window horizontal separation') + self._vertical_separation = \ + float_positive(vertical_separation, 'window vertical separation') + + @property + def window_height(self): + """Get a number or the target height of the windows.""" + return self._window_height + + @property + def sill_height(self): + """Get a number for the height above the bottom edge of the floor.""" + return self._sill_height + + @property + def horizontal_separation(self): + """Get a number for the separation between individual window center lines.""" + return self._horizontal_separation + + @property + def vertical_separation(self): + """Get a number for a vertical separation between top/bottom windows.""" + return self._vertical_separation + +
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): + """Get a version of these WindowParameters as RectangularWindows. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + if self._window_ratio == 0: + return None + base_plane = Plane(segment.v.rotate_xy(math.pi / 2), segment.p, segment.v) + sub_rects = Face3D.sub_rects_from_rect_ratio( + base_plane, segment.length, floor_to_ceiling_height, + self.window_ratio, self.window_height, self.sill_height, + self.horizontal_separation, self.vertical_separation + ) + origins, widths, heights = [], [], [] + for rect in sub_rects: + poly = Polygon2D(tuple(base_plane.xyz_to_xy(pt) for pt in rect)) + min_pt, max_pt = poly.min, poly.max + origins.append(min_pt) + widths.append(max_pt.x - min_pt.x) + heights.append(max_pt.y - min_pt.y) + new_w = RectangularWindows(origins, widths, heights) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def add_window_to_face(self, face, tolerance=0.01): + """Add Apertures to a Honeybee Face using these Window Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: The maximum difference between point values for them to be + considered a part of a rectangle. Default: 0.01, suitable for + objects in meters. + """ + if self._window_ratio == 0: + return None + face.apertures_by_ratio_rectangle( + self.window_ratio, self.window_height, self.sill_height, + self.horizontal_separation, self.vertical_separation, tolerance) + # if the Aperture is interior, set adjacent boundary condition + if isinstance(face._boundary_condition, Surface): + num_aps = face.apertures + for i, ap in enumerate(face.apertures): + ids = face._boundary_condition.boundary_condition_objects + adj_ap_id = '{}_Glz{}'.format(ids[0], num_aps - i - 1) + final_ids = (adj_ap_id,) + ids + ap.boundary_condition = Surface(final_ids, True) + self._apply_user_data_to_honeybee(face.apertures)
+ +
[docs] def scale(self, factor): + """Get a scaled version of these WindowParameters. + + This method is called within the scale methods of the Room2D. + + Args: + factor: A number representing how much the object should be scaled. + """ + new_w = RepeatingWindowRatio( + self.window_ratio, self.window_height * factor, self.sill_height * factor, + self.horizontal_separation * factor, self.vertical_separation * factor) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def split(self, segments, tolerance=0.01): + """Split RepeatingWindowRatio parameters across a list of ordered segments. + + Args: + segments: The segments to which the window parameters are being + split across. These should be in order as they appear on the + parent Room2D. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + # if one of the segments is much larger than the others, add all windows to that + new_ratios = self._all_to_primary_segment(segments) + if new_ratios is not None: + return new_ratios + # otherwise, just distribute the windows evenly + new_w_par = [self] * len(segments) + self._add_user_data(new_w_par) + return new_w_par
+ +
[docs] @staticmethod + def merge(window_parameters, segments, floor_to_ceiling_height): + """Merge RepeatingWindowRatio parameters together using their assigned segments. + + Args: + window_parameters: A list of WindowParameters to be merged. + segments: The segments to which the window parameters are assigned. + These should be in order as they appear on the parent Room2D. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segments belong. + """ + weights, heights, sill_heights, h_seps, v_seps = [], [], [], [], [] + win_area, wall_area = 0, 0 + for wp, s in zip(window_parameters, segments): + if wp is not None: + wall_a = s.length * floor_to_ceiling_height + wall_area += wall_a + if isinstance(wp, RepeatingWindowRatio): + win_area += (wall_area * wp.window_ratio) + weights.append(s.length) + heights.append(wp.window_height) + sill_heights.append(wp.sill_height) + h_seps.append(wp.horizontal_separation) + v_seps.append(wp.vertical_separation) + else: # not all windows are of the same type; convert all to rectangular + return _WindowParameterBase.merge_to_rectangular( + window_parameters, segments, floor_to_ceiling_height) + window_ratio = win_area / wall_area + tw = sum(weights) + weights = [w / tw for w in weights] + height = sum([h * w for h, w in zip(heights, weights)]) + sill_height = sum([h * w for h, w in zip(sill_heights, weights)]) + h_sep = sum([s * w for s, w in zip(h_seps, weights)]) + v_sep = sum([s * w for s, w in zip(v_seps, weights)]) + new_w = RepeatingWindowRatio(window_ratio, height, sill_height, h_sep, v_sep) + new_w._user_data = None if window_parameters[0].user_data is None else \ + window_parameters[0].user_data.copy() + return new_w
+ +
[docs] @classmethod + def from_dict(cls, data): + """Create RepeatingWindowRatio from a dictionary. + + .. code-block:: python + + { + "type": "RepeatingWindowRatio", + "window_ratio": 0.4, + "window_height": 2, + "sill_height": 0.8, + "horizontal_separation": 4, + "vertical_separation": 0.5 + } + """ + assert data['type'] == 'RepeatingWindowRatio', \ + 'Expected RepeatingWindowRatio dictionary. Got {}.'.format(data['type']) + vert = data['vertical_separation'] if 'vertical_separation' in data else 0 + new_w_par = cls(data['window_ratio'], data['window_height'], + data['sill_height'], data['horizontal_separation'], vert) + if 'user_data' in data and data['user_data'] is not None: + new_w_par.user_data = data['user_data'] + return new_w_par
+ +
[docs] def to_dict(self): + """Get RepeatingWindowRatio as a dictionary.""" + base = { + 'type': 'RepeatingWindowRatio', + 'window_ratio': self.window_ratio, + 'window_height': self.window_height, + 'sill_height': self.sill_height, + 'horizontal_separation': self.horizontal_separation + } + if self.vertical_separation != 0: + base['vertical_separation'] = self.vertical_separation + if self.user_data is not None: + base['user_data'] = self.user_data + return base
+ + def __copy__(self): + new_w = RepeatingWindowRatio( + self._window_ratio, self._window_height, self._sill_height, + self._horizontal_separation, self._vertical_separation) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self._window_ratio, self._window_height, self._sill_height, + self._horizontal_separation, self._vertical_separation) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, RepeatingWindowRatio) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'RepeatingWindowRatio: [ratio: {}] [window_height: {}] [sill_height:' \ + ' {}] [horizontal: {}] [vertical: {}]'.format( + self._window_ratio, self.window_height, self.sill_height, + self.horizontal_separation, self.vertical_separation)
+ + +
[docs]class RepeatingWindowWidthHeight(_WindowParameterBase): + """Instructions for repeating rectangular windows of a fixed width and height. + + This class effectively fills a wall with windows at the specified width, height + and separation. + + Args: + window_height: A number for the target height of the windows. + Note that, if the window_height is larger than the height of the wall, + the generated windows will have a height equal to the wall height + in order to avoid having windows extend outside the wall face. + window_width: A number for the target width of the windows. + Note that, if the window_width is larger than the width of the wall, + the generated windows will have a width equal to the wall width + in order to avoid having windows extend outside the wall face. + sill_height: A number for the target height above the bottom edge of + the wall to start the windows. If the window_height + is too large for the sill_height to fit within the rectangle, + the window_height will take precedence. + horizontal_separation: A number for the target separation between + individual window center lines. If this number is larger than + the parent rectangle base, only one window will be produced. + + Properties: + * window_height + * window_width + * sill_height + * horizontal_separation + * user_data + """ + __slots__ = ('_window_height', '_window_width', '_sill_height', + '_horizontal_separation') + + def __init__(self, window_height, window_width, sill_height, horizontal_separation): + """Initialize RepeatingWindowWidthHeight.""" + _WindowParameterBase.__init__(self) # add the user_data + self._window_height = float_positive(window_height, 'window height') + self._window_width = float_positive(window_width, 'window width') + self._sill_height = float_positive(sill_height, 'sill height') + self._horizontal_separation = \ + float_positive(horizontal_separation, 'window horizontal separation') + + @property + def window_height(self): + """Get a number or the target height of the windows.""" + return self._window_height + + @property + def window_width(self): + """Get a number or the target width of the windows.""" + return self._window_width + + @property + def sill_height(self): + """Get a number for the height above the bottom edge of the floor.""" + return self._sill_height + + @property + def horizontal_separation(self): + """Get a number for the separation between individual window center lines.""" + return self._horizontal_separation + +
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): + """Get the window area generated by these parameters from a LineSegment3D. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + hgt = floor_to_ceiling_height - 0.02 * floor_to_ceiling_height if \ + self.window_height >= floor_to_ceiling_height else self.window_height + max_width_break_up = segment.length / 2 + if self.window_width < max_width_break_up: # multiple windows + num_div = round(segment.length / self.horizontal_separation) if \ + segment.length > self.horizontal_separation / 2 else 1 + if num_div * self.window_width + (num_div - 1) * \ + (self.horizontal_separation - self.window_width) > segment.length: + num_div = math.floor(segment.length / self.horizontal_separation) + return num_div * self.window_width * hgt + else: # one single window + return segment.length * 0.98 * hgt
+ +
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): + """Get a version of these WindowParameters as RectangularWindows. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + base_plane = Plane(segment.v.rotate_xy(math.pi / 2), segment.p, segment.v) + sub_rects = Face3D.sub_rects_from_rect_dimensions( + base_plane, segment.length, floor_to_ceiling_height, self.window_height, + self.window_width, self.sill_height, self.horizontal_separation + ) + origins, widths, heights = [], [], [] + for rect in sub_rects: + poly = Polygon2D(tuple(base_plane.xyz_to_xy(pt) for pt in rect)) + min_pt, max_pt = poly.min, poly.max + origins.append(min_pt) + widths.append(max_pt.x - min_pt.x) + heights.append(max_pt.y - min_pt.y) + new_w = RectangularWindows(origins, widths, heights) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def add_window_to_face(self, face, tolerance=0.01): + """Add Apertures to a Honeybee Face using these Window Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: The maximum difference between point values for them to be + considered a part of a rectangle. Default: 0.01, suitable for + objects in meters. + """ + if self._window_width == 0 or self._window_height == 0: + return None + face.apertures_by_width_height_rectangle( + self.window_height, self.window_width, self.sill_height, + self.horizontal_separation, tolerance) + # if the Aperture is interior, set adjacent boundary condition + if isinstance(face._boundary_condition, Surface): + num_aps = face.apertures + for i, ap in enumerate(face.apertures): + ids = face._boundary_condition.boundary_condition_objects + adj_ap_id = '{}_Glz{}'.format(ids[0], num_aps - i - 1) + final_ids = (adj_ap_id,) + ids + ap.boundary_condition = Surface(final_ids, True) + self._apply_user_data_to_honeybee(face.apertures)
+ +
[docs] def scale(self, factor): + """Get a scaled version of these WindowParameters. + + This method is called within the scale methods of the Room2D. + + Args: + factor: A number representing how much the object should be scaled. + """ + new_w = RepeatingWindowWidthHeight( + self.window_height * factor, self.window_width * factor, + self.sill_height * factor, self.horizontal_separation * factor) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def split(self, segments, tolerance=0.01): + """Split RepeatingWindowWidthHeight parameters across a list of ordered segments. + + Args: + segments: The segments to which the window parameters are being + split across. These should be in order as they appear on the + parent Room2D. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + # just distribute the windows evenly + new_w_par = [self] * len(segments) + self._add_user_data(new_w_par) + return new_w_par
+ +
[docs] @staticmethod + def merge(window_parameters, segments, floor_to_ceiling_height): + """Merge RepeatingWindowWidthHeight parameters using their assigned segments. + + Args: + window_parameters: A list of WindowParameters to be merged. + segments: The segments to which the window parameters are assigned. + These should be in order as they appear on the parent Room2D. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segments belong. + """ + weights, heights, widths, sill_heights, h_seps = [], [], [], [], [] + for wp, s in zip(window_parameters, segments): + if wp is not None: + if isinstance(wp, RepeatingWindowWidthHeight): + weights.append(s.length) + heights.append(wp.window_height) + widths.append(wp.window_width) + sill_heights.append(wp.sill_height) + h_seps.append(wp.horizontal_separation) + else: # not all windows are of the same type; convert all to rectangular + return _WindowParameterBase.merge_to_rectangular( + window_parameters, segments, floor_to_ceiling_height) + tw = sum(weights) + weights = [w / tw for w in weights] + height = sum([h * w for h, w in zip(heights, weights)]) + width = sum([x * w for x, w in zip(widths, weights)]) + sill_height = sum([h * w for h, w in zip(sill_heights, weights)]) + h_sep = sum([s * w for s, w in zip(h_seps, weights)]) + new_w = RepeatingWindowWidthHeight(height, width, sill_height, h_sep) + new_w._user_data = None if window_parameters[0].user_data is None else \ + window_parameters[0].user_data.copy() + return new_w
+ +
[docs] @classmethod + def from_dict(cls, data): + """Create RepeatingWindowWidthHeight from a dictionary. + + .. code-block:: python + + { + "type": "RepeatingWindowWidthHeight", + "window_height": 2, + "window_width": 1.5, + "sill_height": 0.8, + "horizontal_separation": 4 + } + """ + assert data['type'] == 'RepeatingWindowWidthHeight', 'Expected ' \ + 'RepeatingWindowWidthHeight dictionary. Got {}.'.format(data['type']) + new_w_par = cls(data['window_height'], data['window_width'], + data['sill_height'], data['horizontal_separation']) + if 'user_data' in data and data['user_data'] is not None: + new_w_par.user_data = data['user_data'] + return new_w_par
+ +
[docs] def to_dict(self): + """Get RepeatingWindowWidthHeight as a dictionary.""" + base = { + 'type': 'RepeatingWindowWidthHeight', + 'window_height': self.window_height, + 'window_width': self.window_width, + 'sill_height': self.sill_height, + 'horizontal_separation': self.horizontal_separation + } + if self.user_data is not None: + base['user_data'] = self.user_data + return base
+ + def __copy__(self): + new_w = RepeatingWindowWidthHeight( + self._window_height, self._window_width, self._sill_height, + self._horizontal_separation) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self._window_height, self._window_width, self._sill_height, + self._horizontal_separation) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, RepeatingWindowWidthHeight) and \ + self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'RepeatingWindowWidthHeight: [window_height: {}] [window_width: ' \ + '{}] [sill_height: {}] [horizontal: {}]'.format( + self.window_height, self.window_width, self.sill_height, + self.horizontal_separation)
+ + +class _AsymmetricBase(_WindowParameterBase): + """Base class for WindowParameters that can make asymmetric windows on a wall. + """ + + def flip(self, seg_length): + """Flip the direction of the windows along a wall segment. + + This is needed since windows can exist asymmetrically across the wall + segment and operations like reflecting the Room2D across a plane will + require the window parameters to be flipped. Reversing the Room2D vertices + also requires flipping. + + Args: + seg_length: The length of the segment along which the parameters are + being flipped. + """ + return self + + def _merge_user_data(self, original_w_par): + """Build this object's user_data from those of merged window parameters. + + Args: + original_w_par: An array of the original window parameters used to + build this one. + """ + if not all((owp is None or owp.user_data is None) for owp in original_w_par): + new_u = {} + for ow_par in original_w_par: + if ow_par is not None and ow_par.user_data is not None: + for key, val in ow_par.user_data.items(): + if key not in new_u: + new_u[key] = val + elif isinstance(new_u[key], (list, tuple)) and \ + isinstance(val, (list, tuple)) and \ + len(val) >= len(ow_par): + new_u[key] = new_u[key] + val + self.user_data = new_u + + def _split_user_data(self, split_win_par, win_par_indices): + """Assign user_data to split WindowParameters with the help of split indices. + + Args: + split_win_par: An array of WindowParameters that were derived by + splitting this one. + win_par_indices: A list of lists where each sub-list represents one of + the WindowParameters in the split_win_par ean each item of each + sub-list represents one of the rectangles or polygons in the + window parameters. + """ + if self.user_data is not None: + for win_par, wp_i in zip(split_win_par, win_par_indices): + if win_par is None: + continue + u_dict = {} + for key, val in self.user_data.items(): + if isinstance(val, (list, tuple)) and len(val) >= len(self): + u_dict[key] = [self.user_data[key][j] for j in wp_i] + else: + u_dict[key] = val + win_par.user_data = u_dict + + +
[docs]class RectangularWindows(_AsymmetricBase): + """Instructions for several rectangular windows, defined by origin, width and height. + + Note that, if these parameters are applied to a base wall that is too short + or too narrow such that the windows fall outside the boundary of the wall, the + generated windows will automatically be shortened or excluded. This way, a + certain pattern of repeating rectangular windows can be encoded in a single + RectangularWindows instance and applied to multiple Room2D segments. + + Args: + origins: An array of ladybug_geometry Point2D objects within the plane + of the wall for the origin of each window. The wall plane is assumed + to have an origin at the first point of the wall segment and an + X-axis extending along the length of the segment. The wall plane's + Y-axis always points upwards. Therefore, both X and Y values of + each origin point should be positive. + widths: An array of positive numbers for the window widths. The length + of this list must match the length of the origins. + heights: An array of positive numbers for the window heights. The length + of this list must match the length of the origins. + are_doors: An array of booleans that align with the origins and note whether + each of the geometries represents a door (True) or a window (False). + If None, it will be assumed that all geometries represent windows and + they will be translated to Apertures in any resulting Honeybee + model. (Default: None). + + Properties: + * origins + * widths + * heights + * are_doors + * user_data + """ + __slots__ = ('_origins', '_widths', '_heights', '_are_doors') + + def __init__(self, origins, widths, heights, are_doors=None): + """Initialize RectangularWindows.""" + _WindowParameterBase.__init__(self) # add the user_data + if not isinstance(origins, tuple): + origins = tuple(origins) + for point in origins: + assert isinstance(point, Point2D), \ + 'Expected Point2D for window origin. Got {}'.format(type(point)) + self._origins = origins + + self._widths = tuple(float_positive(width, 'window width') for width in widths) + self._heights = tuple(float_positive(hgt, 'window height') for hgt in heights) + + assert len(self._origins) == len(self._widths) == len(self._heights), \ + 'Number of window origins, widths, and heights must match.' + assert len(self._origins) != 0, \ + 'There must be at least one window to use RectangularWindows.' + + if are_doors is None: + self._are_doors = (False,) * len(origins) + 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 ' \ + 'RectangularWindows.are_doors. Got {}'.format(type(is_dr)) + assert len(are_doors) == len(origins), \ + 'Length of RectangularWindows.are_doors ({}) does not match length ' \ + 'of RectangularWindows.origins ({}).'.format( + len(are_doors), len(origins)) + self._are_doors = are_doors + + @property + def origins(self): + """Get an array of Point2Ds within the wall plane for the origin of each window. + """ + return self._origins + + @property + def widths(self): + """Get an array of numbers for the window widths.""" + return self._widths + + @property + def heights(self): + """Get an array of numbers for the window heights.""" + return self._heights + + @property + def are_doors(self): + """Get an array of booleans that note whether each geometry is a door.""" + return self._are_doors + +
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): + """Get the window area generated by these parameters from a LineSegment3D. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + max_width = segment.length + max_height = floor_to_ceiling_height + + areas = [] + for o, width, height in zip(self.origins, self.widths, self.heights): + final_width = max_width - o.x if width + o.x > max_width else width + final_height = max_height - o.y if height + o.y > max_height else height + if final_height > 0 and final_height > 0: # inside wall boundary + areas.append(final_width * final_height) + + return sum(areas)
+ +
[docs] def check_window_overlaps(self, tolerance=0.01): + """Check whether any windows overlap with one another. + + Args: + tolerance: The minimum distance that two windows 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 rectangular parameters to gether + zip_obj = zip(self.origins, self.widths, self.heights) + rect_par = [(o, w, h) for o, w, h in zip_obj] + # loop through the polygons and check to see if it overlaps with the others + grouped_rects = [[rect_par[0]]] + for rect in rect_par[1:]: + group_found = False + for rect_group in grouped_rects: + for oth_rect in rect_group: + if self._rectangles_overlap(rect, oth_rect, tolerance): + rect_group.append(rect) + group_found = True + break + if group_found: + break + if not group_found: # the polygon does not overlap with any of the others + grouped_rects.append([rect]) # make a new group for the polygon + # report any polygons that overlap + if not all(len(g) == 1 for g in grouped_rects): + base_msg = '({} windows overlap one another)' + all_msg = [] + for r_group in grouped_rects: + if len(r_group) != 1: + all_msg.append(base_msg.format(len(r_group))) + return ' '.join(all_msg) + return ''
+ +
[docs] def check_valid_for_segment(self, segment, floor_to_ceiling_height): + """Check that these window parameters are valid for a given LineSegment3D. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + + Returns: + A string with the message. Will be an empty string if valid. + """ + total_area = segment.length * floor_to_ceiling_height + win_area = self.area_from_segment(segment, floor_to_ceiling_height) + if win_area >= total_area: + return 'Total area of windows [{}] is greater than the area of the ' \ + 'parent wall [{}].'.format(win_area, total_area) + return ''
+ +
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): + """Returns the class instance. Provided here for consistency with other classes. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + return self
+ +
[docs] def to_detailed_windows(self): + """Get a version of these WindowParameters as DetailedWindows.""" + polygons = [] + for o, w, h in zip(self.origins, self.widths, self.heights): + poly_pts = (o, Point2D(o.x + w, o.y), Point2D(o.x + w, o.y + h), + Point2D(o.x, o.y + h)) + polygons.append(Polygon2D(poly_pts)) + return DetailedWindows(polygons, self.are_doors)
+ +
[docs] def add_window_to_face(self, face, tolerance=0.01): + """Add Apertures to a Honeybee Face using these Window Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: Optional tolerance value. Default: 0.01, suitable for + objects in meters. + """ + # collect the global properties of the face that set limits on apertures + wall_plane = face.geometry.plane + wall_plane = wall_plane.rotate(wall_plane.n, math.pi, wall_plane.o) + width_seg = LineSegment3D.from_end_points(face.geometry[0], face.geometry[1]) + h_vec, b_pt = Vector3D(0, 0, face.max.z - face.min.z), face.geometry[1] + height_seg = LineSegment3D.from_end_points(b_pt, b_pt.move(h_vec)) + max_width = width_seg.length - tolerance + max_height = height_seg.length - tolerance + + # loop through each window and create its geometry + sub_faces = [] + zip_obj = zip(self.origins, self.widths, self.heights, self.are_doors) + for i, (o, wid, hgt, isd) in enumerate(zip_obj): + final_width = max_width - o.x if wid + o.x > max_width else wid + final_height = max_height - o.y if hgt + o.y > max_height else hgt + if final_height > 0 and final_height > 0: # inside wall boundary + base_plane = Plane(wall_plane.n, wall_plane.xy_to_xyz(o), wall_plane.x) + s_geo = Face3D.from_rectangle(final_width, final_height, base_plane) + if isd: + sub_f = Door('{}_Door{}'.format(face.identifier, i + 1), s_geo) + face.add_door(sub_f) + else: + sub_f = Aperture('{}_Glz{}'.format(face.identifier, i + 1), s_geo) + face.add_aperture(sub_f) + sub_faces.append(sub_f) + # if the Aperture is interior, set adjacent boundary condition + if isinstance(face._boundary_condition, Surface): + ids = face._boundary_condition.boundary_condition_objects + sf_type = 'Door' if isd else 'Glz' + adj_sf_id = '{}_{}{}'.format(ids[0], sf_type, i + 1) + final_ids = (adj_sf_id,) + ids + sub_f.boundary_condition = Surface(final_ids, True) + self._apply_user_data_to_honeybee(sub_faces)
+ +
[docs] def scale(self, factor): + """Get a scaled version of these WindowParameters. + + This method is called within the scale methods of the Room2D. + + Args: + factor: A number representing how much the object should be scaled. + """ + new_w = RectangularWindows( + tuple(Point2D(pt.x * factor, pt.y * factor) for pt in self.origins), + tuple(width * factor for width in self.widths), + tuple(height * factor for height in self.heights), + self.are_doors) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def flip(self, seg_length): + """Flip the direction of the windows along a segment. + + This is needed since windows can exist asymmetrically across the wall + segment and operations like reflecting the Room2D across a plane will + require the window parameters to be flipped to remain in the same place. + + Args: + seg_length: The length of the segment along which the parameters are + being flipped. + """ + new_origins = [] + new_widths = [] + new_heights = [] + new_are_doors = [] + zip_obj = zip(self.origins, self.widths, self.heights, self.are_doors) + for o, width, height, is_dr in zip_obj: + new_x = seg_length - o.x - width + if new_x > 0: # fully within the wall boundary + new_origins.append(Point2D(new_x, o.y)) + new_widths.append(width) + new_heights.append(height) + new_are_doors.append(is_dr) + elif new_x + width > seg_length * 0.001: # partially within the boundary + new_widths.append(width + new_x - (seg_length * 0.001)) + new_x = seg_length * 0.001 + new_origins.append(Point2D(new_x, o.y)) + new_heights.append(height) + new_are_doors.append(is_dr) + new_w = RectangularWindows(new_origins, new_widths, new_heights, new_are_doors) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def split(self, segments, tolerance=0.01): + """Split RectangularWindows parameters across a list of ordered segments. + + Args: + segments: The segments to which the window parameters are being + split across. These should be in order as they appear on the + parent Room2D. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + new_win_pars, win_par_is = [], [] + rel_pt, rel_w, rel_h = self.origins, self.widths, self.heights + rel_d, rel_i = self.are_doors, list(range(len(self.origins))) + for segment in segments: + # loop through the vertices and adjust them to the max width + win_par_i, out_i = [], [] + seg_len = segment.length + max_width = seg_len - tolerance + new_pts, new_w, new_h, new_d = [], [], [], [] + out_pts, out_w, out_h, out_d = [], [], [], [] + for pt, w, h, d, i in zip(rel_pt, rel_w, rel_h, rel_d, rel_i): + x_val = pt.x + if x_val >= max_width: # completely outside of the segment + out_pts.append(Point2D(x_val - seg_len, pt.y)) + out_w.append(w) + out_h.append(h) + out_d.append(d) + out_i.append(i) + elif x_val + w >= max_width: # split by segment + split_dist = max_width - x_val + new_pts.append(Point2D(x_val, pt.y)) + new_w.append(split_dist - tolerance) + new_h.append(h) + new_d.append(d) + win_par_i.append(i) + out_pts.append(Point2D(tolerance, pt.y)) + out_w.append(w - split_dist - (2 * tolerance)) + out_h.append(h) + out_d.append(d) + out_i.append(i) + else: # completely inside segment + new_pts.append(pt) + new_w.append(w) + new_h.append(h) + new_d.append(d) + win_par_i.append(i) + + # build the final window parameters from the adjusted windows + if len(new_pts) != 0: + new_win_pars.append(RectangularWindows(new_pts, new_w, new_h, new_d)) + else: + new_win_pars.append(None) + win_par_is.append(win_par_i) + # shift all windows to be relevant for the next segment + rel_pt, rel_w, rel_h, rel_d, rel_i = out_pts, out_w, out_h, out_d, out_i + + # apply the user_data to the split window parameters + self._split_user_data(new_win_pars, win_par_is) + return new_win_pars
+ +
[docs] def trim(self, original_segment, sub_segment, tolerance=0.01): + """Trim RectangularWindows for a sub segment given the original segment. + + Args: + original_segment: The original LineSegment3D to which the window + parameters are assigned. + sub_segment: A LineSegment3D that is a sub-segment of the original_segment, + which will be used to trim the window parameters to fit this segment. + Note that this sub_segment should have the same orientation as + the original segment. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + # shift all origins to be relevant for the sub segment + rel_pt = self.origins + shift_dist = original_segment.p1.distance_to_point(sub_segment.p1) + if shift_dist > tolerance: + rel_pt = [] + for pt in self.origins: + rel_pt.append(Point2D(pt.x - shift_dist, pt.y)) + # loop through the rectangles and trim them for the segment + max_width = sub_segment.length - tolerance + new_pts, new_w, new_h, new_d = [], [], [], [] + for pt, w, h, d in zip(rel_pt, self.widths, self.heights, self.are_doors): + x_val = pt.x + if x_val >= max_width or x_val + w < tolerance: + continue # completely outside of the segment + if x_val < tolerance: # split by segment on left + w = tolerance - x_val + x_val = tolerance + if x_val + w >= max_width: # split by segment on right + split_dist = max_width - x_val + new_pts.append(Point2D(x_val, pt.y)) + new_w.append(split_dist - tolerance) + new_h.append(h) + new_d.append(d) + else: # completely inside segment + new_pts.append(pt) + new_w.append(w) + new_h.append(h) + new_d.append(d) + # build the final window parameters from the adjusted windows + if len(new_pts) == 0: + return None + return RectangularWindows(new_pts, new_w, new_h, new_d)
+ +
[docs] @staticmethod + def merge(window_parameters, segments, floor_to_ceiling_height): + """Merge RectangularWindows parameters together using their assigned segments. + + Args: + window_parameters: A list of WindowParameters to be merged. + segments: The segments to which the window parameters are assigned. + These should be in order as they appear on the parent Room2D. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segments belong. + """ + new_w_par = _WindowParameterBase.merge_to_rectangular( + window_parameters, segments, floor_to_ceiling_height) + new_w_par._merge_user_data(window_parameters) + return new_w_par
+ +
[docs] @classmethod + def from_dict(cls, data): + """Create RectangularWindows from a dictionary. + + .. code-block:: python + + { + "type": "RectangularWindows", + "origins": [(1, 1), (3, 0.5)], # array of (x, y) floats in wall plane + "widths": [1, 3], # array of floats for window widths + "heights": [1, 2.5], # array of floats for window heights + "are_doors": [False, True] # array of booleans for whether it's a door + } + """ + assert data['type'] == 'RectangularWindows', \ + 'Expected RectangularWindows. Got {}.'.format(data['type']) + are_doors = data['are_doors'] if 'are_doors' in data else None + new_w_par = cls(tuple(Point2D.from_array(pt) for pt in data['origins']), + data['widths'], data['heights'], are_doors) + if 'user_data' in data and data['user_data'] is not None: + new_w_par.user_data = data['user_data'] + return new_w_par
+ +
[docs] def to_dict(self): + """Get RectangularWindows as a dictionary.""" + base = { + 'type': 'RectangularWindows', + 'origins': [pt.to_array() for pt in self.origins], + 'widths': self.widths, + 'heights': self.heights, + "are_doors": self.are_doors + } + if self.user_data is not None: + base['user_data'] = self.user_data + return base
+ + @staticmethod + def _rectangles_overlap(rect_par_1, rect_par_2, tolerance): + """Test if two rectangles overlap within a tolerance.""" + min_pt1, w1, h1 = rect_par_1 + max_pt1 = Point2D(min_pt1.x + w1, min_pt1.y + h1) + min_pt2, w2, h2 = rect_par_2 + max_pt2 = Point2D(min_pt2.x + w2, min_pt2.y + h2) + if min_pt2.x > max_pt1.x - tolerance or min_pt1.x > max_pt2.x - tolerance: + return False + if min_pt2.y > max_pt1.y - tolerance or min_pt1.y > max_pt2.y - tolerance: + return False + return True + + def __copy__(self): + new_w = RectangularWindows( + self.origins, self.widths, self.heights, self.are_doors) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (hash(self.origins), hash(self.widths), hash(self.heights), + hash(self.are_doors)) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, RectangularWindows) and \ + len(self._origins) == len(other._origins) + + def __ne__(self, other): + return not self.__eq__(other) + + def __len__(self): + return len(self.origins) + + def __repr__(self): + return 'RectangularWindows: [{} windows]'.format(len(self.origins))
+ + +
[docs]class DetailedWindows(_AsymmetricBase): + """Instructions for detailed windows, defined by 2D Polygons (lists of 2D vertices). + + Note that these parameters are intended to represent windows that are specific + to a particular segment and, unlike the other WindowParameters, this class + performs no automatic checks to ensure that the windows lie within the + boundary of the wall they have been assigned to. + + Args: + polygons: An array of ladybug_geometry Polygon2D objects within the plane + of the wall with one polygon for each window. The wall plane is + assumed to have an origin at the first point of the wall segment + and an X-axis extending along the length of the segment. The wall + plane's Y-axis always points upwards. Therefore, both X and Y + values of each point in the polygon should always be positive. + are_doors: An array of booleans that align with the polygons and note + whether each of the polygons represents a door (True) or a window (False). + If None, it will be assumed that all polygons represent windows and + they will be translated to Apertures in any resulting Honeybee + model. (Default: None). + + Properties: + * polygons + * are_doors + * user_data + + Usage: + Note that, if you are starting from 3D vertices of windows, you can + use this class to represent them. The DetailedWindows.from_face3ds is the + simplest way to do this. Otherwise, below is some sample code to convert + from vertices in the same 3D space as a vertical wall to vertices in the 2D + plane of the wall (as this class interprets it). + + In the code, 'seg_p1' is the first point of a given wall segment and + is assumed to be the origin of the wall plane. 'seg_p2' is the second + point of the wall segment, and 'vertex' is a given vertex of the + window that you want to translate from 3D coordinates into 2D. All + input points are presented as arrays of 3 floats and the output is + an array of 2 (x, y) coordinates. + + .. code-block:: python + + def dot_product(v1, v2): + return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[3] + + def normalize(v): + d = (v[0] ** 2 + v[1] ** 2 + v[2] ** 2) ** 0.5 + return (v[0] / d, v[1] / d, v[2] / d) + + def xyz_to_xy(seg_p1, seg_p2, vertex): + diff = (vertex[0] - seg_p1[0], vertex[1] - seg_p1[1], vertex[2] - seg_p1[2]) + axis = (seg_p2[0] - seg_p1[0], seg_p2[1] - seg_p1[1], seg_p2[2] - seg_p1[2]) + plane_x = normalize(axis) + plane_y = (0, 0, 1) + return (dot_product(plane_x , diff), dot_product(plane_y, diff)) + """ + __slots__ = ('_polygons', '_are_doors') + + def __init__(self, polygons, are_doors=None): + """Initialize DetailedWindows.""" + _WindowParameterBase.__init__(self) # add the user_data + if not isinstance(polygons, tuple): + polygons = tuple(polygons) + for polygon in polygons: + assert isinstance(polygon, Polygon2D), \ + 'Expected Polygon2D for window polygon. Got {}'.format(type(polygon)) + assert len(polygons) != 0, \ + 'There must be at least one polygon to use DetailedWindows.' + 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 ' \ + 'DetailedWindow.are_doors. Got {}'.format(type(is_dr)) + assert len(are_doors) == len(polygons), \ + 'Length of DetailedWindow.are_doors ({}) does not match the length ' \ + 'of DetailedWindow.polygons ({}).'.format(len(are_doors), len(polygons)) + self._are_doors = are_doors + +
[docs] @classmethod + def from_face3ds(cls, face3ds, segment, are_doors=None): + """Create DetailedWindows from Face3Ds and a segment they are assigned to. + + Args: + face3ds: A list of Face3D objects for the detailed windows. + segment: A LineSegment3D that sets the plane in which 3D vertices + are located. + are_doors: An array of booleans that align with the face3ds and note whether + each of the polygons represents a door (True) or a window (False). + If None, it will be assumed that all polygons represent windows and + they will be translated to Apertures in any resulting Honeybee + model. (Default: None). + """ + plane = Plane(Vector3D(segment.v.y, -segment.v.x, 0), segment.p, segment.v) + pt3d = tuple(tuple(pt for pt in face.vertices) for face in face3ds) + return cls( + tuple(Polygon2D(tuple(plane.xyz_to_xy(pt) for pt in poly)) + for poly in pt3d), + are_doors + )
+ + @property + def polygons(self): + """Get an array of Polygon2Ds with one polygon for each window.""" + 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 is_flipped_equivalent(self, other, segment, tolerance=0.01): + """Check if this WindowParameter is equal to another when flipped. + + This is useful to know if the window parameters will be valid when translated + to Honeybee when they are adjacent to the other. + + Args: + other: Another WindowParameter object for which flipped equivalency + will be tested. + segment: A LineSegment3D to which these parameters are applied. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + # first check some fundamental things + if not isinstance(other, DetailedWindows) or \ + len(self._polygons) != len(other._polygons): + return False + # then, check that the polygons actually match + other_flip = other.flip(segment.length) + found_adjacencies = 0 + for poly_1 in self._polygons: + for poly_2 in other_flip._polygons: + if poly_1.center.distance_to_point(poly_2.center) <= tolerance: + found_adjacencies += 1 + break + if len(self._polygons) == found_adjacencies: + return True + return False
+ +
[docs] def area_from_segment(self, segment, floor_to_ceiling_height): + """Get the window area generated by these parameters from a LineSegment3D. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + return sum(polygon.area for polygon in self._polygons)
+ +
[docs] def check_window_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 = '({} windows 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 window 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: + new_geo = polygon.remove_colinear_vertices(tolerance) + if new_geo.is_self_intersecting: + self_int_i.append(str(i)) + if len(self_int_i) != 0: + return 'Window polygons with the following indices are ' \ + 'self-intersecting: ({})'.format(' '.join(self_int_i)) + return ''
+ +
[docs] def check_valid_for_segment(self, segment, floor_to_ceiling_height): + """Check that these window parameters are valid for a given LineSegment3D. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + + Returns: + A string with the message. Will be an empty string if valid. + """ + # first check that the total window area isn't larger than the wall + max_width = segment.length + total_area = max_width * floor_to_ceiling_height + win_area = self.area_from_segment(segment, floor_to_ceiling_height) + if win_area >= total_area: + return 'Total area of windows [{}] is greater than the area of the ' \ + 'parent wall [{}].'.format(win_area, total_area) + # next, check to be sure that no window is out of the wall boundary + msg_template = 'Vertex 2D {} coordinate [{}] is outside the range allowed ' \ + 'by the parent wall segment.' + for p_gon in self.polygons: + min_pt, max_pt = p_gon.min, p_gon.max + if min_pt.x <= 0: + return msg_template.format('X', min_pt.x) + if min_pt.y <= 0: + return msg_template.format('Y', min_pt.y) + if max_pt.x >= max_width: + return msg_template.format('X', max_pt.x) + if max_pt.y >= floor_to_ceiling_height: + return msg_template.format('Y', max_pt.y) + return ''
+ +
[docs] def adjust_for_segment(self, segment, floor_to_ceiling_height, tolerance=0.01, + sliver_tolerance=None): + """Get these parameters with vertices excluded beyond the domain of a given line. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + sliver_tolerance: A number to be used for the tolerance at which sliver + polygons should be removed if they are created during the adjustment + process. If None, the tolerance will be used. (Default: None). + + Returns: + A new DetailedWindows object that fits entirely in the domain of the + input line segment and floor_to_ceiling_height. + """ + # compute the maximum width and height + seg_len = segment.length + double_tol = 2 * tolerance + if seg_len - double_tol < 0 or floor_to_ceiling_height - double_tol < 0: + return None + max_width = seg_len - tolerance + max_height = floor_to_ceiling_height - tolerance + + # loop through the vertices and adjust them + sliver_tol = sliver_tolerance if sliver_tolerance is not None else tolerance + new_polygons, new_are_doors, kept_i = [], [], [] + for i, (p_gon, is_dr) in enumerate(zip(self.polygons, self.are_doors)): + new_verts = [] + for vert in p_gon: + x_val, y_val = vert.x, vert.y + if x_val <= tolerance: + x_val = tolerance + if y_val <= tolerance: + y_val = tolerance + if x_val >= max_width: + x_val = max_width + if y_val >= max_height: + y_val = max_height + new_verts.append(Point2D(x_val, y_val)) + new_poly = Polygon2D(new_verts) + if new_poly.area > sliver_tol: + new_polygons.append(new_poly) + new_are_doors.append(is_dr) + kept_i.append(i) + + # return the final window parameters + new_w_par = None + if len(new_polygons) != 0: + new_w_par = DetailedWindows(new_polygons, new_are_doors) + + # update user_data lists if some windows were not added + if new_w_par is not None and self.user_data is not None: + clean_u = self.user_data + if len(new_polygons) != len(self.polygons): + clean_u = {} + for key, val in self.user_data.items(): + if isinstance(val, (list, tuple)) and len(val) >= len(self.polygons): + clean_u[key] = [val[j] for j in kept_i] + else: + clean_u[key] = val + new_w_par.user_data = clean_u + return new_w_par
+ +
[docs] def to_rectangular_windows(self, segment, floor_to_ceiling_height): + """Get a version of these WindowParameters as RectangularWindows. + + This will simply translate each window polygon to its own rectangular + window. For merging windows that touch or overlap one another into + rectangles, the merge_to_bounding_rectangle method should be used + before using this method. + + Args: + segment: A LineSegment3D to which these parameters are applied. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + origins, widths, heights = [], [], [] + for poly in self.polygons: + min_pt, max_pt = poly.min, poly.max + origins.append(min_pt) + widths.append(max_pt.x - min_pt.x) + heights.append(max_pt.y - min_pt.y) + new_w = RectangularWindows(origins, widths, heights, self.are_doors) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def add_window_to_face(self, face, tolerance=0.01): + """Add Apertures to a Honeybee Face using these Window Parameters. + + Args: + face: A honeybee-core Face object. + tolerance: Optional tolerance value. Default: 0.01, suitable for + objects in meters. + """ + # get the plane of the parent wall + wall_plane = face.geometry.plane + wall_plane = wall_plane.rotate(wall_plane.n, math.pi, wall_plane.o) + width_seg = LineSegment3D.from_end_points(face.geometry[0], face.geometry[1]) + h_vec, b_pt = Vector3D(0, 0, face.max.z - face.min.z), face.geometry[1] + height_seg = LineSegment3D.from_end_points(b_pt, b_pt.move(h_vec)) + max_width = width_seg.length - tolerance + max_height = height_seg.length - tolerance + + # automatically clip any geometries outside of the face so they are bounded by it + clean_polygons, clean_are_doors, kept_i = [], [], [] + for i, (p_gon, is_dr) in enumerate(zip(self.polygons, self.are_doors)): + new_verts, verts_moved = [], [] + for vert in p_gon: + x_val, y_val, v_moved = vert.x, vert.y, False + if x_val <= tolerance: + x_val, v_moved = tolerance, True + if y_val <= tolerance: + y_val, v_moved = tolerance, True + if x_val >= max_width: + x_val, v_moved = max_width, True + if y_val >= max_height: + y_val, v_moved = max_height, True + new_verts.append(Point2D(x_val, y_val)) + verts_moved.append(v_moved) + if not all(verts_moved): + clean_polygons.append(new_verts) + clean_are_doors.append(is_dr) + kept_i.append(i) + + # update user_data lists if some windows were not added + clean_u = self.user_data + if self.user_data is not None: + if len(clean_polygons) != len(self.polygons): + clean_u = {} + for key, val in self.user_data.items(): + if isinstance(val, (list, tuple)) and len(val) >= len(self.polygons): + clean_u[key] = [val[j] for j in kept_i] + else: + clean_u[key] = val + + # loop through each window and create its geometry + sub_faces = [] + for i, (polygon, isd) in enumerate(zip(clean_polygons, clean_are_doors)): + pt3d = tuple(wall_plane.xy_to_xyz(pt) for pt in polygon) + s_geo = Face3D(pt3d) + if isd: + sub_f = Door('{}_Door{}'.format(face.identifier, i + 1), s_geo) + face.add_door(sub_f) + else: + sub_f = Aperture('{}_Glz{}'.format(face.identifier, i + 1), s_geo) + face.add_aperture(sub_f) + sub_faces.append(sub_f) + # if the Aperture is interior, set adjacent boundary condition + if isinstance(face._boundary_condition, Surface): + ids = face._boundary_condition.boundary_condition_objects + sf_type = 'Door' if isd else 'Glz' + adj_sf_id = '{}_{}{}'.format(ids[0], sf_type, i + 1) + final_ids = (adj_sf_id,) + ids + sub_f.boundary_condition = Surface(final_ids, True) + self._apply_user_data_to_honeybee(sub_faces, clean_u)
+ +
[docs] def scale(self, factor): + """Get a scaled version of these WindowParameters. + + This method is called within the scale methods of the Room2D. + + Args: + factor: A number representing how much the object should be scaled. + """ + new_w = DetailedWindows( + tuple(polygon.scale(factor) for polygon in self.polygons), self.are_doors) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def flip(self, seg_length): + """Flip the direction of the windows along a segment. + + This is needed since windows can exist asymmetrically across the wall + segment and operations like reflecting the Room2D across a plane will + require the window parameters to be flipped to remain in the same place. + + Args: + seg_length: The length of the segment along which the parameters are + being flipped. + """ + # set values derived from the property of the segment + normal = Vector2D(1, 0) + origin = Point2D(seg_length / 2, 0) + + # loop through the polygons and reflect them across the mid plane of the wall + 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)))) + new_w = DetailedWindows(new_polygons, self.are_doors) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w
+ +
[docs] def union_overlaps(self, tolerance=0.01): + """Union any window 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 window polygons that are close to one another into a single polygon. + + This can be used to create a simpler set of windows that is easier to + edit and is in the same location as the original windows. + + Args: + max_separation: A number for the maximum distance between window 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 windows 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 window 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 split(self, segments, tolerance=0.01): + """Split DetailedWindows parameters across a list of ordered segments. + + Args: + segments: The segments to which the window parameters are being + split across. These should be in order as they appear on the + parent Room2D. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + new_win_pars, win_par_is = [], [] + rel_polygons, rel_dr = self.polygons, self.are_doors + rel_i = list(range(len(self.polygons))) + for segment in segments: + # loop through the vertices and adjust them to the max width + max_width = segment.length - tolerance + win_par_i, out_i = [], [] + new_polygons, new_dr, out_polygons, out_dr = [], [], [], [] + for p_gon, is_dr, i in zip(rel_polygons, rel_dr, rel_i): + new_verts, verts_moved = [], [] + for vert in p_gon: + x_val, v_moved = vert.x, False + if x_val < tolerance: + x_val, v_moved = tolerance, True + if x_val >= max_width: + x_val, v_moved = max_width, True + new_verts.append(Point2D(x_val, vert.y)) + verts_moved.append(v_moved) + if not all(verts_moved): + new_polygons.append(Polygon2D(new_verts)) + win_par_i.append(i) + new_dr.append(is_dr) + if True in verts_moved: # outside of the segment + out_polygons.append(p_gon) + out_dr.append(is_dr) + out_i.append(i) + else: + out_polygons.append(p_gon) + out_dr.append(is_dr) + out_i.append(i) + # build the final window parameters from the adjusted polygons + if len(new_polygons) != 0: + new_win_pars.append(DetailedWindows(new_polygons, new_dr)) + else: + new_win_pars.append(None) + win_par_is.append(win_par_i) + # shift all polygons to be relevant for the next segment + shift_dist = segment.length + rel_polygons = [] + for p_gon in out_polygons: + new_v = [Point2D(p.x - shift_dist, p.y) for p in p_gon] + rel_polygons.append(Polygon2D(new_v)) + rel_dr = out_dr + rel_i = out_i + + # apply the user_data to the split window parameters + self._split_user_data(new_win_pars, win_par_is) + return new_win_pars
+ +
[docs] def trim(self, original_segment, sub_segment, tolerance=0.01): + """Trim DetailedWindows for a sub segment given the original segment. + + Args: + original_segment: The original LineSegment3D to which the window + parameters are assigned. + sub_segment: A LineSegment3D that is a sub-segment of the original_segment, + which will be used to trim the window parameters to fit this segment. + Note that this sub_segment should have the same orientation as + the original segment. + tolerance: The minimum distance between a vertex and the edge of the + wall segment that is considered not touching. (Default: 0.01, suitable + for objects in meters). + """ + # shift all polygons to be relevant for the sub segment + rel_polygons = self.polygons + shift_dist = original_segment.p1.distance_to_point(sub_segment.p1) + if shift_dist > tolerance: + rel_polygons = [] + for p_gon in self.polygons: + new_v = [Point2D(p.x - shift_dist, p.y) for p in p_gon] + rel_polygons.append(Polygon2D(new_v)) + # loop through the polygon vertices and trim them for the segment + max_width = sub_segment.length - tolerance + new_polygons, new_dr, out_polygons, out_dr = [], [], [], [] + for p_gon, is_dr in zip(rel_polygons, self.are_doors): + new_verts, verts_moved = [], [] + for vert in p_gon: + x_val, v_moved = vert.x, False + if x_val < tolerance: + x_val, v_moved = tolerance, True + if x_val >= max_width: + x_val, v_moved = max_width, True + new_verts.append(Point2D(x_val, vert.y)) + verts_moved.append(v_moved) + if not all(verts_moved): + new_polygons.append(Polygon2D(new_verts)) + new_dr.append(is_dr) + if True in verts_moved: # outside of the segment + out_polygons.append(p_gon) + out_dr.append(is_dr) + else: + out_polygons.append(p_gon) + out_dr.append(is_dr) + # build the final window parameters from the adjusted polygons + if len(new_polygons) == 0: + return None + return DetailedWindows(new_polygons, new_dr)
+ +
[docs] @staticmethod + def merge(window_parameters, segments, floor_to_ceiling_height): + """Merge DetailedWindows parameters together using their assigned segments. + + Args: + window_parameters: A list of WindowParameters to be merged. + segments: The segments to which the window parameters are assigned. + These should be in order as they appear on the parent Room2D. + floor_to_ceiling_height: The floor-to-ceiling height of the Room2D + to which the segment belongs. + """ + base_x = 0 + polygons, are_doors = [], [] + for wp, s in zip(window_parameters, segments): + if wp is not None: + if isinstance(wp, DetailedWindows): + for p_gon, is_dr in zip(wp.polygons, wp.are_doors): + new_pts = [Point2D(pt.x + base_x, pt.y) for pt in p_gon.vertices] + polygons.append(Polygon2D(new_pts)) + are_doors.append(is_dr) + else: # not all windows are of the same type; convert all to rectangular + return _WindowParameterBase.merge_to_rectangular( + window_parameters, segments, floor_to_ceiling_height) + base_x += s.length + if len(polygons) == 0: # all of the input window parameters were None + return None + new_w_par = DetailedWindows(polygons, are_doors) + new_w_par._merge_user_data(window_parameters) + return new_w_par
+ +
[docs] @classmethod + def from_dict(cls, data, segment=None): + """Create DetailedWindows from a dictionary. + + Args: + data: A dictionary in the format below. The vertices of the "polygons" + can either contain 2 values (indicating they are vertices within + the plane of a parent wall segment) or they can contain 3 values + (indicating they are 3D world coordinates). If 3 values are used, + a segment must be specified below to convert them to the 2D format. + segment: A LineSegment3D that sets the plane in which 3D vertices + are located. + + .. code-block:: python + + { + "type": "DetailedWindows", + "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'] == 'DetailedWindows', \ + 'Expected DetailedWindows dictionary. Got {}.'.format(data['type']) + if len(data['polygons']) == 0: + return None # empty object; just treat it as no windows + are_doors = data['are_doors'] if 'are_doors' in data else None + if len(data['polygons'][0][0]) == 2: + new_w_par = cls( + tuple(Polygon2D(tuple(Point2D.from_array(pt) for pt in poly)) + for poly in data['polygons']), + are_doors + ) + else: + assert segment is not None, 'Segment must be specified when using 3D ' \ + 'vertices for DetailedWindows.' + plane = Plane(Vector3D(segment.v.y, -segment.v.x, 0), segment.p, segment.v) + pt3d = tuple(tuple(Point3D.from_array(pt) for pt in poly) + for poly in data['polygons']) + new_w_par = cls( + tuple(Polygon2D(tuple(plane.xyz_to_xy(pt) for pt in poly)) + for poly in pt3d), + are_doors + ) + if 'user_data' in data and data['user_data'] is not None: + new_w_par.user_data = data['user_data'] + return new_w_par
+ +
[docs] def to_dict(self): + """Get DetailedWindows as a dictionary.""" + base = { + 'type': 'DetailedWindows', + 'polygons': [[pt.to_array() for pt in poly] for poly in self.polygons], + 'are_doors': self.are_doors + } + if self.user_data is not None: + base['user_data'] = self.user_data + return base
+ +
[docs] @staticmethod + def is_face3d_in_segment_plane( + face3d, segment, height, tolerance=0.01, angle_tolerance=1): + """Check if a given Face3D is in the plane and range of a LineSegment3D. + + Args: + face3d: A list of Face3D objects for the detailed windows. + segment: A LineSegment3D that sets the plane in which 3D vertices + are located. + height: A number for the height of the wall formed by extruding the segment. + tolerance: The minimum difference between the coordinate values of two + vertices at which they can be considered equivalent. Default: 0.01, + suitable for objects in meters. + angle_tolerance: The max angle in degrees that the plane normals can + differ from one another in order for them to be considered coplanar. + Default: 1 degree. + + Returns: + True if the face3d is in the plane and range of the segment. False + if it is not. + """ + plane = Plane(Vector3D(segment.v.y, -segment.v.x, 0), segment.p, segment.v) + # test whether the face3d is coplanar + if not plane.is_coplanar_tolerance(face3d.plane, tolerance, angle_tolerance): + return False + seg_face = Face3D.from_rectangle(segment.length, height, plane) + return seg_face.is_sub_face(face3d, tolerance, angle_tolerance)
+ + 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): # case of no 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) + + 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): + new_w = DetailedWindows(self._polygons, self._are_doors) + new_w._user_data = None if self.user_data is None else self.user_data.copy() + return new_w + + 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, DetailedWindows) and \ + len(self._polygons) == len(other._polygons) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'DetailedWindows: [{} windows]'.format(len(self._polygons))
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/docs/_modules/index.html b/docs/_modules/index.html new file mode 100644 index 00000000..9cccfa18 --- /dev/null +++ b/docs/_modules/index.html @@ -0,0 +1,988 @@ + + + + + + + Overview: module code — dragonfly-core documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ + +
+
+ + + \ No newline at end of file diff --git a/docs/_sources/cli/create.rst.txt b/docs/_sources/cli/create.rst.txt new file mode 100644 index 00000000..e5b85b38 --- /dev/null +++ b/docs/_sources/cli/create.rst.txt @@ -0,0 +1,6 @@ +create +====== + +.. click:: dragonfly.cli.create:create + :prog: dragonfly create + :show-nested: diff --git a/docs/_sources/cli/edit.rst.txt b/docs/_sources/cli/edit.rst.txt new file mode 100644 index 00000000..97c68a8a --- /dev/null +++ b/docs/_sources/cli/edit.rst.txt @@ -0,0 +1,6 @@ +edit +==== + +.. click:: dragonfly.cli.edit:edit + :prog: dragonfly edit + :show-nested: diff --git a/docs/_sources/cli/index.rst.txt b/docs/_sources/cli/index.rst.txt new file mode 100644 index 00000000..b29f05e0 --- /dev/null +++ b/docs/_sources/cli/index.rst.txt @@ -0,0 +1,18 @@ +CLI Docs +======== + +Installation +------------ + +To check if the Dragonfly command line interface is installed correctly use ``dragonfly viz`` and you +should get a ``viiiiiiiiiiiiizzzzzzzzz!`` back in response! + +Commands +-------- +.. toctree:: + :maxdepth: 1 + + translate + edit + create + validate diff --git a/docs/_sources/cli/translate.rst.txt b/docs/_sources/cli/translate.rst.txt new file mode 100644 index 00000000..403324b5 --- /dev/null +++ b/docs/_sources/cli/translate.rst.txt @@ -0,0 +1,6 @@ +translate +========= + +.. click:: dragonfly.cli.translate:translate + :prog: dragonfly translate + :show-nested: diff --git a/docs/_sources/cli/validate.rst.txt b/docs/_sources/cli/validate.rst.txt new file mode 100644 index 00000000..49e42c78 --- /dev/null +++ b/docs/_sources/cli/validate.rst.txt @@ -0,0 +1,6 @@ +validate +======== + +.. click:: dragonfly.cli.validate:validate + :prog: dragonfly validate + :show-nested: diff --git a/docs/_sources/dragonfly.building.rst.txt b/docs/_sources/dragonfly.building.rst.txt new file mode 100644 index 00000000..c941f285 --- /dev/null +++ b/docs/_sources/dragonfly.building.rst.txt @@ -0,0 +1,7 @@ +dragonfly.building module +========================= + +.. automodule:: dragonfly.building + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.cli.create.rst.txt b/docs/_sources/dragonfly.cli.create.rst.txt new file mode 100644 index 00000000..6dcad6b6 --- /dev/null +++ b/docs/_sources/dragonfly.cli.create.rst.txt @@ -0,0 +1,7 @@ +dragonfly.cli.create module +=========================== + +.. automodule:: dragonfly.cli.create + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.cli.edit.rst.txt b/docs/_sources/dragonfly.cli.edit.rst.txt new file mode 100644 index 00000000..206e8b53 --- /dev/null +++ b/docs/_sources/dragonfly.cli.edit.rst.txt @@ -0,0 +1,7 @@ +dragonfly.cli.edit module +========================= + +.. automodule:: dragonfly.cli.edit + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.cli.rst.txt b/docs/_sources/dragonfly.cli.rst.txt new file mode 100644 index 00000000..82d65b81 --- /dev/null +++ b/docs/_sources/dragonfly.cli.rst.txt @@ -0,0 +1,21 @@ +dragonfly.cli package +===================== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + dragonfly.cli.create + dragonfly.cli.edit + dragonfly.cli.translate + dragonfly.cli.validate + +Module contents +--------------- + +.. automodule:: dragonfly.cli + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.cli.translate.rst.txt b/docs/_sources/dragonfly.cli.translate.rst.txt new file mode 100644 index 00000000..592e1238 --- /dev/null +++ b/docs/_sources/dragonfly.cli.translate.rst.txt @@ -0,0 +1,7 @@ +dragonfly.cli.translate module +============================== + +.. automodule:: dragonfly.cli.translate + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.cli.validate.rst.txt b/docs/_sources/dragonfly.cli.validate.rst.txt new file mode 100644 index 00000000..1735da52 --- /dev/null +++ b/docs/_sources/dragonfly.cli.validate.rst.txt @@ -0,0 +1,7 @@ +dragonfly.cli.validate module +============================= + +.. automodule:: dragonfly.cli.validate + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.colorobj.rst.txt b/docs/_sources/dragonfly.colorobj.rst.txt new file mode 100644 index 00000000..880a951e --- /dev/null +++ b/docs/_sources/dragonfly.colorobj.rst.txt @@ -0,0 +1,7 @@ +dragonfly.colorobj module +========================= + +.. automodule:: dragonfly.colorobj + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.config.rst.txt b/docs/_sources/dragonfly.config.rst.txt new file mode 100644 index 00000000..cddd8348 --- /dev/null +++ b/docs/_sources/dragonfly.config.rst.txt @@ -0,0 +1,7 @@ +dragonfly.config module +======================= + +.. automodule:: dragonfly.config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.context.rst.txt b/docs/_sources/dragonfly.context.rst.txt new file mode 100644 index 00000000..701d0f0f --- /dev/null +++ b/docs/_sources/dragonfly.context.rst.txt @@ -0,0 +1,7 @@ +dragonfly.context module +======================== + +.. automodule:: dragonfly.context + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.dictutil.rst.txt b/docs/_sources/dragonfly.dictutil.rst.txt new file mode 100644 index 00000000..7af0f90a --- /dev/null +++ b/docs/_sources/dragonfly.dictutil.rst.txt @@ -0,0 +1,7 @@ +dragonfly.dictutil module +========================= + +.. automodule:: dragonfly.dictutil + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.extensionutil.rst.txt b/docs/_sources/dragonfly.extensionutil.rst.txt new file mode 100644 index 00000000..678f2b8a --- /dev/null +++ b/docs/_sources/dragonfly.extensionutil.rst.txt @@ -0,0 +1,7 @@ +dragonfly.extensionutil module +============================== + +.. automodule:: dragonfly.extensionutil + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.model.rst.txt b/docs/_sources/dragonfly.model.rst.txt new file mode 100644 index 00000000..a253e7ea --- /dev/null +++ b/docs/_sources/dragonfly.model.rst.txt @@ -0,0 +1,7 @@ +dragonfly.model module +====================== + +.. automodule:: dragonfly.model + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.projection.rst.txt b/docs/_sources/dragonfly.projection.rst.txt new file mode 100644 index 00000000..588a9677 --- /dev/null +++ b/docs/_sources/dragonfly.projection.rst.txt @@ -0,0 +1,7 @@ +dragonfly.projection module +=========================== + +.. automodule:: dragonfly.projection + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.properties.rst.txt b/docs/_sources/dragonfly.properties.rst.txt new file mode 100644 index 00000000..95c98c66 --- /dev/null +++ b/docs/_sources/dragonfly.properties.rst.txt @@ -0,0 +1,7 @@ +dragonfly.properties module +=========================== + +.. automodule:: dragonfly.properties + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.roof.rst.txt b/docs/_sources/dragonfly.roof.rst.txt new file mode 100644 index 00000000..31ecbc53 --- /dev/null +++ b/docs/_sources/dragonfly.roof.rst.txt @@ -0,0 +1,7 @@ +dragonfly.roof module +===================== + +.. automodule:: dragonfly.roof + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.room2d.rst.txt b/docs/_sources/dragonfly.room2d.rst.txt new file mode 100644 index 00000000..e8107a4d --- /dev/null +++ b/docs/_sources/dragonfly.room2d.rst.txt @@ -0,0 +1,7 @@ +dragonfly.room2d module +======================= + +.. automodule:: dragonfly.room2d + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.rst.txt b/docs/_sources/dragonfly.rst.txt new file mode 100644 index 00000000..fc8d43bb --- /dev/null +++ b/docs/_sources/dragonfly.rst.txt @@ -0,0 +1,42 @@ +dragonfly package +================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + dragonfly.cli + dragonfly.writer + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + dragonfly.building + dragonfly.colorobj + dragonfly.config + dragonfly.context + dragonfly.dictutil + dragonfly.extensionutil + dragonfly.model + dragonfly.projection + dragonfly.properties + dragonfly.roof + dragonfly.room2d + dragonfly.shadingparameter + dragonfly.skylightparameter + dragonfly.story + dragonfly.subdivide + dragonfly.windowparameter + +Module contents +--------------- + +.. automodule:: dragonfly + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.shadingparameter.rst.txt b/docs/_sources/dragonfly.shadingparameter.rst.txt new file mode 100644 index 00000000..a7764ecd --- /dev/null +++ b/docs/_sources/dragonfly.shadingparameter.rst.txt @@ -0,0 +1,7 @@ +dragonfly.shadingparameter module +================================= + +.. automodule:: dragonfly.shadingparameter + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.skylightparameter.rst.txt b/docs/_sources/dragonfly.skylightparameter.rst.txt new file mode 100644 index 00000000..148d7724 --- /dev/null +++ b/docs/_sources/dragonfly.skylightparameter.rst.txt @@ -0,0 +1,7 @@ +dragonfly.skylightparameter module +================================== + +.. automodule:: dragonfly.skylightparameter + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.story.rst.txt b/docs/_sources/dragonfly.story.rst.txt new file mode 100644 index 00000000..fd076d80 --- /dev/null +++ b/docs/_sources/dragonfly.story.rst.txt @@ -0,0 +1,7 @@ +dragonfly.story module +====================== + +.. automodule:: dragonfly.story + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.subdivide.rst.txt b/docs/_sources/dragonfly.subdivide.rst.txt new file mode 100644 index 00000000..e5871621 --- /dev/null +++ b/docs/_sources/dragonfly.subdivide.rst.txt @@ -0,0 +1,7 @@ +dragonfly.subdivide module +========================== + +.. automodule:: dragonfly.subdivide + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.windowparameter.rst.txt b/docs/_sources/dragonfly.windowparameter.rst.txt new file mode 100644 index 00000000..0dbfa1a5 --- /dev/null +++ b/docs/_sources/dragonfly.windowparameter.rst.txt @@ -0,0 +1,7 @@ +dragonfly.windowparameter module +================================ + +.. automodule:: dragonfly.windowparameter + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.writer.building.rst.txt b/docs/_sources/dragonfly.writer.building.rst.txt new file mode 100644 index 00000000..3c558cea --- /dev/null +++ b/docs/_sources/dragonfly.writer.building.rst.txt @@ -0,0 +1,7 @@ +dragonfly.writer.building module +================================ + +.. automodule:: dragonfly.writer.building + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.writer.context.rst.txt b/docs/_sources/dragonfly.writer.context.rst.txt new file mode 100644 index 00000000..6ddd464f --- /dev/null +++ b/docs/_sources/dragonfly.writer.context.rst.txt @@ -0,0 +1,7 @@ +dragonfly.writer.context module +=============================== + +.. automodule:: dragonfly.writer.context + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.writer.model.rst.txt b/docs/_sources/dragonfly.writer.model.rst.txt new file mode 100644 index 00000000..09e586d8 --- /dev/null +++ b/docs/_sources/dragonfly.writer.model.rst.txt @@ -0,0 +1,7 @@ +dragonfly.writer.model module +============================= + +.. automodule:: dragonfly.writer.model + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.writer.room2d.rst.txt b/docs/_sources/dragonfly.writer.room2d.rst.txt new file mode 100644 index 00000000..c00a43fa --- /dev/null +++ b/docs/_sources/dragonfly.writer.room2d.rst.txt @@ -0,0 +1,7 @@ +dragonfly.writer.room2d module +============================== + +.. automodule:: dragonfly.writer.room2d + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.writer.rst.txt b/docs/_sources/dragonfly.writer.rst.txt new file mode 100644 index 00000000..fc2ad56a --- /dev/null +++ b/docs/_sources/dragonfly.writer.rst.txt @@ -0,0 +1,22 @@ +dragonfly.writer package +======================== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + dragonfly.writer.building + dragonfly.writer.context + dragonfly.writer.model + dragonfly.writer.room2d + dragonfly.writer.story + +Module contents +--------------- + +.. automodule:: dragonfly.writer + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/dragonfly.writer.story.rst.txt b/docs/_sources/dragonfly.writer.story.rst.txt new file mode 100644 index 00000000..49b41b53 --- /dev/null +++ b/docs/_sources/dragonfly.writer.story.rst.txt @@ -0,0 +1,7 @@ +dragonfly.writer.story module +============================= + +.. automodule:: dragonfly.writer.story + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt new file mode 100644 index 00000000..ca8fc687 --- /dev/null +++ b/docs/_sources/index.rst.txt @@ -0,0 +1,59 @@ +Welcome to Dragonfly's documentation! +========================================= + +.. image:: http://www.ladybug.tools/assets/img/dragonfly.png + +Dragonfly is a collection of Python libraries to create representations of buildings +following `dragonfly-schema `_. +It abstracts the capabilities of `honeybee-core `_ +to make it easier to construct models on the urban scale. + + +Installation +============ + +To install the core library use ``pip install -U dragonfly-core``. + +To check if the Dragonfly command line interface is installed correctly use ``dragonfly viz`` and you +should get a ``viiiiiiiiiiiiizzzzzzzzz!`` back in response! + + +Documentation +============= + +This document includes `Dragonfly API documentation <#dragonfly>`_ and +`Dragonfly Command Line Interface <#cli-docs>`_ documentation for ``dragonfly core`` and does +not include the documentation for dragonfly extensions. For each extension refer to +extension's documentation page. + +Here are a number of popular Dragonfly extensions: + +- `dragonfly-energy `_ + + +CLI Docs +============= + +For command line interface documentation and API documentation see the pages below. + + +.. toctree:: + :maxdepth: 2 + + cli/index + + +dragonfly +============= + +.. toctree:: + :maxdepth: 4 + + modules + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/_sources/modules.rst.txt b/docs/_sources/modules.rst.txt new file mode 100644 index 00000000..c025c661 --- /dev/null +++ b/docs/_sources/modules.rst.txt @@ -0,0 +1,7 @@ +dragonfly +========= + +.. toctree:: + :maxdepth: 4 + + dragonfly diff --git a/docs/_static/_sphinx_javascript_frameworks_compat.js b/docs/_static/_sphinx_javascript_frameworks_compat.js new file mode 100644 index 00000000..8549469d --- /dev/null +++ b/docs/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1,134 @@ +/* + * _sphinx_javascript_frameworks_compat.js + * ~~~~~~~~~~ + * + * Compatability shim for jQuery and underscores.js. + * + * WILL BE REMOVED IN Sphinx 6.0 + * xref RemovedInSphinx60Warning + * + */ + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} diff --git a/docs/_static/basic.css b/docs/_static/basic.css new file mode 100644 index 00000000..eeb0519a --- /dev/null +++ b/docs/_static/basic.css @@ -0,0 +1,899 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} +a.brackets:before, +span.brackets > a:before{ + content: "["; +} + +a.brackets:after, +span.brackets > a:after { + content: "]"; +} + + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} +dl.footnote > dt, +dl.citation > dt { + float: left; + margin-right: 0.5em; +} + +dl.footnote > dd, +dl.citation > dd { + margin-bottom: 0em; +} + +dl.footnote > dd:after, +dl.citation > dd:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} +dl.field-list > dt:after { + content: ":"; +} + + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.css b/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.css new file mode 100644 index 00000000..09e88ce3 --- /dev/null +++ b/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.css @@ -0,0 +1,1109 @@ +/*! + * Bootstrap Responsive v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +@-ms-viewport { + width: device-width; +} + +.hidden { + display: none; + visibility: hidden; +} + +.visible-phone { + display: none !important; +} + +.visible-tablet { + display: none !important; +} + +.hidden-desktop { + display: none !important; +} + +.visible-desktop { + display: inherit !important; +} + +@media (min-width: 768px) and (max-width: 979px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important ; + } + .visible-tablet { + display: inherit !important; + } + .hidden-tablet { + display: none !important; + } +} + +@media (max-width: 767px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important; + } + .visible-phone { + display: inherit !important; + } + .hidden-phone { + display: none !important; + } +} + +.visible-print { + display: none !important; +} + +@media print { + .visible-print { + display: inherit !important; + } + .hidden-print { + display: none !important; + } +} + +@media (min-width: 1200px) { + .row { + margin-left: -30px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 30px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 1170px; + } + .span12 { + width: 1170px; + } + .span11 { + width: 1070px; + } + .span10 { + width: 970px; + } + .span9 { + width: 870px; + } + .span8 { + width: 770px; + } + .span7 { + width: 670px; + } + .span6 { + width: 570px; + } + .span5 { + width: 470px; + } + .span4 { + width: 370px; + } + .span3 { + width: 270px; + } + .span2 { + width: 170px; + } + .span1 { + width: 70px; + } + .offset12 { + margin-left: 1230px; + } + .offset11 { + margin-left: 1130px; + } + .offset10 { + margin-left: 1030px; + } + .offset9 { + margin-left: 930px; + } + .offset8 { + margin-left: 830px; + } + .offset7 { + margin-left: 730px; + } + .offset6 { + margin-left: 630px; + } + .offset5 { + margin-left: 530px; + } + .offset4 { + margin-left: 430px; + } + .offset3 { + margin-left: 330px; + } + .offset2 { + margin-left: 230px; + } + .offset1 { + margin-left: 130px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.564102564102564%; + *margin-left: 2.5109110747408616%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.564102564102564%; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.45299145299145%; + *width: 91.39979996362975%; + } + .row-fluid .span10 { + width: 82.90598290598291%; + *width: 82.8527914166212%; + } + .row-fluid .span9 { + width: 74.35897435897436%; + *width: 74.30578286961266%; + } + .row-fluid .span8 { + width: 65.81196581196582%; + *width: 65.75877432260411%; + } + .row-fluid .span7 { + width: 57.26495726495726%; + *width: 57.21176577559556%; + } + .row-fluid .span6 { + width: 48.717948717948715%; + *width: 48.664757228587014%; + } + .row-fluid .span5 { + width: 40.17094017094017%; + *width: 40.11774868157847%; + } + .row-fluid .span4 { + width: 31.623931623931625%; + *width: 31.570740134569924%; + } + .row-fluid .span3 { + width: 23.076923076923077%; + *width: 23.023731587561375%; + } + .row-fluid .span2 { + width: 14.52991452991453%; + *width: 14.476723040552828%; + } + .row-fluid .span1 { + width: 5.982905982905983%; + *width: 5.929714493544281%; + } + .row-fluid .offset12 { + margin-left: 105.12820512820512%; + *margin-left: 105.02182214948171%; + } + .row-fluid .offset12:first-child { + margin-left: 102.56410256410257%; + *margin-left: 102.45771958537915%; + } + .row-fluid .offset11 { + margin-left: 96.58119658119658%; + *margin-left: 96.47481360247316%; + } + .row-fluid .offset11:first-child { + margin-left: 94.01709401709402%; + *margin-left: 93.91071103837061%; + } + .row-fluid .offset10 { + margin-left: 88.03418803418803%; + *margin-left: 87.92780505546462%; + } + .row-fluid .offset10:first-child { + margin-left: 85.47008547008548%; + *margin-left: 85.36370249136206%; + } + .row-fluid .offset9 { + margin-left: 79.48717948717949%; + *margin-left: 79.38079650845607%; + } + .row-fluid .offset9:first-child { + margin-left: 76.92307692307693%; + *margin-left: 76.81669394435352%; + } + .row-fluid .offset8 { + margin-left: 70.94017094017094%; + *margin-left: 70.83378796144753%; + } + .row-fluid .offset8:first-child { + margin-left: 68.37606837606839%; + *margin-left: 68.26968539734497%; + } + .row-fluid .offset7 { + margin-left: 62.393162393162385%; + *margin-left: 62.28677941443899%; + } + .row-fluid .offset7:first-child { + margin-left: 59.82905982905982%; + *margin-left: 59.72267685033642%; + } + .row-fluid .offset6 { + margin-left: 53.84615384615384%; + *margin-left: 53.739770867430444%; + } + .row-fluid .offset6:first-child { + margin-left: 51.28205128205128%; + *margin-left: 51.175668303327875%; + } + .row-fluid .offset5 { + margin-left: 45.299145299145295%; + *margin-left: 45.1927623204219%; + } + .row-fluid .offset5:first-child { + margin-left: 42.73504273504273%; + *margin-left: 42.62865975631933%; + } + .row-fluid .offset4 { + margin-left: 36.75213675213675%; + *margin-left: 36.645753773413354%; + } + .row-fluid .offset4:first-child { + margin-left: 34.18803418803419%; + *margin-left: 34.081651209310785%; + } + .row-fluid .offset3 { + margin-left: 28.205128205128204%; + *margin-left: 28.0987452264048%; + } + .row-fluid .offset3:first-child { + margin-left: 25.641025641025642%; + *margin-left: 25.53464266230224%; + } + .row-fluid .offset2 { + margin-left: 19.65811965811966%; + *margin-left: 19.551736679396257%; + } + .row-fluid .offset2:first-child { + margin-left: 17.094017094017094%; + *margin-left: 16.98763411529369%; + } + .row-fluid .offset1 { + margin-left: 11.11111111111111%; + *margin-left: 11.004728132387708%; + } + .row-fluid .offset1:first-child { + margin-left: 8.547008547008547%; + *margin-left: 8.440625568285142%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 30px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 1156px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 1056px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 956px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 856px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 756px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 656px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 556px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 456px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 356px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 256px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 156px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 56px; + } + .thumbnails { + margin-left: -30px; + } + .thumbnails > li { + margin-left: 30px; + } + .row-fluid .thumbnails { + margin-left: 0; + } +} + +@media (min-width: 768px) and (max-width: 979px) { + .row { + margin-left: -20px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 724px; + } + .span12 { + width: 724px; + } + .span11 { + width: 662px; + } + .span10 { + width: 600px; + } + .span9 { + width: 538px; + } + .span8 { + width: 476px; + } + .span7 { + width: 414px; + } + .span6 { + width: 352px; + } + .span5 { + width: 290px; + } + .span4 { + width: 228px; + } + .span3 { + width: 166px; + } + .span2 { + width: 104px; + } + .span1 { + width: 42px; + } + .offset12 { + margin-left: 764px; + } + .offset11 { + margin-left: 702px; + } + .offset10 { + margin-left: 640px; + } + .offset9 { + margin-left: 578px; + } + .offset8 { + margin-left: 516px; + } + .offset7 { + margin-left: 454px; + } + .offset6 { + margin-left: 392px; + } + .offset5 { + margin-left: 330px; + } + .offset4 { + margin-left: 268px; + } + .offset3 { + margin-left: 206px; + } + .offset2 { + margin-left: 144px; + } + .offset1 { + margin-left: 82px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.7624309392265194%; + *margin-left: 2.709239449864817%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.7624309392265194%; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.43646408839778%; + *width: 91.38327259903608%; + } + .row-fluid .span10 { + width: 82.87292817679558%; + *width: 82.81973668743387%; + } + .row-fluid .span9 { + width: 74.30939226519337%; + *width: 74.25620077583166%; + } + .row-fluid .span8 { + width: 65.74585635359117%; + *width: 65.69266486422946%; + } + .row-fluid .span7 { + width: 57.18232044198895%; + *width: 57.12912895262725%; + } + .row-fluid .span6 { + width: 48.61878453038674%; + *width: 48.56559304102504%; + } + .row-fluid .span5 { + width: 40.05524861878453%; + *width: 40.00205712942283%; + } + .row-fluid .span4 { + width: 31.491712707182323%; + *width: 31.43852121782062%; + } + .row-fluid .span3 { + width: 22.92817679558011%; + *width: 22.87498530621841%; + } + .row-fluid .span2 { + width: 14.3646408839779%; + *width: 14.311449394616199%; + } + .row-fluid .span1 { + width: 5.801104972375691%; + *width: 5.747913483013988%; + } + .row-fluid .offset12 { + margin-left: 105.52486187845304%; + *margin-left: 105.41847889972962%; + } + .row-fluid .offset12:first-child { + margin-left: 102.76243093922652%; + *margin-left: 102.6560479605031%; + } + .row-fluid .offset11 { + margin-left: 96.96132596685082%; + *margin-left: 96.8549429881274%; + } + .row-fluid .offset11:first-child { + margin-left: 94.1988950276243%; + *margin-left: 94.09251204890089%; + } + .row-fluid .offset10 { + margin-left: 88.39779005524862%; + *margin-left: 88.2914070765252%; + } + .row-fluid .offset10:first-child { + margin-left: 85.6353591160221%; + *margin-left: 85.52897613729868%; + } + .row-fluid .offset9 { + margin-left: 79.8342541436464%; + *margin-left: 79.72787116492299%; + } + .row-fluid .offset9:first-child { + margin-left: 77.07182320441989%; + *margin-left: 76.96544022569647%; + } + .row-fluid .offset8 { + margin-left: 71.2707182320442%; + *margin-left: 71.16433525332079%; + } + .row-fluid .offset8:first-child { + margin-left: 68.50828729281768%; + *margin-left: 68.40190431409427%; + } + .row-fluid .offset7 { + margin-left: 62.70718232044199%; + *margin-left: 62.600799341718584%; + } + .row-fluid .offset7:first-child { + margin-left: 59.94475138121547%; + *margin-left: 59.838368402492065%; + } + .row-fluid .offset6 { + margin-left: 54.14364640883978%; + *margin-left: 54.037263430116376%; + } + .row-fluid .offset6:first-child { + margin-left: 51.38121546961326%; + *margin-left: 51.27483249088986%; + } + .row-fluid .offset5 { + margin-left: 45.58011049723757%; + *margin-left: 45.47372751851417%; + } + .row-fluid .offset5:first-child { + margin-left: 42.81767955801105%; + *margin-left: 42.71129657928765%; + } + .row-fluid .offset4 { + margin-left: 37.01657458563536%; + *margin-left: 36.91019160691196%; + } + .row-fluid .offset4:first-child { + margin-left: 34.25414364640884%; + *margin-left: 34.14776066768544%; + } + .row-fluid .offset3 { + margin-left: 28.45303867403315%; + *margin-left: 28.346655695309746%; + } + .row-fluid .offset3:first-child { + margin-left: 25.69060773480663%; + *margin-left: 25.584224756083227%; + } + .row-fluid .offset2 { + margin-left: 19.88950276243094%; + *margin-left: 19.783119783707537%; + } + .row-fluid .offset2:first-child { + margin-left: 17.12707182320442%; + *margin-left: 17.02068884448102%; + } + .row-fluid .offset1 { + margin-left: 11.32596685082873%; + *margin-left: 11.219583872105325%; + } + .row-fluid .offset1:first-child { + margin-left: 8.56353591160221%; + *margin-left: 8.457152932878806%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 710px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 648px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 586px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 524px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 462px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 400px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 338px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 276px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 214px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 152px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 90px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 28px; + } +} + +@media (max-width: 767px) { + body { + padding-right: 20px; + padding-left: 20px; + } + .navbar-fixed-top, + .navbar-fixed-bottom, + .navbar-static-top { + margin-right: -20px; + margin-left: -20px; + } + .container-fluid { + padding: 0; + } + .dl-horizontal dt { + float: none; + width: auto; + clear: none; + text-align: left; + } + .dl-horizontal dd { + margin-left: 0; + } + .container { + width: auto; + } + .row-fluid { + width: 100%; + } + .row, + .thumbnails { + margin-left: 0; + } + .thumbnails > li { + float: none; + margin-left: 0; + } + [class*="span"], + .uneditable-input[class*="span"], + .row-fluid [class*="span"] { + display: block; + float: none; + width: 100%; + margin-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .span12, + .row-fluid .span12 { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="offset"]:first-child { + margin-left: 0; + } + .input-large, + .input-xlarge, + .input-xxlarge, + input[class*="span"], + select[class*="span"], + textarea[class*="span"], + .uneditable-input { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .input-prepend input, + .input-append input, + .input-prepend input[class*="span"], + .input-append input[class*="span"] { + display: inline-block; + width: auto; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 0; + } + .modal { + position: fixed; + top: 20px; + right: 20px; + left: 20px; + width: auto; + margin: 0; + } + .modal.fade { + top: -100px; + } + .modal.fade.in { + top: 20px; + } +} + +@media (max-width: 480px) { + .nav-collapse { + -webkit-transform: translate3d(0, 0, 0); + } + .page-header h1 small { + display: block; + line-height: 20px; + } + input[type="checkbox"], + input[type="radio"] { + border: 1px solid #ccc; + } + .form-horizontal .control-label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + .form-horizontal .controls { + margin-left: 0; + } + .form-horizontal .control-list { + padding-top: 0; + } + .form-horizontal .form-actions { + padding-right: 10px; + padding-left: 10px; + } + .media .pull-left, + .media .pull-right { + display: block; + float: none; + margin-bottom: 10px; + } + .media-object { + margin-right: 0; + margin-left: 0; + } + .modal { + top: 10px; + right: 10px; + left: 10px; + } + .modal-header .close { + padding: 10px; + margin: -10px; + } + .carousel-caption { + position: static; + } +} + +@media (max-width: 979px) { + body { + padding-top: 0; + } + .navbar-fixed-top, + .navbar-fixed-bottom { + position: static; + } + .navbar-fixed-top { + margin-bottom: 20px; + } + .navbar-fixed-bottom { + margin-top: 20px; + } + .navbar-fixed-top .navbar-inner, + .navbar-fixed-bottom .navbar-inner { + padding: 5px; + } + .navbar .container { + width: auto; + padding: 0; + } + .navbar .brand { + padding-right: 10px; + padding-left: 10px; + margin: 0 0 0 -5px; + } + .nav-collapse { + clear: both; + } + .nav-collapse .nav { + float: none; + margin: 0 0 10px; + } + .nav-collapse .nav > li { + float: none; + } + .nav-collapse .nav > li > a { + margin-bottom: 2px; + } + .nav-collapse .nav > .divider-vertical { + display: none; + } + .nav-collapse .nav .nav-header { + color: #777777; + text-shadow: none; + } + .nav-collapse .nav > li > a, + .nav-collapse .dropdown-menu a { + padding: 9px 15px; + font-weight: bold; + color: #777777; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + .nav-collapse .btn { + padding: 4px 10px 4px; + font-weight: normal; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } + .nav-collapse .dropdown-menu li + li a { + margin-bottom: 2px; + } + .nav-collapse .nav > li > a:hover, + .nav-collapse .nav > li > a:focus, + .nav-collapse .dropdown-menu a:hover, + .nav-collapse .dropdown-menu a:focus { + background-color: #f2f2f2; + } + .navbar-inverse .nav-collapse .nav > li > a, + .navbar-inverse .nav-collapse .dropdown-menu a { + color: #999999; + } + .navbar-inverse .nav-collapse .nav > li > a:hover, + .navbar-inverse .nav-collapse .nav > li > a:focus, + .navbar-inverse .nav-collapse .dropdown-menu a:hover, + .navbar-inverse .nav-collapse .dropdown-menu a:focus { + background-color: #111111; + } + .nav-collapse.in .btn-group { + padding: 0; + margin-top: 5px; + } + .nav-collapse .dropdown-menu { + position: static; + top: auto; + left: auto; + display: none; + float: none; + max-width: none; + padding: 0; + margin: 0 15px; + background-color: transparent; + border: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + .nav-collapse .open > .dropdown-menu { + display: block; + } + .nav-collapse .dropdown-menu:before, + .nav-collapse .dropdown-menu:after { + display: none; + } + .nav-collapse .dropdown-menu .divider { + display: none; + } + .nav-collapse .nav > li > .dropdown-menu:before, + .nav-collapse .nav > li > .dropdown-menu:after { + display: none; + } + .nav-collapse .navbar-form, + .nav-collapse .navbar-search { + float: none; + padding: 10px 15px; + margin: 10px 0; + border-top: 1px solid #f2f2f2; + border-bottom: 1px solid #f2f2f2; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar-inverse .nav-collapse .navbar-form, + .navbar-inverse .nav-collapse .navbar-search { + border-top-color: #111111; + border-bottom-color: #111111; + } + .navbar .nav-collapse .nav.pull-right { + float: none; + margin-left: 0; + } + .nav-collapse, + .nav-collapse.collapse { + height: 0; + overflow: hidden; + } + .navbar .btn-navbar { + display: block; + } + .navbar-static .navbar-inner { + padding-right: 10px; + padding-left: 10px; + } +} + +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + overflow: visible !important; + } +} diff --git a/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.min.css b/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.min.css new file mode 100644 index 00000000..f4ede63f --- /dev/null +++ b/docs/_static/bootstrap-2.3.2/css/bootstrap-responsive.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap Responsive v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} diff --git a/docs/_static/bootstrap-2.3.2/css/bootstrap.css b/docs/_static/bootstrap-2.3.2/css/bootstrap.css new file mode 100644 index 00000000..b725064a --- /dev/null +++ b/docs/_static/bootstrap-2.3.2/css/bootstrap.css @@ -0,0 +1,6167 @@ +/*! + * Bootstrap v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +audio:not([controls]) { + display: none; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:hover, +a:active { + outline: 0; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + width: auto\9; + height: auto; + max-width: 100%; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} + +#map_canvas img, +.google-maps img { + max-width: none; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +button, +input { + *overflow: visible; + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +label, +select, +button, +input[type="button"], +input[type="reset"], +input[type="submit"], +input[type="radio"], +input[type="checkbox"] { + cursor: pointer; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +@media print { + * { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page { + margin: 0.5cm; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } +} + +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; + color: #333333; + background-color: #ffffff; +} + +a { + color: #0088cc; + text-decoration: none; +} + +a:hover, +a:focus { + color: #005580; + text-decoration: underline; +} + +.img-rounded { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.img-polaroid { + padding: 4px; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.img-circle { + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + border-radius: 500px; +} + +.row { + margin-left: -20px; + *zoom: 1; +} + +.row:before, +.row:after { + display: table; + line-height: 0; + content: ""; +} + +.row:after { + clear: both; +} + +[class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; +} + +.container, +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.span12 { + width: 940px; +} + +.span11 { + width: 860px; +} + +.span10 { + width: 780px; +} + +.span9 { + width: 700px; +} + +.span8 { + width: 620px; +} + +.span7 { + width: 540px; +} + +.span6 { + width: 460px; +} + +.span5 { + width: 380px; +} + +.span4 { + width: 300px; +} + +.span3 { + width: 220px; +} + +.span2 { + width: 140px; +} + +.span1 { + width: 60px; +} + +.offset12 { + margin-left: 980px; +} + +.offset11 { + margin-left: 900px; +} + +.offset10 { + margin-left: 820px; +} + +.offset9 { + margin-left: 740px; +} + +.offset8 { + margin-left: 660px; +} + +.offset7 { + margin-left: 580px; +} + +.offset6 { + margin-left: 500px; +} + +.offset5 { + margin-left: 420px; +} + +.offset4 { + margin-left: 340px; +} + +.offset3 { + margin-left: 260px; +} + +.offset2 { + margin-left: 180px; +} + +.offset1 { + margin-left: 100px; +} + +.row-fluid { + width: 100%; + *zoom: 1; +} + +.row-fluid:before, +.row-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.row-fluid:after { + clear: both; +} + +.row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.127659574468085%; + *margin-left: 2.074468085106383%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.row-fluid [class*="span"]:first-child { + margin-left: 0; +} + +.row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.127659574468085%; +} + +.row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; +} + +.row-fluid .span11 { + width: 91.48936170212765%; + *width: 91.43617021276594%; +} + +.row-fluid .span10 { + width: 82.97872340425532%; + *width: 82.92553191489361%; +} + +.row-fluid .span9 { + width: 74.46808510638297%; + *width: 74.41489361702126%; +} + +.row-fluid .span8 { + width: 65.95744680851064%; + *width: 65.90425531914893%; +} + +.row-fluid .span7 { + width: 57.44680851063829%; + *width: 57.39361702127659%; +} + +.row-fluid .span6 { + width: 48.93617021276595%; + *width: 48.88297872340425%; +} + +.row-fluid .span5 { + width: 40.42553191489362%; + *width: 40.37234042553192%; +} + +.row-fluid .span4 { + width: 31.914893617021278%; + *width: 31.861702127659576%; +} + +.row-fluid .span3 { + width: 23.404255319148934%; + *width: 23.351063829787233%; +} + +.row-fluid .span2 { + width: 14.893617021276595%; + *width: 14.840425531914894%; +} + +.row-fluid .span1 { + width: 6.382978723404255%; + *width: 6.329787234042553%; +} + +.row-fluid .offset12 { + margin-left: 104.25531914893617%; + *margin-left: 104.14893617021275%; +} + +.row-fluid .offset12:first-child { + margin-left: 102.12765957446808%; + *margin-left: 102.02127659574467%; +} + +.row-fluid .offset11 { + margin-left: 95.74468085106382%; + *margin-left: 95.6382978723404%; +} + +.row-fluid .offset11:first-child { + margin-left: 93.61702127659574%; + *margin-left: 93.51063829787232%; +} + +.row-fluid .offset10 { + margin-left: 87.23404255319149%; + *margin-left: 87.12765957446807%; +} + +.row-fluid .offset10:first-child { + margin-left: 85.1063829787234%; + *margin-left: 84.99999999999999%; +} + +.row-fluid .offset9 { + margin-left: 78.72340425531914%; + *margin-left: 78.61702127659572%; +} + +.row-fluid .offset9:first-child { + margin-left: 76.59574468085106%; + *margin-left: 76.48936170212764%; +} + +.row-fluid .offset8 { + margin-left: 70.2127659574468%; + *margin-left: 70.10638297872339%; +} + +.row-fluid .offset8:first-child { + margin-left: 68.08510638297872%; + *margin-left: 67.9787234042553%; +} + +.row-fluid .offset7 { + margin-left: 61.70212765957446%; + *margin-left: 61.59574468085106%; +} + +.row-fluid .offset7:first-child { + margin-left: 59.574468085106375%; + *margin-left: 59.46808510638297%; +} + +.row-fluid .offset6 { + margin-left: 53.191489361702125%; + *margin-left: 53.085106382978715%; +} + +.row-fluid .offset6:first-child { + margin-left: 51.063829787234035%; + *margin-left: 50.95744680851063%; +} + +.row-fluid .offset5 { + margin-left: 44.68085106382979%; + *margin-left: 44.57446808510638%; +} + +.row-fluid .offset5:first-child { + margin-left: 42.5531914893617%; + *margin-left: 42.4468085106383%; +} + +.row-fluid .offset4 { + margin-left: 36.170212765957444%; + *margin-left: 36.06382978723405%; +} + +.row-fluid .offset4:first-child { + margin-left: 34.04255319148936%; + *margin-left: 33.93617021276596%; +} + +.row-fluid .offset3 { + margin-left: 27.659574468085104%; + *margin-left: 27.5531914893617%; +} + +.row-fluid .offset3:first-child { + margin-left: 25.53191489361702%; + *margin-left: 25.425531914893618%; +} + +.row-fluid .offset2 { + margin-left: 19.148936170212764%; + *margin-left: 19.04255319148936%; +} + +.row-fluid .offset2:first-child { + margin-left: 17.02127659574468%; + *margin-left: 16.914893617021278%; +} + +.row-fluid .offset1 { + margin-left: 10.638297872340425%; + *margin-left: 10.53191489361702%; +} + +.row-fluid .offset1:first-child { + margin-left: 8.51063829787234%; + *margin-left: 8.404255319148938%; +} + +[class*="span"].hide, +.row-fluid [class*="span"].hide { + display: none; +} + +[class*="span"].pull-right, +.row-fluid [class*="span"].pull-right { + float: right; +} + +.container { + margin-right: auto; + margin-left: auto; + *zoom: 1; +} + +.container:before, +.container:after { + display: table; + line-height: 0; + content: ""; +} + +.container:after { + clear: both; +} + +.container-fluid { + padding-right: 20px; + padding-left: 20px; + *zoom: 1; +} + +.container-fluid:before, +.container-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.container-fluid:after { + clear: both; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 21px; + font-weight: 200; + line-height: 30px; +} + +small { + font-size: 85%; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +cite { + font-style: normal; +} + +.muted { + color: #999999; +} + +a.muted:hover, +a.muted:focus { + color: #808080; +} + +.text-warning { + color: #c09853; +} + +a.text-warning:hover, +a.text-warning:focus { + color: #a47e3c; +} + +.text-error { + color: #b94a48; +} + +a.text-error:hover, +a.text-error:focus { + color: #953b39; +} + +.text-info { + color: #3a87ad; +} + +a.text-info:hover, +a.text-info:focus { + color: #2d6987; +} + +.text-success { + color: #468847; +} + +a.text-success:hover, +a.text-success:focus { + color: #356635; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 10px 0; + font-family: inherit; + font-weight: bold; + line-height: 20px; + color: inherit; + text-rendering: optimizelegibility; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1, +h2, +h3 { + line-height: 40px; +} + +h1 { + font-size: 38.5px; +} + +h2 { + font-size: 31.5px; +} + +h3 { + font-size: 24.5px; +} + +h4 { + font-size: 17.5px; +} + +h5 { + font-size: 14px; +} + +h6 { + font-size: 11.9px; +} + +h1 small { + font-size: 24.5px; +} + +h2 small { + font-size: 17.5px; +} + +h3 small { + font-size: 14px; +} + +h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 20px 0 30px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + padding: 0; + margin: 0 0 10px 25px; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} + +li { + line-height: 20px; +} + +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} + +ul.inline, +ol.inline { + margin-left: 0; + list-style: none; +} + +ul.inline > li, +ol.inline > li { + display: inline-block; + *display: inline; + padding-right: 5px; + padding-left: 5px; + *zoom: 1; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 20px; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 10px; +} + +.dl-horizontal { + *zoom: 1; +} + +.dl-horizontal:before, +.dl-horizontal:after { + display: table; + line-height: 0; + content: ""; +} + +.dl-horizontal:after { + clear: both; +} + +.dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dl-horizontal dd { + margin-left: 180px; +} + +hr { + margin: 20px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + margin-bottom: 0; + font-size: 17.5px; + font-weight: 300; + line-height: 1.25; +} + +blockquote small { + display: block; + line-height: 20px; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + float: right; + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 20px; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +code { + padding: 2px 4px; + color: #d14; + white-space: nowrap; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 20px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + color: inherit; + white-space: pre; + white-space: pre-wrap; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +form { + margin: 0 0 20px; +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: 40px; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +legend small { + font-size: 15px; + color: #999999; +} + +label, +input, +button, +select, +textarea { + font-size: 14px; + font-weight: normal; + line-height: 20px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +label { + display: block; + margin-bottom: 5px; +} + +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + display: inline-block; + height: 20px; + padding: 4px 6px; + margin-bottom: 10px; + font-size: 14px; + line-height: 20px; + color: #555555; + vertical-align: middle; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +input, +textarea, +.uneditable-input { + width: 206px; +} + +textarea { + height: auto; +} + +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + background-color: #ffffff; + border: 1px solid #cccccc; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +.uneditable-input:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + *margin-top: 0; + line-height: normal; +} + +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { + width: auto; +} + +select, +input[type="file"] { + height: 30px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 30px; +} + +select { + width: 220px; + background-color: #ffffff; + border: 1px solid #cccccc; +} + +select[multiple], +select[size] { + height: auto; +} + +select:focus, +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.uneditable-input, +.uneditable-textarea { + color: #999999; + cursor: not-allowed; + background-color: #fcfcfc; + border-color: #cccccc; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); +} + +.uneditable-input { + overflow: hidden; + white-space: nowrap; +} + +.uneditable-textarea { + width: auto; + height: auto; +} + +input:-moz-placeholder, +textarea:-moz-placeholder { + color: #999999; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #999999; +} + +input::-webkit-input-placeholder, +textarea::-webkit-input-placeholder { + color: #999999; +} + +.radio, +.checkbox { + min-height: 20px; + padding-left: 20px; +} + +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -20px; +} + +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} + +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} + +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} + +.input-mini { + width: 60px; +} + +.input-small { + width: 90px; +} + +.input-medium { + width: 150px; +} + +.input-large { + width: 210px; +} + +.input-xlarge { + width: 270px; +} + +.input-xxlarge { + width: 530px; +} + +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"] { + float: none; + margin-left: 0; +} + +.input-append input[class*="span"], +.input-append .uneditable-input[class*="span"], +.input-prepend input[class*="span"], +.input-prepend .uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"], +.row-fluid .input-prepend [class*="span"], +.row-fluid .input-append [class*="span"] { + display: inline-block; +} + +input, +textarea, +.uneditable-input { + margin-left: 0; +} + +.controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; +} + +input.span12, +textarea.span12, +.uneditable-input.span12 { + width: 926px; +} + +input.span11, +textarea.span11, +.uneditable-input.span11 { + width: 846px; +} + +input.span10, +textarea.span10, +.uneditable-input.span10 { + width: 766px; +} + +input.span9, +textarea.span9, +.uneditable-input.span9 { + width: 686px; +} + +input.span8, +textarea.span8, +.uneditable-input.span8 { + width: 606px; +} + +input.span7, +textarea.span7, +.uneditable-input.span7 { + width: 526px; +} + +input.span6, +textarea.span6, +.uneditable-input.span6 { + width: 446px; +} + +input.span5, +textarea.span5, +.uneditable-input.span5 { + width: 366px; +} + +input.span4, +textarea.span4, +.uneditable-input.span4 { + width: 286px; +} + +input.span3, +textarea.span3, +.uneditable-input.span3 { + width: 206px; +} + +input.span2, +textarea.span2, +.uneditable-input.span2 { + width: 126px; +} + +input.span1, +textarea.span1, +.uneditable-input.span1 { + width: 46px; +} + +.controls-row { + *zoom: 1; +} + +.controls-row:before, +.controls-row:after { + display: table; + line-height: 0; + content: ""; +} + +.controls-row:after { + clear: both; +} + +.controls-row [class*="span"], +.row-fluid .controls-row [class*="span"] { + float: left; +} + +.controls-row .checkbox[class*="span"], +.controls-row .radio[class*="span"] { + padding-top: 5px; +} + +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #eeeeee; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { + background-color: transparent; +} + +.control-group.warning .control-label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} + +.control-group.warning .checkbox, +.control-group.warning .radio, +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; +} + +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.control-group.error .control-label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} + +.control-group.error .checkbox, +.control-group.error .radio, +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; +} + +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.control-group.success .control-label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} + +.control-group.success .checkbox, +.control-group.success .radio, +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; +} + +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.control-group.info .control-label, +.control-group.info .help-block, +.control-group.info .help-inline { + color: #3a87ad; +} + +.control-group.info .checkbox, +.control-group.info .radio, +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + color: #3a87ad; +} + +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + border-color: #3a87ad; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.info input:focus, +.control-group.info select:focus, +.control-group.info textarea:focus { + border-color: #2d6987; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; +} + +.control-group.info .input-prepend .add-on, +.control-group.info .input-append .add-on { + color: #3a87ad; + background-color: #d9edf7; + border-color: #3a87ad; +} + +input:focus:invalid, +textarea:focus:invalid, +select:focus:invalid { + color: #b94a48; + border-color: #ee5f5b; +} + +input:focus:invalid:focus, +textarea:focus:invalid:focus, +select:focus:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.form-actions { + padding: 19px 20px 20px; + margin-top: 20px; + margin-bottom: 20px; + background-color: #f5f5f5; + border-top: 1px solid #e5e5e5; + *zoom: 1; +} + +.form-actions:before, +.form-actions:after { + display: table; + line-height: 0; + content: ""; +} + +.form-actions:after { + clear: both; +} + +.help-block, +.help-inline { + color: #595959; +} + +.help-block { + display: block; + margin-bottom: 10px; +} + +.help-inline { + display: inline-block; + *display: inline; + padding-left: 5px; + vertical-align: middle; + *zoom: 1; +} + +.input-append, +.input-prepend { + display: inline-block; + margin-bottom: 10px; + font-size: 0; + white-space: nowrap; + vertical-align: middle; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input, +.input-append .dropdown-menu, +.input-prepend .dropdown-menu, +.input-append .popover, +.input-prepend .popover { + font-size: 14px; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + *margin-left: 0; + vertical-align: top; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append input:focus, +.input-prepend input:focus, +.input-append select:focus, +.input-prepend select:focus, +.input-append .uneditable-input:focus, +.input-prepend .uneditable-input:focus { + z-index: 2; +} + +.input-append .add-on, +.input-prepend .add-on { + display: inline-block; + width: auto; + height: 20px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: normal; + line-height: 20px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #eeeeee; + border: 1px solid #ccc; +} + +.input-append .add-on, +.input-prepend .add-on, +.input-append .btn, +.input-prepend .btn, +.input-append .btn-group > .dropdown-toggle, +.input-prepend .btn-group > .dropdown-toggle { + vertical-align: top; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-append .active, +.input-prepend .active { + background-color: #a9dba9; + border-color: #46a546; +} + +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input, +.input-append select, +.input-append .uneditable-input { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input + .btn-group .btn:last-child, +.input-append select + .btn-group .btn:last-child, +.input-append .uneditable-input + .btn-group .btn:last-child { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append .add-on, +.input-append .btn, +.input-append .btn-group { + margin-left: -1px; +} + +.input-append .add-on:last-child, +.input-append .btn:last-child, +.input-append .btn-group:last-child > .dropdown-toggle { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend.input-append input + .btn-group .btn, +.input-prepend.input-append select + .btn-group .btn, +.input-prepend.input-append .uneditable-input + .btn-group .btn { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .btn-group:first-child { + margin-left: 0; +} + +input.search-query { + padding-right: 14px; + padding-right: 4px \9; + padding-left: 14px; + padding-left: 4px \9; + /* IE7-8 doesn't have border-radius, so don't indent the padding */ + + margin-bottom: 0; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +/* Allow for input prepend/append in search forms */ + +.form-search .input-append .search-query, +.form-search .input-prepend .search-query { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.form-search .input-append .search-query { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search .input-append .btn { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .search-query { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .btn { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + *display: inline; + margin-bottom: 0; + vertical-align: middle; + *zoom: 1; +} + +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} + +.form-search label, +.form-inline label, +.form-search .btn-group, +.form-inline .btn-group { + display: inline-block; +} + +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} + +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} + +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-right: 3px; + margin-left: 0; +} + +.control-group { + margin-bottom: 10px; +} + +legend + .control-group { + margin-top: 20px; + -webkit-margin-top-collapse: separate; +} + +.form-horizontal .control-group { + margin-bottom: 20px; + *zoom: 1; +} + +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + line-height: 0; + content: ""; +} + +.form-horizontal .control-group:after { + clear: both; +} + +.form-horizontal .control-label { + float: left; + width: 160px; + padding-top: 5px; + text-align: right; +} + +.form-horizontal .controls { + *display: inline-block; + *padding-left: 20px; + margin-left: 180px; + *margin-left: 0; +} + +.form-horizontal .controls:first-child { + *padding-left: 180px; +} + +.form-horizontal .help-block { + margin-bottom: 0; +} + +.form-horizontal input + .help-block, +.form-horizontal select + .help-block, +.form-horizontal textarea + .help-block, +.form-horizontal .uneditable-input + .help-block, +.form-horizontal .input-prepend + .help-block, +.form-horizontal .input-append + .help-block { + margin-top: 10px; +} + +.form-horizontal .form-actions { + padding-left: 180px; +} + +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table th, +.table td { + padding: 8px; + line-height: 20px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table th { + font-weight: bold; +} + +.table thead th { + vertical-align: bottom; +} + +.table caption + thead tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table .table { + background-color: #ffffff; +} + +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} + +.table-bordered { + border: 1px solid #dddddd; + border-collapse: separate; + *border-collapse: collapse; + border-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} + +.table-bordered caption + thead tr:first-child th, +.table-bordered caption + tbody tr:first-child th, +.table-bordered caption + tbody tr:first-child td, +.table-bordered colgroup + thead tr:first-child th, +.table-bordered colgroup + tbody tr:first-child th, +.table-bordered colgroup + tbody tr:first-child td, +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} + +.table-bordered thead:first-child tr:first-child > th:first-child, +.table-bordered tbody:first-child tr:first-child > td:first-child, +.table-bordered tbody:first-child tr:first-child > th:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered thead:first-child tr:first-child > th:last-child, +.table-bordered tbody:first-child tr:first-child > td:last-child, +.table-bordered tbody:first-child tr:first-child > th:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:first-child, +.table-bordered tbody:last-child tr:last-child > td:first-child, +.table-bordered tbody:last-child tr:last-child > th:first-child, +.table-bordered tfoot:last-child tr:last-child > td:first-child, +.table-bordered tfoot:last-child tr:last-child > th:first-child { + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:last-child, +.table-bordered tbody:last-child tr:last-child > td:last-child, +.table-bordered tbody:last-child tr:last-child > th:last-child, +.table-bordered tfoot:last-child tr:last-child > td:last-child, +.table-bordered tfoot:last-child tr:last-child > th:last-child { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:first-child { + -webkit-border-bottom-left-radius: 0; + border-bottom-left-radius: 0; + -moz-border-radius-bottomleft: 0; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 0; + border-bottom-right-radius: 0; + -moz-border-radius-bottomright: 0; +} + +.table-bordered caption + thead tr:first-child th:first-child, +.table-bordered caption + tbody tr:first-child td:first-child, +.table-bordered colgroup + thead tr:first-child th:first-child, +.table-bordered colgroup + tbody tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered caption + thead tr:first-child th:last-child, +.table-bordered caption + tbody tr:first-child td:last-child, +.table-bordered colgroup + thead tr:first-child th:last-child, +.table-bordered colgroup + tbody tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-striped tbody > tr:nth-child(odd) > td, +.table-striped tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover > td, +.table-hover tbody tr:hover > th { + background-color: #f5f5f5; +} + +table td[class*="span"], +table th[class*="span"], +.row-fluid table td[class*="span"], +.row-fluid table th[class*="span"] { + display: table-cell; + float: none; + margin-left: 0; +} + +.table td.span1, +.table th.span1 { + float: none; + width: 44px; + margin-left: 0; +} + +.table td.span2, +.table th.span2 { + float: none; + width: 124px; + margin-left: 0; +} + +.table td.span3, +.table th.span3 { + float: none; + width: 204px; + margin-left: 0; +} + +.table td.span4, +.table th.span4 { + float: none; + width: 284px; + margin-left: 0; +} + +.table td.span5, +.table th.span5 { + float: none; + width: 364px; + margin-left: 0; +} + +.table td.span6, +.table th.span6 { + float: none; + width: 444px; + margin-left: 0; +} + +.table td.span7, +.table th.span7 { + float: none; + width: 524px; + margin-left: 0; +} + +.table td.span8, +.table th.span8 { + float: none; + width: 604px; + margin-left: 0; +} + +.table td.span9, +.table th.span9 { + float: none; + width: 684px; + margin-left: 0; +} + +.table td.span10, +.table th.span10 { + float: none; + width: 764px; + margin-left: 0; +} + +.table td.span11, +.table th.span11 { + float: none; + width: 844px; + margin-left: 0; +} + +.table td.span12, +.table th.span12 { + float: none; + width: 924px; + margin-left: 0; +} + +.table tbody tr.success > td { + background-color: #dff0d8; +} + +.table tbody tr.error > td { + background-color: #f2dede; +} + +.table tbody tr.warning > td { + background-color: #fcf8e3; +} + +.table tbody tr.info > td { + background-color: #d9edf7; +} + +.table-hover tbody tr.success:hover > td { + background-color: #d0e9c6; +} + +.table-hover tbody tr.error:hover > td { + background-color: #ebcccc; +} + +.table-hover tbody tr.warning:hover > td { + background-color: #faf2cc; +} + +.table-hover tbody tr.info:hover > td { + background-color: #c4e3f3; +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + margin-top: 1px; + *margin-right: .3em; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; +} + +/* White icons with optional class, or on hover/focus/active states of certain elements */ + +.icon-white, +.nav-pills > .active > a > [class^="icon-"], +.nav-pills > .active > a > [class*=" icon-"], +.nav-list > .active > a > [class^="icon-"], +.nav-list > .active > a > [class*=" icon-"], +.navbar-inverse .nav > .active > a > [class^="icon-"], +.navbar-inverse .nav > .active > a > [class*=" icon-"], +.dropdown-menu > li > a:hover > [class^="icon-"], +.dropdown-menu > li > a:focus > [class^="icon-"], +.dropdown-menu > li > a:hover > [class*=" icon-"], +.dropdown-menu > li > a:focus > [class*=" icon-"], +.dropdown-menu > .active > a > [class^="icon-"], +.dropdown-menu > .active > a > [class*=" icon-"], +.dropdown-submenu:hover > a > [class^="icon-"], +.dropdown-submenu:focus > a > [class^="icon-"], +.dropdown-submenu:hover > a > [class*=" icon-"], +.dropdown-submenu:focus > a > [class*=" icon-"] { + background-image: url("../img/glyphicons-halflings-white.png"); +} + +.icon-glass { + background-position: 0 0; +} + +.icon-music { + background-position: -24px 0; +} + +.icon-search { + background-position: -48px 0; +} + +.icon-envelope { + background-position: -72px 0; +} + +.icon-heart { + background-position: -96px 0; +} + +.icon-star { + background-position: -120px 0; +} + +.icon-star-empty { + background-position: -144px 0; +} + +.icon-user { + background-position: -168px 0; +} + +.icon-film { + background-position: -192px 0; +} + +.icon-th-large { + background-position: -216px 0; +} + +.icon-th { + background-position: -240px 0; +} + +.icon-th-list { + background-position: -264px 0; +} + +.icon-ok { + background-position: -288px 0; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-zoom-in { + background-position: -336px 0; +} + +.icon-zoom-out { + background-position: -360px 0; +} + +.icon-off { + background-position: -384px 0; +} + +.icon-signal { + background-position: -408px 0; +} + +.icon-cog { + background-position: -432px 0; +} + +.icon-trash { + background-position: -456px 0; +} + +.icon-home { + background-position: 0 -24px; +} + +.icon-file { + background-position: -24px -24px; +} + +.icon-time { + background-position: -48px -24px; +} + +.icon-road { + background-position: -72px -24px; +} + +.icon-download-alt { + background-position: -96px -24px; +} + +.icon-download { + background-position: -120px -24px; +} + +.icon-upload { + background-position: -144px -24px; +} + +.icon-inbox { + background-position: -168px -24px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-repeat { + background-position: -216px -24px; +} + +.icon-refresh { + background-position: -240px -24px; +} + +.icon-list-alt { + background-position: -264px -24px; +} + +.icon-lock { + background-position: -287px -24px; +} + +.icon-flag { + background-position: -312px -24px; +} + +.icon-headphones { + background-position: -336px -24px; +} + +.icon-volume-off { + background-position: -360px -24px; +} + +.icon-volume-down { + background-position: -384px -24px; +} + +.icon-volume-up { + background-position: -408px -24px; +} + +.icon-qrcode { + background-position: -432px -24px; +} + +.icon-barcode { + background-position: -456px -24px; +} + +.icon-tag { + background-position: 0 -48px; +} + +.icon-tags { + background-position: -25px -48px; +} + +.icon-book { + background-position: -48px -48px; +} + +.icon-bookmark { + background-position: -72px -48px; +} + +.icon-print { + background-position: -96px -48px; +} + +.icon-camera { + background-position: -120px -48px; +} + +.icon-font { + background-position: -144px -48px; +} + +.icon-bold { + background-position: -167px -48px; +} + +.icon-italic { + background-position: -192px -48px; +} + +.icon-text-height { + background-position: -216px -48px; +} + +.icon-text-width { + background-position: -240px -48px; +} + +.icon-align-left { + background-position: -264px -48px; +} + +.icon-align-center { + background-position: -288px -48px; +} + +.icon-align-right { + background-position: -312px -48px; +} + +.icon-align-justify { + background-position: -336px -48px; +} + +.icon-list { + background-position: -360px -48px; +} + +.icon-indent-left { + background-position: -384px -48px; +} + +.icon-indent-right { + background-position: -408px -48px; +} + +.icon-facetime-video { + background-position: -432px -48px; +} + +.icon-picture { + background-position: -456px -48px; +} + +.icon-pencil { + background-position: 0 -72px; +} + +.icon-map-marker { + background-position: -24px -72px; +} + +.icon-adjust { + background-position: -48px -72px; +} + +.icon-tint { + background-position: -72px -72px; +} + +.icon-edit { + background-position: -96px -72px; +} + +.icon-share { + background-position: -120px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-move { + background-position: -168px -72px; +} + +.icon-step-backward { + background-position: -192px -72px; +} + +.icon-fast-backward { + background-position: -216px -72px; +} + +.icon-backward { + background-position: -240px -72px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-stop { + background-position: -312px -72px; +} + +.icon-forward { + background-position: -336px -72px; +} + +.icon-fast-forward { + background-position: -360px -72px; +} + +.icon-step-forward { + background-position: -384px -72px; +} + +.icon-eject { + background-position: -408px -72px; +} + +.icon-chevron-left { + background-position: -432px -72px; +} + +.icon-chevron-right { + background-position: -456px -72px; +} + +.icon-plus-sign { + background-position: 0 -96px; +} + +.icon-minus-sign { + background-position: -24px -96px; +} + +.icon-remove-sign { + background-position: -48px -96px; +} + +.icon-ok-sign { + background-position: -72px -96px; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-screenshot { + background-position: -144px -96px; +} + +.icon-remove-circle { + background-position: -168px -96px; +} + +.icon-ok-circle { + background-position: -192px -96px; +} + +.icon-ban-circle { + background-position: -216px -96px; +} + +.icon-arrow-left { + background-position: -240px -96px; +} + +.icon-arrow-right { + background-position: -264px -96px; +} + +.icon-arrow-up { + background-position: -289px -96px; +} + +.icon-arrow-down { + background-position: -312px -96px; +} + +.icon-share-alt { + background-position: -336px -96px; +} + +.icon-resize-full { + background-position: -360px -96px; +} + +.icon-resize-small { + background-position: -384px -96px; +} + +.icon-plus { + background-position: -408px -96px; +} + +.icon-minus { + background-position: -433px -96px; +} + +.icon-asterisk { + background-position: -456px -96px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-gift { + background-position: -24px -120px; +} + +.icon-leaf { + background-position: -48px -120px; +} + +.icon-fire { + background-position: -72px -120px; +} + +.icon-eye-open { + background-position: -96px -120px; +} + +.icon-eye-close { + background-position: -120px -120px; +} + +.icon-warning-sign { + background-position: -144px -120px; +} + +.icon-plane { + background-position: -168px -120px; +} + +.icon-calendar { + background-position: -192px -120px; +} + +.icon-random { + width: 16px; + background-position: -216px -120px; +} + +.icon-comment { + background-position: -240px -120px; +} + +.icon-magnet { + background-position: -264px -120px; +} + +.icon-chevron-up { + background-position: -288px -120px; +} + +.icon-chevron-down { + background-position: -313px -119px; +} + +.icon-retweet { + background-position: -336px -120px; +} + +.icon-shopping-cart { + background-position: -360px -120px; +} + +.icon-folder-close { + width: 16px; + background-position: -384px -120px; +} + +.icon-folder-open { + width: 16px; + background-position: -408px -120px; +} + +.icon-resize-vertical { + background-position: -432px -119px; +} + +.icon-resize-horizontal { + background-position: -456px -118px; +} + +.icon-hdd { + background-position: 0 -144px; +} + +.icon-bullhorn { + background-position: -24px -144px; +} + +.icon-bell { + background-position: -48px -144px; +} + +.icon-certificate { + background-position: -72px -144px; +} + +.icon-thumbs-up { + background-position: -96px -144px; +} + +.icon-thumbs-down { + background-position: -120px -144px; +} + +.icon-hand-right { + background-position: -144px -144px; +} + +.icon-hand-left { + background-position: -168px -144px; +} + +.icon-hand-up { + background-position: -192px -144px; +} + +.icon-hand-down { + background-position: -216px -144px; +} + +.icon-circle-arrow-right { + background-position: -240px -144px; +} + +.icon-circle-arrow-left { + background-position: -264px -144px; +} + +.icon-circle-arrow-up { + background-position: -288px -144px; +} + +.icon-circle-arrow-down { + background-position: -312px -144px; +} + +.icon-globe { + background-position: -336px -144px; +} + +.icon-wrench { + background-position: -360px -144px; +} + +.icon-tasks { + background-position: -384px -144px; +} + +.icon-filter { + background-position: -408px -144px; +} + +.icon-briefcase { + background-position: -432px -144px; +} + +.icon-fullscreen { + background-position: -456px -144px; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle { + *margin-bottom: -3px; +} + +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-submenu:hover > a, +.dropdown-submenu:focus > a { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + outline: 0; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #999999; +} + +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: default; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.open { + *z-index: 1000; +} + +.open > .dropdown-menu { + display: block; +} + +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +.dropdown-submenu { + position: relative; +} + +.dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + -webkit-border-radius: 0 6px 6px 6px; + -moz-border-radius: 0 6px 6px 6px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover > .dropdown-menu { + display: block; +} + +.dropup .dropdown-submenu > .dropdown-menu { + top: auto; + bottom: 0; + margin-top: 0; + margin-bottom: -2px; + -webkit-border-radius: 5px 5px 5px 0; + -moz-border-radius: 5px 5px 5px 0; + border-radius: 5px 5px 5px 0; +} + +.dropdown-submenu > a:after { + display: block; + float: right; + width: 0; + height: 0; + margin-top: 5px; + margin-right: -10px; + border-color: transparent; + border-left-color: #cccccc; + border-style: solid; + border-width: 5px 0 5px 5px; + content: " "; +} + +.dropdown-submenu:hover > a:after { + border-left-color: #ffffff; +} + +.dropdown-submenu.pull-left { + float: none; +} + +.dropdown-submenu.pull-left > .dropdown-menu { + left: -100%; + margin-left: 10px; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.dropdown .dropdown-menu .nav-header { + padding-right: 20px; + padding-left: 20px; +} + +.typeahead { + z-index: 1051; + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +.collapse.in { + height: auto; +} + +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 20px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover, +.close:focus { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 12px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 14px; + line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #cccccc; + *border: 0; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-bottom-color: #b3b3b3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:focus, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + color: #333333; + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover, +.btn:focus { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 11px 19px; + font-size: 17.5px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.btn-large [class^="icon-"], +.btn-large [class*=" icon-"] { + margin-top: 4px; +} + +.btn-small { + padding: 2px 10px; + font-size: 11.9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-small [class^="icon-"], +.btn-small [class*=" icon-"] { + margin-top: 0; +} + +.btn-mini [class^="icon-"], +.btn-mini [class*=" icon-"] { + margin-top: -1px; +} + +.btn-mini { + padding: 0 6px; + font-size: 10.5px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn-primary { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #006dcc; + *background-color: #0044cc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(to bottom, #0088cc, #0044cc); + background-repeat: repeat-x; + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + color: #ffffff; + background-color: #0044cc; + *background-color: #003bb3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #003399 \9; +} + +.btn-warning { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #faa732; + *background-color: #f89406; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:focus, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + color: #ffffff; + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #da4f49; + *background-color: #bd362f; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:focus, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + color: #ffffff; + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #5bb75b; + *background-color: #51a351; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(to bottom, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:focus, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + color: #ffffff; + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:focus, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + color: #ffffff; + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #363636; + *background-color: #222222; + background-image: -moz-linear-gradient(top, #444444, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); + background-image: -webkit-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(top, #444444, #222222); + background-image: linear-gradient(to bottom, #444444, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-inverse:hover, +.btn-inverse:focus, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + color: #ffffff; + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +button.btn, +input[type="submit"].btn { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled] { + background-color: transparent; + background-image: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-link { + color: #0088cc; + cursor: pointer; + border-color: transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-link:hover, +.btn-link:focus { + color: #005580; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover, +.btn-link[disabled]:focus { + color: #333333; + text-decoration: none; +} + +.btn-group { + position: relative; + display: inline-block; + *display: inline; + *margin-left: .3em; + font-size: 0; + white-space: nowrap; + vertical-align: middle; + *zoom: 1; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 10px; + margin-bottom: 10px; + font-size: 0; +} + +.btn-toolbar > .btn + .btn, +.btn-toolbar > .btn-group + .btn, +.btn-toolbar > .btn + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn { + position: relative; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn + .btn { + margin-left: -1px; +} + +.btn-group > .btn, +.btn-group > .dropdown-menu, +.btn-group > .popover { + font-size: 14px; +} + +.btn-group > .btn-mini { + font-size: 10.5px; +} + +.btn-group > .btn-small { + font-size: 11.9px; +} + +.btn-group > .btn-large { + font-size: 17.5px; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + *padding-top: 5px; + padding-right: 8px; + *padding-bottom: 5px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini + .dropdown-toggle { + *padding-top: 2px; + padding-right: 5px; + *padding-bottom: 2px; + padding-left: 5px; +} + +.btn-group > .btn-small + .dropdown-toggle { + *padding-top: 5px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large + .dropdown-toggle { + *padding-top: 7px; + padding-right: 12px; + *padding-bottom: 7px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0044cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 8px; + margin-left: 0; +} + +.btn-large .caret { + margin-top: 6px; +} + +.btn-large .caret { + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.btn-mini .caret, +.btn-small .caret { + margin-top: 8px; +} + +.dropup .btn-large .caret { + border-bottom-width: 5px; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.btn-group-vertical { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group-vertical > .btn { + display: block; + float: none; + max-width: 100%; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group-vertical > .btn + .btn { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:first-child { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.btn-group-vertical > .btn:last-child { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.btn-group-vertical > .btn-large:first-child { + -webkit-border-radius: 6px 6px 0 0; + -moz-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} + +.btn-group-vertical > .btn-large:last-child { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 20px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.alert, +.alert h4 { + color: #c09853; +} + +.alert h4 { + margin: 0; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 20px; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-success h4 { + color: #468847; +} + +.alert-danger, +.alert-error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-danger h4, +.alert-error h4 { + color: #b94a48; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-info h4 { + color: #3a87ad; +} + +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} + +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} + +.alert-block p + p { + margin-top: 5px; +} + +.nav { + margin-bottom: 20px; + margin-left: 0; + list-style: none; +} + +.nav > li > a { + display: block; +} + +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > li > a > img { + max-width: none; +} + +.nav > .pull-right { + float: right; +} + +.nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.nav li + .nav-header { + margin-top: 9px; +} + +.nav-list { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 0; +} + +.nav-list > li > a, +.nav-list .nav-header { + margin-right: -15px; + margin-left: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.nav-list > li > a { + padding: 3px 15px; +} + +.nav-list > .active > a, +.nav-list > .active > a:hover, +.nav-list > .active > a:focus { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} + +.nav-list [class^="icon-"], +.nav-list [class*=" icon-"] { + margin-right: 2px; +} + +.nav-list .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.nav-tabs, +.nav-pills { + *zoom: 1; +} + +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + line-height: 0; + content: ""; +} + +.nav-tabs:after, +.nav-pills:after { + clear: both; +} + +.nav-tabs > li, +.nav-pills > li { + float: left; +} + +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + margin-bottom: -1px; +} + +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 20px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover, +.nav-tabs > li > a:focus { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover, +.nav-tabs > .active > a:focus { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} + +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.nav-pills > .active > a, +.nav-pills > .active > a:hover, +.nav-pills > .active > a:focus { + color: #ffffff; + background-color: #0088cc; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li > a { + margin-right: 0; +} + +.nav-tabs.nav-stacked { + border-bottom: 0; +} + +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-topleft: 4px; +} + +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.nav-tabs.nav-stacked > li > a:hover, +.nav-tabs.nav-stacked > li > a:focus { + z-index: 2; + border-color: #ddd; +} + +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} + +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} + +.nav-tabs .dropdown-menu { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.nav-pills .dropdown-menu { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.nav .dropdown-toggle .caret { + margin-top: 6px; + border-top-color: #0088cc; + border-bottom-color: #0088cc; +} + +.nav .dropdown-toggle:hover .caret, +.nav .dropdown-toggle:focus .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} + +/* move down carets for tabs */ + +.nav-tabs .dropdown-toggle .caret { + margin-top: 8px; +} + +.nav .active .dropdown-toggle .caret { + border-top-color: #fff; + border-bottom-color: #fff; +} + +.nav-tabs .active .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.nav > .dropdown.active > a:hover, +.nav > .dropdown.active > a:focus { + cursor: pointer; +} + +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > li.dropdown.open.active > a:hover, +.nav > li.dropdown.open.active > a:focus { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} + +.nav li.dropdown.open .caret, +.nav li.dropdown.open.active .caret, +.nav li.dropdown.open a:hover .caret, +.nav li.dropdown.open a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} + +.tabs-stacked .open > a:hover, +.tabs-stacked .open > a:focus { + border-color: #999999; +} + +.tabbable { + *zoom: 1; +} + +.tabbable:before, +.tabbable:after { + display: table; + line-height: 0; + content: ""; +} + +.tabbable:after { + clear: both; +} + +.tab-content { + overflow: auto; +} + +.tabs-below > .nav-tabs, +.tabs-right > .nav-tabs, +.tabs-left > .nav-tabs { + border-bottom: 0; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.tabs-below > .nav-tabs { + border-top: 1px solid #ddd; +} + +.tabs-below > .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} + +.tabs-below > .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.tabs-below > .nav-tabs > li > a:hover, +.tabs-below > .nav-tabs > li > a:focus { + border-top-color: #ddd; + border-bottom-color: transparent; +} + +.tabs-below > .nav-tabs > .active > a, +.tabs-below > .nav-tabs > .active > a:hover, +.tabs-below > .nav-tabs > .active > a:focus { + border-color: transparent #ddd #ddd #ddd; +} + +.tabs-left > .nav-tabs > li, +.tabs-right > .nav-tabs > li { + float: none; +} + +.tabs-left > .nav-tabs > li > a, +.tabs-right > .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} + +.tabs-left > .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} + +.tabs-left > .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.tabs-left > .nav-tabs > li > a:hover, +.tabs-left > .nav-tabs > li > a:focus { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} + +.tabs-left > .nav-tabs .active > a, +.tabs-left > .nav-tabs .active > a:hover, +.tabs-left > .nav-tabs .active > a:focus { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} + +.tabs-right > .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} + +.tabs-right > .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.tabs-right > .nav-tabs > li > a:hover, +.tabs-right > .nav-tabs > li > a:focus { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} + +.tabs-right > .nav-tabs .active > a, +.tabs-right > .nav-tabs .active > a:hover, +.tabs-right > .nav-tabs .active > a:focus { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} + +.nav > .disabled > a { + color: #999999; +} + +.nav > .disabled > a:hover, +.nav > .disabled > a:focus { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.navbar { + *position: relative; + *z-index: 2; + margin-bottom: 20px; + overflow: visible; +} + +.navbar-inner { + min-height: 40px; + padding-right: 20px; + padding-left: 20px; + background-color: #fafafa; + background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); + background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); + background-repeat: repeat-x; + border: 1px solid #d4d4d4; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); + *zoom: 1; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); +} + +.navbar-inner:before, +.navbar-inner:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-inner:after { + clear: both; +} + +.navbar .container { + width: auto; +} + +.nav-collapse.collapse { + height: auto; + overflow: visible; +} + +.navbar .brand { + display: block; + float: left; + padding: 10px 20px 10px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + color: #777777; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .brand:hover, +.navbar .brand:focus { + text-decoration: none; +} + +.navbar-text { + margin-bottom: 0; + line-height: 40px; + color: #777777; +} + +.navbar-link { + color: #777777; +} + +.navbar-link:hover, +.navbar-link:focus { + color: #333333; +} + +.navbar .divider-vertical { + height: 40px; + margin: 0 9px; + border-right: 1px solid #ffffff; + border-left: 1px solid #f2f2f2; +} + +.navbar .btn, +.navbar .btn-group { + margin-top: 5px; +} + +.navbar .btn-group .btn, +.navbar .input-prepend .btn, +.navbar .input-append .btn, +.navbar .input-prepend .btn-group, +.navbar .input-append .btn-group { + margin-top: 0; +} + +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} + +.navbar-form:before, +.navbar-form:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-form:after { + clear: both; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .btn { + display: inline-block; + margin-bottom: 0; +} + +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} + +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 5px; + white-space: nowrap; +} + +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} + +.navbar-search { + position: relative; + float: left; + margin-top: 5px; + margin-bottom: 0; +} + +.navbar-search .search-query { + padding: 4px 14px; + margin-bottom: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.navbar-static-top { + position: static; + margin-bottom: 0; +} + +.navbar-static-top .navbar-inner { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + border-width: 0 0 1px; +} + +.navbar-fixed-bottom .navbar-inner { + border-width: 1px 0 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-right: 0; + padding-left: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-fixed-bottom .navbar-inner { + -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} + +.navbar .nav.pull-right { + float: right; + margin-right: 0; +} + +.navbar .nav > li { + float: left; +} + +.navbar .nav > li > a { + float: none; + padding: 10px 15px 10px; + color: #777777; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .nav .dropdown-toggle .caret { + margin-top: 8px; +} + +.navbar .nav > li > a:focus, +.navbar .nav > li > a:hover { + color: #333333; + text-decoration: none; + background-color: transparent; +} + +.navbar .nav > .active > a, +.navbar .nav > .active > a:hover, +.navbar .nav > .active > a:focus { + color: #555555; + text-decoration: none; + background-color: #e5e5e5; + -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); +} + +.navbar .btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-right: 5px; + margin-left: 5px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #ededed; + *background-color: #e5e5e5; + background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); + background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); + background-repeat: repeat-x; + border-color: #e5e5e5 #e5e5e5 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} + +.navbar .btn-navbar:hover, +.navbar .btn-navbar:focus, +.navbar .btn-navbar:active, +.navbar .btn-navbar.active, +.navbar .btn-navbar.disabled, +.navbar .btn-navbar[disabled] { + color: #ffffff; + background-color: #e5e5e5; + *background-color: #d9d9d9; +} + +.navbar .btn-navbar:active, +.navbar .btn-navbar.active { + background-color: #cccccc \9; +} + +.navbar .btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} + +.navbar .nav > li > .dropdown-menu:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.navbar .nav > li > .dropdown-menu:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:before { + top: auto; + bottom: -7px; + border-top: 7px solid #ccc; + border-bottom: 0; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:after { + top: auto; + bottom: -6px; + border-top: 6px solid #ffffff; + border-bottom: 0; +} + +.navbar .nav li.dropdown > a:hover .caret, +.navbar .nav li.dropdown > a:focus .caret { + border-top-color: #333333; + border-bottom-color: #333333; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle, +.navbar .nav li.dropdown.active > .dropdown-toggle, +.navbar .nav li.dropdown.open.active > .dropdown-toggle { + color: #555555; + background-color: #e5e5e5; +} + +.navbar .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .pull-right > li > .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:before, +.navbar .nav > li > .dropdown-menu.pull-right:before { + right: 12px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:after, +.navbar .nav > li > .dropdown-menu.pull-right:after { + right: 13px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { + right: 100%; + left: auto; + margin-right: -1px; + margin-left: 0; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.navbar-inverse .navbar-inner { + background-color: #1b1b1b; + background-image: -moz-linear-gradient(top, #222222, #111111); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); + background-image: -webkit-linear-gradient(top, #222222, #111111); + background-image: -o-linear-gradient(top, #222222, #111111); + background-image: linear-gradient(to bottom, #222222, #111111); + background-repeat: repeat-x; + border-color: #252525; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); +} + +.navbar-inverse .brand, +.navbar-inverse .nav > li > a { + color: #999999; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar-inverse .brand:hover, +.navbar-inverse .nav > li > a:hover, +.navbar-inverse .brand:focus, +.navbar-inverse .nav > li > a:focus { + color: #ffffff; +} + +.navbar-inverse .brand { + color: #999999; +} + +.navbar-inverse .navbar-text { + color: #999999; +} + +.navbar-inverse .nav > li > a:focus, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .nav .active > a, +.navbar-inverse .nav .active > a:hover, +.navbar-inverse .nav .active > a:focus { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover, +.navbar-inverse .navbar-link:focus { + color: #ffffff; +} + +.navbar-inverse .divider-vertical { + border-right-color: #222222; + border-left-color: #111111; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .nav li.dropdown > a:hover .caret, +.navbar-inverse .nav li.dropdown > a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-search .search-query { + color: #ffffff; + background-color: #515151; + border-color: #111111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; +} + +.navbar-inverse .navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:focus, +.navbar-inverse .navbar-search .search-query.focused { + padding: 5px 15px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + outline: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); +} + +.navbar-inverse .btn-navbar { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e0e0e; + *background-color: #040404; + background-image: -moz-linear-gradient(top, #151515, #040404); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); + background-image: -webkit-linear-gradient(top, #151515, #040404); + background-image: -o-linear-gradient(top, #151515, #040404); + background-image: linear-gradient(to bottom, #151515, #040404); + background-repeat: repeat-x; + border-color: #040404 #040404 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.navbar-inverse .btn-navbar:hover, +.navbar-inverse .btn-navbar:focus, +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active, +.navbar-inverse .btn-navbar.disabled, +.navbar-inverse .btn-navbar[disabled] { + color: #ffffff; + background-color: #040404; + *background-color: #000000; +} + +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active { + background-color: #000000 \9; +} + +.breadcrumb { + padding: 8px 15px; + margin: 0 0 20px; + list-style: none; + background-color: #f5f5f5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.breadcrumb > li { + display: inline-block; + *display: inline; + text-shadow: 0 1px 0 #ffffff; + *zoom: 1; +} + +.breadcrumb > li > .divider { + padding: 0 5px; + color: #ccc; +} + +.breadcrumb > .active { + color: #999999; +} + +.pagination { + margin: 20px 0; +} + +.pagination ul { + display: inline-block; + *display: inline; + margin-bottom: 0; + margin-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + *zoom: 1; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.pagination ul > li { + display: inline; +} + +.pagination ul > li > a, +.pagination ul > li > span { + float: left; + padding: 4px 12px; + line-height: 20px; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; + border-left-width: 0; +} + +.pagination ul > li > a:hover, +.pagination ul > li > a:focus, +.pagination ul > .active > a, +.pagination ul > .active > span { + background-color: #f5f5f5; +} + +.pagination ul > .active > a, +.pagination ul > .active > span { + color: #999999; + cursor: default; +} + +.pagination ul > .disabled > span, +.pagination ul > .disabled > a, +.pagination ul > .disabled > a:hover, +.pagination ul > .disabled > a:focus { + color: #999999; + cursor: default; + background-color: transparent; +} + +.pagination ul > li:first-child > a, +.pagination ul > li:first-child > span { + border-left-width: 1px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.pagination ul > li:last-child > a, +.pagination ul > li:last-child > span { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.pagination-centered { + text-align: center; +} + +.pagination-right { + text-align: right; +} + +.pagination-large ul > li > a, +.pagination-large ul > li > span { + padding: 11px 19px; + font-size: 17.5px; +} + +.pagination-large ul > li:first-child > a, +.pagination-large ul > li:first-child > span { + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.pagination-large ul > li:last-child > a, +.pagination-large ul > li:last-child > span { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.pagination-mini ul > li:first-child > a, +.pagination-small ul > li:first-child > a, +.pagination-mini ul > li:first-child > span, +.pagination-small ul > li:first-child > span { + -webkit-border-bottom-left-radius: 3px; + border-bottom-left-radius: 3px; + -webkit-border-top-left-radius: 3px; + border-top-left-radius: 3px; + -moz-border-radius-bottomleft: 3px; + -moz-border-radius-topleft: 3px; +} + +.pagination-mini ul > li:last-child > a, +.pagination-small ul > li:last-child > a, +.pagination-mini ul > li:last-child > span, +.pagination-small ul > li:last-child > span { + -webkit-border-top-right-radius: 3px; + border-top-right-radius: 3px; + -webkit-border-bottom-right-radius: 3px; + border-bottom-right-radius: 3px; + -moz-border-radius-topright: 3px; + -moz-border-radius-bottomright: 3px; +} + +.pagination-small ul > li > a, +.pagination-small ul > li > span { + padding: 2px 10px; + font-size: 11.9px; +} + +.pagination-mini ul > li > a, +.pagination-mini ul > li > span { + padding: 0 6px; + font-size: 10.5px; +} + +.pager { + margin: 20px 0; + text-align: center; + list-style: none; + *zoom: 1; +} + +.pager:before, +.pager:after { + display: table; + line-height: 0; + content: ""; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #f5f5f5; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #999999; + cursor: default; + background-color: #fff; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.modal { + position: fixed; + top: 10%; + left: 50%; + z-index: 1050; + width: 560px; + margin-left: -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + outline: none; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.modal.fade { + top: -25%; + -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; + -moz-transition: opacity 0.3s linear, top 0.3s ease-out; + -o-transition: opacity 0.3s linear, top 0.3s ease-out; + transition: opacity 0.3s linear, top 0.3s ease-out; +} + +.modal.fade.in { + top: 10%; +} + +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} + +.modal-header .close { + margin-top: 2px; +} + +.modal-header h3 { + margin: 0; + line-height: 30px; +} + +.modal-body { + position: relative; + max-height: 400px; + padding: 15px; + overflow-y: auto; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + line-height: 0; + content: ""; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + font-size: 11px; + line-height: 1.4; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + white-space: normal; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.popover.top { + margin-top: -10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-left: -10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.popover-title:empty { + display: none; +} + +.popover-content { + padding: 9px 14px; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow { + border-width: 11px; +} + +.popover .arrow:after { + border-width: 10px; + content: ""; +} + +.popover.top .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.top .arrow:after { + bottom: 1px; + margin-left: -10px; + border-top-color: #ffffff; + border-bottom-width: 0; +} + +.popover.right .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.right .arrow:after { + bottom: -10px; + left: 1px; + border-right-color: #ffffff; + border-left-width: 0; +} + +.popover.bottom .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-top-width: 0; +} + +.popover.bottom .arrow:after { + top: 1px; + margin-left: -10px; + border-bottom-color: #ffffff; + border-top-width: 0; +} + +.popover.left .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, 0.25); + border-right-width: 0; +} + +.popover.left .arrow:after { + right: 1px; + bottom: -10px; + border-left-color: #ffffff; + border-right-width: 0; +} + +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} + +.thumbnails:before, +.thumbnails:after { + display: table; + line-height: 0; + content: ""; +} + +.thumbnails:after { + clear: both; +} + +.row-fluid .thumbnails { + margin-left: 0; +} + +.thumbnails > li { + float: left; + margin-bottom: 20px; + margin-left: 20px; +} + +.thumbnail { + display: block; + padding: 4px; + line-height: 20px; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +a.thumbnail:hover, +a.thumbnail:focus { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} + +.thumbnail > img { + display: block; + max-width: 100%; + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #555555; +} + +.media, +.media-body { + overflow: hidden; + *overflow: visible; + zoom: 1; +} + +.media, +.media .media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media-object { + display: block; +} + +.media-heading { + margin: 0 0 5px; +} + +.media > .pull-left { + margin-right: 10px; +} + +.media > .pull-right { + margin-left: 10px; +} + +.media-list { + margin-left: 0; + list-style: none; +} + +.label, +.badge { + display: inline-block; + padding: 2px 4px; + font-size: 11.844px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +.label { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.badge { + padding-right: 9px; + padding-left: 9px; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} + +.label:empty, +.badge:empty { + display: none; +} + +a.label:hover, +a.label:focus, +a.badge:hover, +a.badge:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label-important, +.badge-important { + background-color: #b94a48; +} + +.label-important[href], +.badge-important[href] { + background-color: #953b39; +} + +.label-warning, +.badge-warning { + background-color: #f89406; +} + +.label-warning[href], +.badge-warning[href] { + background-color: #c67605; +} + +.label-success, +.badge-success { + background-color: #468847; +} + +.label-success[href], +.badge-success[href] { + background-color: #356635; +} + +.label-info, +.badge-info { + background-color: #3a87ad; +} + +.label-info[href], +.badge-info[href] { + background-color: #2d6987; +} + +.label-inverse, +.badge-inverse { + background-color: #333333; +} + +.label-inverse[href], +.badge-inverse[href] { + background-color: #1a1a1a; +} + +.btn .label, +.btn .badge { + position: relative; + top: -1px; +} + +.btn-mini .label, +.btn-mini .badge { + top: 0; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress .bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(to bottom, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress .bar + .bar { + -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); +} + +.progress-striped .bar { + background-color: #149bdf; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-danger .bar, +.progress .bar-danger { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); +} + +.progress-danger.progress-striped .bar, +.progress-striped .bar-danger { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-success .bar, +.progress .bar-success { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(to bottom, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); +} + +.progress-success.progress-striped .bar, +.progress-striped .bar-success { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-info .bar, +.progress .bar-info { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(to bottom, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); +} + +.progress-info.progress-striped .bar, +.progress-striped .bar-info { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-warning .bar, +.progress .bar-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); +} + +.progress-warning.progress-striped .bar, +.progress-striped .bar-warning { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.accordion { + margin-bottom: 20px; +} + +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.accordion-heading { + border-bottom: 0; +} + +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} + +.accordion-toggle { + cursor: pointer; +} + +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} + +.carousel { + position: relative; + margin-bottom: 20px; + line-height: 1; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + line-height: 1; +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + right: 15px; + left: auto; +} + +.carousel-control:hover, +.carousel-control:focus { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-indicators { + position: absolute; + top: 15px; + right: 15px; + z-index: 5; + margin: 0; + list-style: none; +} + +.carousel-indicators li { + display: block; + float: left; + width: 10px; + height: 10px; + margin-left: 5px; + text-indent: -999px; + background-color: #ccc; + background-color: rgba(255, 255, 255, 0.25); + border-radius: 5px; +} + +.carousel-indicators .active { + background-color: #fff; +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 15px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} + +.carousel-caption h4, +.carousel-caption p { + line-height: 20px; + color: #ffffff; +} + +.carousel-caption h4 { + margin: 0 0 5px; +} + +.carousel-caption p { + margin-bottom: 0; +} + +.hero-unit { + padding: 60px; + margin-bottom: 30px; + font-size: 18px; + font-weight: 200; + line-height: 30px; + color: inherit; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; + color: inherit; +} + +.hero-unit li { + line-height: 30px; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.invisible { + visibility: hidden; +} + +.affix { + position: fixed; +} diff --git a/docs/_static/bootstrap-2.3.2/css/bootstrap.min.css b/docs/_static/bootstrap-2.3.2/css/bootstrap.min.css new file mode 100644 index 00000000..b6428e69 --- /dev/null +++ b/docs/_static/bootstrap-2.3.2/css/bootstrap.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v2.3.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings-white.png b/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings-white.png new file mode 100644 index 00000000..3bf6484a Binary files /dev/null and b/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings-white.png differ diff --git a/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings.png b/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings.png new file mode 100644 index 00000000..a9969993 Binary files /dev/null and b/docs/_static/bootstrap-2.3.2/img/glyphicons-halflings.png differ diff --git a/docs/_static/bootstrap-2.3.2/js/bootstrap.js b/docs/_static/bootstrap-2.3.2/js/bootstrap.js new file mode 100644 index 00000000..638bb187 --- /dev/null +++ b/docs/_static/bootstrap-2.3.2/js/bootstrap.js @@ -0,0 +1,2287 @@ +/* =================================================== + * bootstrap-transition.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) + * ======================================================= */ + + $(function () { + + $.support.transition = (function () { + + var transitionEnd = (function () { + + var el = document.createElement('bootstrap') + , transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd' + , 'MozTransition' : 'transitionend' + , 'OTransition' : 'oTransitionEnd otransitionend' + , 'transition' : 'transitionend' + } + , name + + for (name in transEndEventNames){ + if (el.style[name] !== undefined) { + return transEndEventNames[name] + } + } + + }()) + + return transitionEnd && { + end: transitionEnd + } + + })() + + }) + +}(window.$jqTheme || window.jQuery); +/* ========================================================== + * bootstrap-alert.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.trigger(e = $.Event('close')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + var old = $.fn.alert + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT NO CONFLICT + * ================= */ + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + /* ALERT DATA-API + * ============== */ + + $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) + +}(window.$jqTheme || window.jQuery); +/* ============================================================ + * bootstrap-button.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + Button.prototype.toggle = function () { + var $parent = this.$element.closest('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.button + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON NO CONFLICT + * ================== */ + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + /* BUTTON DATA-API + * =============== */ + + $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + }) + +}(window.$jqTheme || window.jQuery); +/* ========================================================== + * bootstrap-carousel.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.prototype = { + + cycle: function (e) { + if (!e) this.paused = false + if (this.interval) clearInterval(this.interval); + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + return this + } + + , getActiveIndex: function () { + this.$active = this.$element.find('.item.active') + this.$items = this.$active.parent().children() + return this.$items.index(this.$active) + } + + , to: function (pos) { + var activeIndex = this.getActiveIndex() + , that = this + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activeIndex == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + , pause: function (e) { + if (!e) this.paused = true + if (this.$element.find('.next, .prev').length && $.support.transition.end) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + clearInterval(this.interval) + this.interval = null + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.item.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + , e + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + e = $.Event('slide', { + relatedTarget: $next[0] + , direction: direction + }) + + if ($next.hasClass('active')) return + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + this.$element.one('slid', function () { + var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) + $nextIndicator && $nextIndicator.addClass('active') + }) + } + + if ($.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } else { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.carousel + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) + , action = typeof option == 'string' ? option : options.slide + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + , pause: 'hover' + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL NO CONFLICT + * ==================== */ + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + /* CAROUSEL DATA-API + * ================= */ + + $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = $.extend({}, $target.data(), $this.data()) + , slideIndex + + $target.carousel(options) + + if (slideIndex = $this.attr('data-slide-to')) { + $target.data('carousel').pause().to(slideIndex).cycle() + } + + e.preventDefault() + }) + +}(window.$jqTheme || window.jQuery); +/* ============================================================= + * bootstrap-collapse.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* COLLAPSE PUBLIC CLASS DEFINITION + * ================================ */ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options.parent) { + this.$parent = $(this.options.parent) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension + , scroll + , actives + , hasData + + if (this.transitioning || this.$element.hasClass('in')) return + + dimension = this.dimension() + scroll = $.camelCase(['scroll', dimension].join('-')) + actives = this.$parent && this.$parent.find('> .accordion-group > .in') + + if (actives && actives.length) { + hasData = actives.data('collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', $.Event('show'), 'shown') + $.support.transition && this.$element[dimension](this.$element[0][scroll]) + } + + , hide: function () { + var dimension + if (this.transitioning || !this.$element.hasClass('in')) return + dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', $.Event('hide'), 'hidden') + this.$element[dimension](0) + } + + , reset: function (size) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') + + return this + } + + , transition: function (method, startEvent, completeEvent) { + var that = this + , complete = function () { + if (startEvent.type == 'show') that.reset() + that.transitioning = 0 + that.$element.trigger(completeEvent) + } + + this.$element.trigger(startEvent) + + if (startEvent.isDefaultPrevented()) return + + this.transitioning = 1 + + this.$element[method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* COLLAPSE PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.collapse + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSE NO CONFLICT + * ==================== */ + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + /* COLLAPSE DATA-API + * ================= */ + + $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + $(target).collapse(option) + }) + +}(window.$jqTheme || window.jQuery); +/* ============================================================ + * bootstrap-dropdown.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle=dropdown]' + , Dropdown = function (element) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function (e) { + var $this = $(this) + , $parent + , isActive + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement) { + // if mobile we we use a backdrop because click events don't delegate + $('