Source code for honeybee_vtk.model

"""A VTK representation of HBModel."""

from __future__ import annotations
from json.decoder import JSONDecodeError
import pathlib
import shutil
import webbrowser
import tempfile
import os
import tempfile

from collections import defaultdict
from typing import Dict, List
from honeybee.facetype import face_types
from honeybee.model import Model as HBModel
from ladybug.color import Color
from .camera import Camera
from .types import ModelDataSet, PolyData
from .to_vtk import convert_aperture, convert_face, convert_room, convert_shade, \
    convert_sensor_grid, convert_door
from .vtkjs.schema import IndexJSON, DisplayMode, SensorGridOptions
from .vtkjs.helper import convert_directory_to_zip_file, add_data_to_viewer
from .types import DataSetNames, VTKWriters, JoinedPolyData


_COLORSET = {
    'Wall': [0.901, 0.705, 0.235, 1],
    'Aperture': [0.250, 0.705, 1, 0.5],
    'Door': [0.627, 0.588, 0.392, 1],
    'Shade': [0.470, 0.294, 0.745, 1],
    'Floor': [1, 0.501, 0.501, 1],
    'RoofCeiling': [0.501, 0.078, 0.078, 1],
    'AirBoundary': [1, 1, 0.784, 1],
    'Grid': [0.925, 0.250, 0.403, 1]
}


DATA_SETS = {
    'Aperture': 'apertures', 'Door': 'doors', 'Shade': 'shades',
    'Wall': 'walls', 'Floor': 'floors', 'RoofCeiling': 'roof_ceilings',
    'AirBoundary': 'air_boundaries'
}


[docs]class Model(object): """A honeybee-vtk model. The model objects are accessible based on their types: * apertures * doors * shades * walls * floors * roof_ceilings * air_boundaries * sensor_grids You can control the style for each type separately. """ def __init__( self, model: HBModel, load_grids: SensorGridOptions = SensorGridOptions.Ignore) -> None: """Instantiate a honeybee-vtk model object. Args: model : A text string representing the path to the hbjson file. load_grids: A SensorGridOptions object. Defaults to SensorGridOptions.Ignore which will ignore the grids in hbjson and will not load them in the honeybee-vtk model. """ super().__init__() # apertures and orphaned apertures self._apertures = \ ModelDataSet('Aperture', color=self.get_default_color('Aperture')) # doors and orphaned doors self._doors = ModelDataSet('Door', color=self.get_default_color('Door')) # shades and orphaned shades self._shades = ModelDataSet('Shade', color=self.get_default_color('Shade')) # face objects based on type self._walls = ModelDataSet('Wall', color=self.get_default_color('Wall')) self._floors = ModelDataSet('Floor', color=self.get_default_color('Floor')) self._roof_ceilings = \ ModelDataSet('RoofCeiling', color=self.get_default_color('RoofCeiling')) self._air_boundaries = \ ModelDataSet('AirBoundary', color=self.get_default_color('AirBoundary')) self._sensor_grids = ModelDataSet('Grid', color=self.get_default_color('Grid')) self._cameras = [] self._convert_model(model) self._load_grids(model, load_grids) self._load_cameras(model) self._sensor_grids_option = load_grids # keep this for adding data
[docs] @classmethod def from_hbjson(cls, hbjson: str, load_grids: SensorGridOptions = SensorGridOptions.Ignore) -> Model: """Translate hbjson to a honeybee-vtk model. Args: model : A text string representing the path to the hbjson file. load_grids: A SensorGridOptions object. Defaults to SensorGridOptions.Ignore which will ignore the grids in hbjson and will not load them in the honeybee-vtk model. Returns: A honeybee-vtk model object. """ hb_file = pathlib.Path(hbjson) assert hb_file.is_file(), f'{hbjson} doesn\'t exist.' model = HBModel.from_hbjson(hb_file.as_posix()) return cls(model, load_grids)
@property def walls(self) -> ModelDataSet: """Model walls.""" return self._walls @property def apertures(self) -> ModelDataSet: """Model aperture.""" return self._apertures @property def shades(self) -> ModelDataSet: """Model shades.""" return self._shades @property def doors(self) -> ModelDataSet: """Model doors.""" return self._doors @property def floors(self) -> ModelDataSet: """Model floors.""" return self._floors @property def roof_ceilings(self) -> ModelDataSet: """Roof and ceilings.""" return self._roof_ceilings @property def air_boundaries(self) -> ModelDataSet: """Air boundaries.""" return self._air_boundaries @property def sensor_grids(self) -> ModelDataSet: """Sensor grids.""" return self._sensor_grids @property def cameras(self): """List of Camera objects attached to this Model object.""" return self._cameras
[docs] def get_modeldataset(self, dataset: DataSetNames) -> ModelDataSet: """Get a ModelDataSet object from a model. Args: dataset: A DataSetNames object. Returns: A ModelDataSet object. """ ds = {ds.name.lower(): ds for ds in self} return ds[dataset.value]
def __iter__(self): """This dunder method makes this class an iterator object. Due to this method, you can access apertures, walls, shades, doors, floors, roof_ceilings, air_boundaries and sensor_grids in a model like items of a list in Python. Which means, you can use loops on these objects of a model. """ for dataset in ( self.apertures, self.walls, self.shades, self.doors, self.floors, self.roof_ceilings, self.air_boundaries, self.sensor_grids ): yield dataset def _load_grids(self, model: HBModel, grid_options: SensorGridOptions) -> None: """Load sensor grids.""" if grid_options == SensorGridOptions.Ignore: return if hasattr(model.properties, 'radiance'): for sensor_grid in model.properties.radiance.sensor_grids: self._sensor_grids.data.append( convert_sensor_grid(sensor_grid, grid_options) ) def _load_cameras(self, model: HBModel) -> None: """Load radiance views.""" if len(model.properties.radiance.views) > 0: for view in model.properties.radiance.views: self._cameras.append(Camera.from_view(view))
[docs] def update_display_mode(self, value: DisplayMode) -> None: """Change display mode for all the object types in the model. Sensor grids display model will not be affected. For changing the display model for a single object type, change the display_mode property separately. .. code-block:: python model.sensor_grids.display_mode = DisplayMode.Wireframe """ for attr in DATA_SETS.values(): self.__getattribute__(attr).display_mode = value
def _convert_model(self, model: HBModel) -> None: """An internal method to convert the objects on class initiation.""" if hasattr(model, 'rooms'): for room in model.rooms: objects = convert_room(room) self._add_objects(self.separate_by_type(objects)) if hasattr(model, 'orphaned_shades'): for face in model.orphaned_shades: self._shades.data.append(convert_shade(face)) if hasattr(model, 'orphaned_apertures'): for face in model.orphaned_apertures: self._apertures.data.extend(convert_aperture(face)) if hasattr(model, 'orphaned_doors'): for face in model.orphaned_doors: self._doors.data.extend(convert_door(face)) if hasattr(model, 'orphaned_faces'): for face in model.orphaned_faces: objects = convert_face(face) self._add_objects(self.separate_by_type(objects)) def _add_objects(self, data: Dict) -> None: """Add object to different fields based on data type. This method is called from inside ``_convert_model``. Valid values for key are different types: * aperture * door * shade * wall * floor * roof_ceiling * air_boundary * sensor_grid """ for key, value in data.items(): try: # Note: this approach will fail for air_boundary attr = DATA_SETS[key] self.__getattribute__(attr).data.extend(value) except KeyError: raise ValueError(f'Unsupported type: {key}') except AttributeError: raise AttributeError(f'Invalid attribute: {attr}')
[docs] def to_vtkjs(self, folder: str = '.', name: str = None) -> str: """Write a vtkjs file. Write your honeybee-vtk model to a vtkjs file that you can open in Paraview-Glance. Args: folder: A valid text string representing the location of folder where you'd want to write the vtkjs file. Defaults to current working directory. name : Name for the vtkjs file. File name will be Model.vtkjs if not provided. Returns: A text string representing the file path to the vtkjs file. """ # name of the vtkjs file file_name = name or 'model' # create a temp folder temp_folder = tempfile.mkdtemp() # The folder set by the user is the target folder target_folder = os.path.abspath(folder) # Set a file path to move the .zip file to the target folder target_vtkjs_file = os.path.join(target_folder, file_name + '.vtkjs') # write every dataset scene = [] for data_set in DATA_SETS.values(): data = getattr(self, data_set) path = data.to_folder(temp_folder) if not path: # empty dataset continue scene.append(data.as_data_set()) # add sensor grids # it is separate from other DATA_SETS mainly for data visualization data = self.sensor_grids path = data.to_folder(temp_folder) if path: scene.append(data.as_data_set()) # write index.json index_json = IndexJSON() index_json.scene = scene index_json.to_json(temp_folder) # zip as vtkjs temp_vtkjs_file = convert_directory_to_zip_file(temp_folder, extension='vtkjs', move=False) # Move the generated vtkjs to target folder shutil.move(temp_vtkjs_file, target_vtkjs_file) try: shutil.rmtree(temp_folder) except Exception: pass return target_vtkjs_file
[docs] def to_html(self, folder: str = '.', name: str = None, show: bool = False) -> str: """Write an HTML file. Write your honeybee-vtk model to an HTML file. Args: folder: A valid text string representing the location of folder where you'd want to write the HTML file. Defaults to current working directory. name : Name for the HTML file. File name will be Model.html if not provided. show: A boolean value. If set to True, the HTML file will be opened in the default browser. Defaults to False Returns: A text string representing the file path to the HTML file. """ # Name of the html file file_name = name or 'model' # Set the target folder target_folder = os.path.abspath(folder) # Set a file path to move the .html file to the target folder html_file = os.path.join(target_folder, file_name + '.html') # Set temp folder to do the operation temp_folder = tempfile.mkdtemp() vtkjs_file = self.to_vtkjs(temp_folder) temp_html_file = add_data_to_viewer(vtkjs_file) shutil.copy(temp_html_file, html_file) try: shutil.rmtree(temp_folder) except Exception: pass if show: webbrowser.open(html_file) return html_file
[docs] def to_files(self, folder: str, name: str, writer: VTKWriters) -> str: """ Write a .zip of VTK/VTP files. Args: name: A text string for the name of the .zip file to be written. If no text string is provided, the name of the HBJSON file will be used as a file name for the .zip file. folder: File path to the output folder. The file will be written to the current folder if not provided. writer: A VTkWriters object. Returns: A text string containing the path to the .zip file with VTK/VTP files. """ # Name of the html file file_name = name or 'model' # Set the target folder target_folder = os.path.abspath(folder) # Set a file path to move the .zip file to the target folder target_zip_file = os.path.join(target_folder, file_name + '.zip') # Set temp folder to do the operation temp_folder = tempfile.mkdtemp() # Write datasets to vtk/vtp files for ds in self: if len(ds.data) == 0: continue elif len(ds.data) > 1: jp = JoinedPolyData() jp.extend(ds.data) jp.to_vtk(temp_folder, ds.name, writer) elif len(ds.data) == 1: polydata = ds.data[0] polydata.to_vtk(temp_folder, ds.name, writer) # collect files in a zip temp_zip_file = convert_directory_to_zip_file(temp_folder, extension='zip', move=False) # Move the generated zip file to the target folder shutil.move(temp_zip_file, target_zip_file) try: shutil.rmtree(temp_folder) except Exception: pass return target_zip_file
[docs] @ staticmethod def get_default_color(face_type: face_types) -> Color: """Get the default color based of face type. Use these colors to generate visualizations that are familiar for Ladybug Tools users. User can overwrite these colors as needed. This method converts decimal RGBA to integer RGBA values. """ color = _COLORSET.get(face_type, [1, 1, 1, 1]) return Color(*(v * 255 for v in color))
[docs] @ staticmethod def separate_by_type(data: List[PolyData]) -> Dict: """Separate PolyData objects by type.""" data_dict = defaultdict(list) for d in data: data_dict[d.type].append(d) return data_dict