"""A VTK representation of HBModel."""
from __future__ import annotations
import pathlib
import shutil
import webbrowser
import tempfile
import os
import json
import warnings
from collections import defaultdict
from typing import Dict, List, Union, Tuple
from honeybee.facetype import face_types
from honeybee.model import Model as HBModel
from honeybee_radiance.sensorgrid import SensorGrid
from honeybee_radiance.writer import _filter_by_pattern
from ladybug.color import Color
from ladybug_geometry.geometry3d import Mesh3D
from .actor import Actor
from .scene import Scene
from .camera import Camera
from .types import ModelDataSet, PolyData, RadialSensor
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, ImageTypes
from .config import DataConfig, Autocalculate
from .legend_parameter import Text
from .text_actor import TextActor
_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, hb_model: HBModel,
            grid_options: SensorGridOptions = SensorGridOptions.Ignore,
            radial_sensor: RadialSensor = RadialSensor()) -> 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.
            radial_sensor: A RadialSensor object to customize the triangles to be 
                created in the radial sensor grid in case the radial grid is selected
                from the sensor grid options.
        """
        super().__init__()
        self._hb_model = hb_model
        self._sensor_grids_option = grid_options
        self._apertures = ModelDataSet('Aperture',
                                       color=self.get_default_color('Aperture'))
        self._doors = ModelDataSet('Door', color=self.get_default_color('Door'))
        self._shades = ModelDataSet('Shade', color=self.get_default_color('Shade'))
        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._radial_sensor = radial_sensor
        self._convert_model()
        self._load_grids()
        self._load_cameras()
[docs]    @classmethod
    def from_hbjson(cls, hbjson: str,
                    load_grids: SensorGridOptions = SensorGridOptions.Ignore,
                    radial_sensor: RadialSensor = RadialSensor()) -> 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.
            radial_sensor: A RadialSensor object to customize the triangles to be 
                created in the radial sensor grid in case the radial grid is selected
                from the sensor grid options.
        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, radial_sensor) 
    @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
    @cameras.setter
    def cameras(self, cams: List(Camera)) -> None:
        """Set the cameras for this Model object."""
        self._cameras = cams
[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) -> None:
        """Load sensor grids."""
        if self._sensor_grids_option == SensorGridOptions.Ignore:
            return
        if hasattr(self._hb_model.properties, 'radiance') and \
                
self._hb_model.properties.radiance.sensor_grids:
            grids = self._hb_model.properties.radiance.sensor_grids
            # list of unique sensor_grid identifiers in the model
            ids = set([grid.identifier for grid in grids])
            # if all the grids have the same identifier, merge them into one grid
            if len(ids) == 1:
                id = grids[0].identifier
                # if it's just one grid, use it
                if len(grids) == 1:
                    sensor_grid = grids[0]
                # if there are more than one grid, merge them first
                else:
                    grid_meshes = [grid.mesh for grid in grids]
                    if all(grid_meshes):
                        mesh = Mesh3D.join_meshes(grid_meshes)
                        sensor_grid = SensorGrid.from_mesh3d(id, mesh)
                    else:
                        sensors = [sensor for grid in grids for sensor
                                   in grid.sensors]
                        sensor_grid = SensorGrid(id, sensors)
                # TODO extract this to a function
                try:
                    convert_sensor_grid(sensor_grid, self._sensor_grids_option,
                                        self._radial_sensor.angle, 
                                        self._radial_sensor.radius)
                except ValueError:
                    warnings.warn(f'Grid {id} does not have mesh information. Hence, '
                                  'it will not be converted to a sensor grid. Try with'
                                  ' SensorGridOptions.Sensors.')
                else:
                    self._sensor_grids.data.append(convert_sensor_grid(
                        sensor_grid, self._sensor_grids_option, self._radial_sensor.angle,
                        self._radial_sensor.radius))
            # else add them as separate grids
            else:
                for sensor_grid in grids:
                    # TODO extract this to a function
                    try:
                        convert_sensor_grid(sensor_grid, self._sensor_grids_option,
                                            self._radial_sensor.angle,
                                            self._radial_sensor.radius)
                    except ValueError:
                        warnings.warn(f'Grid {sensor_grid.identifier} does not have'
                                      ' mesh information. Hence, it will not be'
                                      ' converted to a sensor grid. Try with'
                                      ' SensorGridOptions.Sensors.')
                    else:
                        self._sensor_grids.data.append(convert_sensor_grid(
                            sensor_grid, self._sensor_grids_option,
                            self._radial_sensor.angle, self._radial_sensor.radius))
    def _load_cameras(self) -> None:
        """Load radiance views."""
        if len(self._hb_model.properties.radiance.views) > 0:
            for view in self._hb_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) -> None:
        """An internal method to convert the objects on class initiation."""
        if hasattr(self._hb_model, 'rooms'):
            for room in self._hb_model.rooms:
                objects = convert_room(room)
                self._add_objects(self.separate_by_type(objects))
        if hasattr(self._hb_model, 'orphaned_shades'):
            for face in self._hb_model.orphaned_shades:
                self._shades.data.append(convert_shade(face))
        if hasattr(self._hb_model, 'orphaned_apertures'):
            for face in self._hb_model.orphaned_apertures:
                self._apertures.data.extend(convert_aperture(face))
        if hasattr(self._hb_model, 'orphaned_doors'):
            for face in self._hb_model.orphaned_doors:
                self._doors.data.extend(convert_door(face))
        if hasattr(self._hb_model, 'orphaned_faces'):
            for face in self._hb_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 actors(self) -> List[Actor]:
        """Create a list of vtk actors from a honeybee-vtk model.
        Args:
            model: A honeybee-vtk model.
        Returns:
            A list of vtk actors.
        """
        return [Actor(modeldataset=ds) for ds in self if len(ds.data) > 0] 
[docs]    def to_vtkjs(self, *, folder: str = '.', name: str = None, config: str = None,
                 validation: bool = False,
                 model_display_mode: DisplayMode = DisplayMode.Shaded,
                 grid_display_mode: DisplayMode = DisplayMode.Shaded) -> str:
        """Write the model to a vtkjs file.
        Write your honeybee-vtk model to a vtkjs file that you can open in
        Paraview-Glance or the Pollination Viewer.
        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.
            config: Path to the config file in JSON format. Defaults to None.
            validation: Boolean to indicate whether to validate the data before loading.
                Defaults to False.
            model_display_mode: Display mode for the model. Defaults to shaded.
            grid_display_mode: Display mode for the Grids. Defaults to shaded.
        Returns:
            A text string representing the file path to the vtkjs file.
        """
        scene = Scene()
        actors = self.actors()
        scene.add_actors(actors)
        self.update_display_mode(model_display_mode)
        self.sensor_grids.display_mode = grid_display_mode
        # load data if provided
        if config:
            self.load_config(config, scene=scene, validation=validation, legend=True)
        # 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,
                config: str = None,
                validation: bool = False,
                model_display_mode: DisplayMode = DisplayMode.Shaded,
                grid_display_mode: DisplayMode = DisplayMode.Shaded) -> str:
        """Write the model to an HTML file.
        Write your honeybee-vtk model to an HTML file that you can open in any modern
        browser and can also share with other.
        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
            config: Path to the config file in JSON format. Defaults to None.
            validation: Boolean to indicate whether to validate the data before loading.
            model_display_mode: Display mode for the model. Defaults to shaded.
            grid_display_mode: Display mode for the Grids. Defaults to shaded.
        Returns:
            A text string representing the file path to the HTML file.
        """
        self.update_display_mode(model_display_mode)
        # 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(
            folder=temp_folder, config=config, validation=validation,
            model_display_mode=model_display_mode, grid_display_mode=grid_display_mode)
        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 = None,
                 writer: VTKWriters = VTKWriters.binary, config: str = None,
                 validation: bool = False,
                 model_display_mode: DisplayMode = DisplayMode.Shaded,
                 grid_display_mode: DisplayMode = DisplayMode.Shaded) -> str:
        """
        Write a .zip of VTK/VTP files.
        Args:
            folder: File path to the output folder. The file
                will be written to the current folder if not provided.
            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.
            writer: A VTkWriters object. Default is binary which will write .vtp files.
            config: Path to the config file in JSON format. Defaults to None.
            validation: Boolean to indicate whether to validate the data before loading.
            model_display_mode: Display mode for the model. Defaults to shaded.
            grid_display_mode: Display mode for the Grids. Defaults to shaded.
        Returns:
            A text string containing the path to the .zip file with VTK/VTP files.
        """
        scene = Scene()
        actors = self.actors()
        scene.add_actors(actors)
        self.update_display_mode(model_display_mode)
        self.sensor_grids.display_mode = grid_display_mode
        # load data if provided
        if config:
            self.load_config(config, scene=scene, validation=validation, legend=True)
        # 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]    def to_images(self, *, folder: str = '.', config: str = None,
                  validation: bool = False,
                  model_display_mode: DisplayMode = DisplayMode.Shaded,
                  grid_display_mode: DisplayMode = DisplayMode.Shaded,
                  background_color: Tuple[int, int, int] = None,
                  view: List[str] = None,
                  image_type: ImageTypes = ImageTypes.png,
                  image_width: int = 0, image_height: int = 0,
                  image_scale: int = 1
                  ) -> List[str]:
        """Export images from model.
        Args:
            folder: A valid text string representing the location of folder where
                you'd want to write the images. Defaults to current working directory.
            config: Path to the config file in JSON format. Defaults to None.
            validation: Boolean to indicate whether to validate the data before loading.
            model_display_mode: Display mode for the model. Defaults to shaded.
            grid_display_mode: Display mode for the grid. Defaults to shaded.
            background_color: Background color of the image. Defaults to white.
            view: A list of paths to radiance view files. Defaults to None.
            image_type: Image type to be exported. Defaults to png.
            image_width: Image width. Defaults to 0. Which will use the default radiance
                view's horizontal angle.
            image_height: Image height. Defaults to 0. Which will use the default radiance
                view's vertical angle.
            image_scale: An integer value as a scale factor. Defaults to 1.
        Returns:
            A list of text strings representing the file paths to the images.
        """
        scene = Scene(background_color=background_color)
        actors = self.actors()
        scene.add_actors(actors)
        self.update_display_mode(model_display_mode)
        self.sensor_grids.display_mode = grid_display_mode
        if config:
            self.load_config(config, scene=scene, legend=True, validation=validation)
        # Set a default camera if there are no cameras in the model
        if not self.cameras and not view:
            camera = Camera(identifier='plan', projection='l')
            scene.add_cameras(camera)
            bounds = Actor.get_bounds(actors)
            centroid = Actor.get_centroid(actors)
            aerial_cameras = camera.aerial_cameras(bounds, centroid)
            scene.add_cameras(aerial_cameras)
        else:
            if len(self.cameras) != 0:
                cameras = self.cameras
                scene.add_cameras(cameras)
            if view:
                for vf in view:
                    camera = Camera.from_view_file(file_path=vf)
                    scene.add_cameras(camera)
        for actor in scene.actors:
            if actor.name == 'Grid':
                assert actor.modeldataset.display_mode == grid_display_mode, 'Grid display'\
                    
' mode is not set correctly.'
            else:
                assert actor.modeldataset.display_mode == model_display_mode, 'Model display'\
                    
' mode is not set correctly.'
        return scene.export_images(
            folder=folder, image_type=image_type,
            image_width=image_width, image_height=image_height,
            image_scale=image_scale
            ) 
[docs]    def to_grid_images(self, config: str, *, folder: str = '.',
                       grid_filter: Union[str, List[str]] = '*',
                       full_match: bool = False,
                       grid_display_mode: DisplayMode = DisplayMode.SurfaceWithEdges,
                       background_color: Tuple[int, int, int] = None,
                       image_type: ImageTypes = ImageTypes.png,
                       image_width: int = 0, image_height: int = 0,
                       image_name: str = '', image_scale: int = 1,
                       text_actor: TextActor = None,
                       grid_camera_dict: Dict[str, vtk.vtkCamera] = None,
                       extract_camera: bool = False,
                       grid_colors: List[Color] = None,
                       sub_folder_name: str = None) -> Union[Dict[str, Camera],
                                                             List[str]]:
        """Export am image for each grid in the model.
        Use the config file to specify which grids with which data to export. For
        instance, if the config file has DataConfig objects for 'DA' and 'UDI', and
        'DA' is kept hidden, then all grids with 'UDI' data will be exported.
        Additionally, images from a selected number of grids can be exported by
        using the by specifying the identifiers of the grids to export in the
        grid_filter object in the config file.
        Note that the parameters grid_camera_dict and extract_camera are mutually
        exclusive. If both are provided, the extract_camera will be used and the
        grid_camera_dict will be ignored.
        Args:
            config: Path to the config file in JSON format.
            folder: Path to the folder where you'd like to export the images. Defaults to
                    the current working directory.
            grid_filter: A list of grid identifiers or a regex pattern as a string to
                filter the grids. Defaults to None.
            full_match: A boolean to filter grids by their identifiers as full matches.
                Defaults to False.
            display_mode: Display mode for the grid. Defaults to surface with edges.
            background_color: Background color of the image. Defaults to white.
            image_type: Image type to be exported. Defaults to png.
            image_width: Image width in pixels. Defaults to 0. Which will use the
                default radiance view's horizontal angle to derive the width.
            image_height: Image height in pixels. Defaults to 0. Which will use the
                default radiance view's vertical angle to derive the height.
            image_name: A text string that sets the name of the image. Defaults to ''.
            image_scale: An integer value as a scale factor. Defaults to 1.
            text_actor: A TextActor object that defines the properties of the text to be
                added to the image. Defaults to None.
            grid_camera_dict: A dictionary of grid identifiers and vtkCamera objects.
                If provided, the camera objects specified in the dict will be used to
                export the images. This is useful when a camera from another run is
                to be used in this run to export an image. Defaults to None.
            extract_camera: Boolean to indicate whether to extract the camera from the
                for this run to use for the next run. Defaults to False.
            sub_folder_name: A text string that sets the name of the subfolder where
                the images will be exported. This is useful when the images are to be
                exported for multiple time periods such as whole day of June 21, and
                the whole day of March, 21. Defaults to None.
        Returns:
            Path to the folder where the images are exported for each grid. Or a
            dictionary of grid identifiers and camera objects.
        """
        assert len(self.sensor_grids.data) != 0, 'No sensor grids found in the model.'
        if self._sensor_grids_option == SensorGridOptions.Sensors:
            grid_display_mode = DisplayMode.Points
        config_data = self.load_config(config)
        grid_polydata_lst = _filter_grid_polydata(
            self.sensor_grids.data, self._hb_model, grid_filter, full_match)
        output: Union[Dict[str, Camera], List[str]] = {} if extract_camera else []
        grid_colors_supplied = True if grid_colors else False
        for data in config_data:
            for grid_polydata in grid_polydata_lst:
                dataset = ModelDataSet(name=grid_polydata.identifier,
                                       data=[grid_polydata],
                                       display_mode=grid_display_mode)
                if not grid_colors_supplied and data.grid_colors:
                    grid_colors = [Color(r, g, b) for r, g, b in data.grid_colors]
                dataset.color_by = data.identifier
                if grid_colors:
                    if len(grid_colors) == 1:
                        dataset.active_field_info.legend_parameter._assign_colors(
                            [Color(255, 255, 255), grid_colors[0]])
                    else:
                        dataset.active_field_info.legend_parameter._assign_colors(
                            grid_colors)
                actor = Actor(dataset)
                camera = _camera_to_grid_actor(actor, data.identifier)
                scene = Scene(background_color=background_color, actors=[actor],
                              cameras=[camera],
                              text_actor=text_actor)
                legend_range = self._get_legend_range(data)
                self._load_legend_parameters(data, scene, legend_range)
                if extract_camera:
                    vtk_camera = None
                    output[grid_polydata.identifier] = scene.export_images(
                        folder=folder, image_type=image_type, image_width=image_width,
                        image_height=image_height, image_name=image_name,
                        image_scale=image_scale,
                        vtk_camera=vtk_camera, extract_camera=extract_camera
                    )
                else:
                    if grid_camera_dict:
                        vtk_camera = grid_camera_dict[grid_polydata.identifier]
                    else:
                        vtk_camera = None
                    # this is not a good design but it takes too much refactoring to
                    # remove it. I'm writing this down for whenever we get a chance
                    # to refactor honeybee-vtk.
                    if not sub_folder_name:
                        grid_folder = pathlib.Path(
                            f'{folder}/{grid_polydata.identifier}')
                    else:
                        grid_folder = pathlib.Path(
                            f'{folder}/{grid_polydata.identifier}/{sub_folder_name}')
                    if not grid_folder.exists():
                        grid_folder.mkdir(parents=True, exist_ok=True)
                    output += scene.export_images(folder=grid_folder,
                                                  image_type=image_type,
                                                  image_width=image_width,
                                                  image_height=image_height,
                                                  image_name=image_name,
                                                  image_scale=image_scale,
                                                  vtk_camera=vtk_camera)
        return output 
[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 
[docs]    def load_config(self, json_path: str, scene: Scene = None,
                    validation: bool = False, legend: bool = False) -> List[DataConfig]:
        """Mount data on model from config json.
        Args:
            json_path: File path to the config json file.
            scene: A honeybee-vtk scene object. Defaults to None.
            validation: A boolean indicating whether to validate the data before loading.
            legend: A boolean indicating whether to load legend parameters.
        Returns:
            A list of parsed DataConfig objects.
        """
        assert len(self.sensor_grids.data) > 0, 'Sensor grids are not loaded on'
        ' this model. Reload them using grid options.'
        config_dir = pathlib.Path(json_path).parent
        config_data: List[DataConfig] = []
        try:
            with open(json_path) as fh:
                config = json.load(fh)
        except json.decoder.JSONDecodeError:
            raise TypeError(
                'Not a valid json file.'
            )
        else:
            for json_obj in config['data']:
                # validate config
                data = DataConfig.parse_obj(json_obj)
                # only if data is requested move forward.
                if not data.hide:
                    folder_path = pathlib.Path(data.path)
                    if not folder_path.is_dir():
                        folder_path = config_dir.joinpath(
                            folder_path).resolve().absolute()
                        data.path = folder_path.as_posix()
                        if not folder_path.is_dir():
                            raise FileNotFoundError(
                                f'No folder found at {data.path}')
                    grid_type = self._get_grid_type()
                    # Validate data if asked for
                    if validation:
                        self._validate_simulation_data(data, grid_type)
                    # get legend range if provided by the user
                    legend_range = self._get_legend_range(data)
                    # Load data
                    self._load_data(data, grid_type, legend_range)
                    # Load legend parameters
                    if legend:
                        self._load_legend_parameters(data, scene, legend_range)
                    config_data.append(data)
                else:
                    warnings.warn(
                        f'Data for {data.identifier} is not loaded.'
                    )
            return config_data 
    def _get_grid_type(self) -> str:
        """Get the type of grid in the model
        Args:
            model(Model): A honeybee-vtk model.
        Returns:
            A string indicating whether the model has points and meshes.
        """
        if self.sensor_grids.data[0].GetNumberOfCells() == 1:
            return 'points'
        else:
            return 'meshes'
    def _validate_simulation_data(self, data: DataConfig, grid_type: str) -> None:
        """Match result data with the sensor grids in the model.
        It will be checked if the number of data files and the names of the
        data files match with the grid identifiers. This function does not support
        validating result data for other than sensor grids as of now.
        This is a helper method to the public load_config method.
        Args:
            data: A DataConfig object.
            grid_type: A string indicating whether the model has points and meshes.
        """
        # file path to the json file
        grids_info_json = pathlib.Path(data.path).joinpath('grids_info.json')
        # read the json file
        with open(grids_info_json) as fh:
            grids_info = json.load(fh)
        # TODO: Make sure to remove this limitation. A user should not have to always
        # TODO: load all the grids
        assert len(self.sensor_grids.data) == len(grids_info), 'The number of result'\
            
f' files {len(grids_info)} does for {data.identifier} does not match'\
            
f' the number of sensor grids in the model {len(self.sensor_grids.data)}.'
        # match identifiers of the grids with the identifiers of the result files
        grids_model_identifiers = [grid.identifier for grid in self.sensor_grids.data]
        grids_info_identifiers = [grid['identifier'] for grid in grids_info]
        assert grids_model_identifiers == grids_info_identifiers, 'The identifiers of'\
            
' the sensor grids in the model do not match the identifiers of the grids'\
            
f' in the grids_info.json for {data.identifier}.'
        # make sure length of each file matches the number of sensors in grid
        file_lengths = [grid['count'] for grid in grids_info]
        # check if the grid data is meshes or points
        # if grid is sensors
        if grid_type == 'points':
            num_sensors = [polydata.GetNumberOfPoints()
                           for polydata in self.sensor_grids.data]
        # if grid is meshes
        else:
            num_sensors = [polydata.GetNumberOfCells()
                           for polydata in self.sensor_grids.data]
        # lastly check if the length of a file matches the number of sensors or
        # meshes on grid
        if file_lengths != num_sensors:
            length_matching = {
                grids_info_identifiers[i]: file_lengths[i] == num_sensors[i] for i in
                range(len(grids_model_identifiers))
            }
            names_to_report = [
                id for id in length_matching if length_matching[id] is False]
            raise ValueError(
                'File lengths of result files must match the number of sensors on grids.'
                ' Lengths of files with following names do not match'
                f' {tuple(names_to_report)}.')
    def _load_data(self, data: DataConfig, grid_type: str,
                   legend_range: List[Union[float, int]]) -> None:
        """Load validated data on a honeybee-vtk model.
        This is a helper method to the public load_config method.
        Args:
            data (DataConfig): A Dataconfig object.
            model: A honeybee-vtk model.
            grid_type: A string indicating whether the sensor grid in the model is
                made of points or meshes.
            legend_range: A list of min and max values of the legend parameters
                provided by the user in the config file.
        """
        folder_path = pathlib.Path(data.path)
        folder_path = folder_path.as_posix()
        folder_path = pathlib.Path(folder_path)
        identifier = data.identifier
        if isinstance(data.lower_threshold, float):
            lower_threshold = data.lower_threshold
        else:
            lower_threshold = None
        if isinstance(data.upper_threshold, float):
            upper_threshold = data.upper_threshold
        else:
            upper_threshold = None
        result_file_paths = _get_result_file_paths(folder_path)
        result = []
        for res_file_path in result_file_paths:
            assert res_file_path.exists(), f'No file found at {res_file_path}'
            grid_res = [float(v)
                        for v in res_file_path.read_text().splitlines()]
            result.append(grid_res)
        ds = self.get_modeldataset(DataSetNames.grid)
        if grid_type == 'meshes':
            ds.add_data_fields(result, name=identifier, per_face=True,
                               data_range=legend_range, lower_threshold=lower_threshold,
                               upper_threshold=upper_threshold)
            ds.color_by = identifier
        else:
            ds.add_data_fields(result, name=identifier, per_face=False,
                               data_range=legend_range, lower_threshold=lower_threshold,
                               upper_threshold=upper_threshold)
            ds.color_by = identifier
    @ staticmethod
    def _get_legend_range(data: DataConfig) -> List[Union[float, int]]:
        """Read and get legend min and max values from data if provided by the user.
        The value provided by this function is processed and validated in _get_data_range
        function in the type module.
        Args:
            data (DataConfig): A Dataconfig object.
        Returns:
            A list of two numbers representing min and max values for data.
        """
        if data.legend_parameters:
            legend_params = data.legend_parameters
            if isinstance(legend_params.min, Autocalculate):
                min = None
            else:
                min = legend_params.min
            if isinstance(legend_params.max, Autocalculate):
                max = None
            else:
                max = legend_params.max
            return [min, max]
    def _load_legend_parameters(self, data: DataConfig, scene: Scene,
                                legend_range: List[Union[float, int]]) -> None:
        """Load legend_parameters.
        Args:
            data: A Dataconfig object.
            scene: A honeyebee-vtk scene object.
            legend_range: A list of min and max values of the legend parameters provided by
                the user in the config file.
        """
        legend_params = data.legend_parameters
        legend = scene.legend_parameter(data.identifier)
        legend.colorset = legend_params.color_set
        legend.reverse_colorset = legend_params.reverse_color_set
        legend.unit = data.unit
        if legend_range:
            legend.min, legend.max = legend_range
        else:
            legend.min, legend.max = [None, None]
        legend.hide_legend = legend_params.hide_legend
        legend.orientation = legend_params.orientation
        legend.position = legend_params.position
        legend.width = legend_params.width
        legend.height = legend_params.height
        if isinstance(legend_params.color_count, int):
            legend.color_count = legend_params.color_count
        else:
            legend.color_count = None
        if isinstance(legend_params.label_count, int):
            legend.label_count = legend_params.label_count
        else:
            legend.label_count = None
        legend.decimal_count = legend_params.decimal_count
        legend.preceding_labels = legend_params.preceding_labels
        label_params = legend_params.label_parameters
        legend.label_parameters = Text(
            label_params.color, label_params.size, label_params.bold)
        title_params = legend_params.title_parameters
        legend.title_parameters = Text(
            title_params.color, title_params.size, title_params.bold) 
def _camera_to_grid_actor(actor: Actor, data_name: str, zoom: int = 2,
                          auto_zoom: bool = True, camera_offset: int = 3,
                          clipping_range: Tuple[int, int] = (0, 4), ) -> Camera:
    """Create a Camera for a grid actor.
    This function uses the center point of a grid actor to create a camera that is
    setup at the camera_offset distance from the center point.
    Args:
        actor: An Actor object.
        data_name: name of the data being loaded on the grid. This is
            used in naming the image files.
        zoom: The zoom level of the camera. Defaults to 2.
        auto_zoom: A boolean to set the camera to auto zoom. Setting this to True will
            discard the Zoom level. Set this to False to use the zoom level. Defaults to
            True.
        camera_offset: The distance between the camera and the sensor grid.
            Defaults to 100.
        clipping_range: The clipping range of the camera. Defaults to (100, 101).
    Returns:
        A Camera object.
    """
    cent_pt = actor.centroid
    return Camera(identifier=f'{data_name}_{actor.name}',
                  position=(cent_pt.x, cent_pt.y, cent_pt.z + camera_offset),
                  projection='l',
                  focal_point=cent_pt,
                  clipping_range=clipping_range,
                  parallel_scale=zoom,
                  reset_camera=auto_zoom)
def _get_result_file_paths(folder_path: Union[str, pathlib.Path]):
    if not isinstance(folder_path, pathlib.Path):
        folder_path = pathlib.Path(folder_path)
    grids_info_json = folder_path.joinpath('grids_info.json')
    with open(grids_info_json) as fh:
        grids_info = json.load(fh)
    # finding file extension for grid results
    # This could have been avoided if the file extension was provided in the
    # grids_info.json file.
    extension = None
    match = pathlib.Path(grids_info[0]['full_id']).stem
    for path in folder_path.rglob('*'):
        if path.stem == match:
            extension = path.suffix
            break
    else:
        raise ValueError(f'Failed to find the extension from {path}')
    # result file paths
    return [folder_path.joinpath(f"{grid['full_id']}{extension}")
            for grid in grids_info]
def _filter_grid_polydata(grid_polydata_lst: List[PolyData], model: HBModel,
                          grid_filter: Union[str, List[str]],
                          full_match) -> List[PolyData]:
    """Filter grid polydata based on sensor grids.
    Args:
        grid_polydata_lst: A list of grid polydata objects.
        model: A honeybee model object.
        grid_filter: A list of grid identifiers or a regex pattern as a string to filter
            the grid polydata.
        full_match: A boolean to filter grids by their identifiers as full matches.
    Returns:
        A list of PolyData objects for Grids.
    """
    if not grid_filter or grid_filter[0] == '*':
        return grid_polydata_lst
    else:
        filtered_sensor_grids = _filter_by_pattern(
            model.properties.radiance.sensor_grids, grid_filter, full_match)
        sensorgrid_identifiers = [
            grid.identifier for grid in filtered_sensor_grids]
        filtered_grid_polydata_lst = [grid for grid in grid_polydata_lst
                                      if grid.name in sensorgrid_identifiers]
        if not filtered_grid_polydata_lst:
            raise ValueError('No grids found in the model that match the'
                             f' filter {grid_filter}.')
        return filtered_grid_polydata_lst