diff --git a/dragonfly_display/cli/__init__.py b/dragonfly_display/cli/__init__.py index ef9f683..a5f456a 100644 --- a/dragonfly_display/cli/__init__.py +++ b/dragonfly_display/cli/__init__.py @@ -37,8 +37,7 @@ def display(): '--no-ceil-adjacency/--ceil-adjacency', ' /-a', help='Flag to indicate ' '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.', + 'that Surface boundary conditions are used instead of Adiabatic ones.', default=True, show_default=True) @click.option( '--color-by', '-c', help='Text for the property that dictates the colors of ' @@ -70,7 +69,7 @@ def display(): 'in the resulting VisualizationSet. Multiple instances of this option can be passed ' 'and a separate VisualizationData will be added to the AnalysisGeometry that ' 'represents the attribute in the resulting VisualizationSet (or a separate ' - 'ContextGeometry layer if room_text_labels is True). Room attributes ' + 'ContextGeometry layer if --text-attr is True). Room attributes ' 'input here can have . that separates the nested attributes from ' 'one another. For example, properties.energy.program_type.', type=click.STRING, multiple=True, default=None, show_default=True) @@ -80,7 +79,7 @@ def display(): 'the resulting VisualizationSet. Multiple instances of this option can be passed and' ' a separate VisualizationData will be added to the AnalysisGeometry that ' 'represents the attribute in the resulting VisualizationSet (or a separate ' - 'ContextGeometry layer if face_text_labels is True). Face attributes ' + 'ContextGeometry layer if --text-attr is True). Face attributes ' 'input here can have . that separates the nested attributes from ' 'one another. For example, properties.energy.construction.', type=click.STRING, multiple=True, default=None, show_default=True) @@ -97,7 +96,7 @@ def display(): 'SurfaceWithEdges, None.', type=str, default='Default', show_default=True) @click.option( - '--hide-grid/--show-grid', ' /-sg', help='Flag to note whether the SensorGrid ' + '--show-grid/--hide-grid', ' /-hg', help='Flag to note whether the SensorGrid ' 'ContextGeometry should be hidden or shown by default.', default=True, show_default=True) @click.option( @@ -110,13 +109,13 @@ def display(): 'and the html format refers to a web page with the vtkjs file embedded within it.', type=str, default='vsf', show_default=True) @click.option( - '--output-file', help='Optional file to output the JSON string of ' - 'the config object. By default, it will be printed out to stdout', + '--output-file', help='Optional file to output the string of the visualization ' + 'file contents. By default, it will be printed out to stdout.', type=click.File('w'), default='-', show_default=True) -def model_to_vis_set( +def model_to_vis_set_cli( model_file, multiplier, no_ceil_adjacency, color_by, wireframe, mesh, show_color_by, - room_attr, face_attr, color_attr, grid_display_mode, hide_grid, + room_attr, face_attr, color_attr, grid_display_mode, show_grid, output_format, output_file): """Translate a Dragonfly Model file (.dfjson) to a VisualizationSet file (.vsf). @@ -128,75 +127,196 @@ def model_to_vis_set( model_file: Full path to a Dragonfly Model (DFJSON or DFpkl) file. """ try: - model_obj = Model.from_file(model_file) + # process all of the CLI input so that it can be passed to the function + full_geometry = not multiplier + ceil_adjacency = not no_ceil_adjacency + exclude_wireframe = not wireframe + faces = not mesh + hide_color_by = not show_color_by room_attrs = [] if len(room_attr) == 0 or room_attr[0] == '' else room_attr face_attrs = [] if len(face_attr) == 0 or face_attr[0] == '' else face_attr text_labels = not color_attr - hide_color_by = not show_color_by + hide_grid = not show_grid - face_attributes = [] - for fa in face_attrs: - faa = FaceAttribute(name=fa, attrs=[fa], color=color_attr, text=text_labels) - face_attributes.append(faa) + # pass the input to the function in order to convert the + model_to_vis_set(model_file, full_geometry, ceil_adjacency, color_by, + exclude_wireframe, faces, hide_color_by, + room_attrs, face_attrs, text_labels, grid_display_mode, + hide_grid, output_format, output_file) + except Exception as e: + _logger.exception('Failed to translate Model to VisualizationSet.\n{}'.format(e)) + sys.exit(1) + else: + sys.exit(0) - room_attributes = [] - for ra in room_attrs: - raa = RoomAttribute(name=ra, attrs=[ra], color=color_attr, text=text_labels) - room_attributes.append(raa) - ceil_adjacency = not no_ceil_adjacency - vis_set = model_obj.to_vis_set( - multiplier, ceil_adjacency, color_by=color_by, include_wireframe=wireframe, - use_mesh=mesh, hide_color_by=hide_color_by, room_attrs=room_attributes, - face_attrs=face_attributes, grid_display_mode=grid_display_mode, - hide_grid=hide_grid) - output_format = output_format.lower() - if output_format in ('vsf', 'json'): +def model_to_vis_set( + model_file, full_geometry=False, ceil_adjacency=False, color_by='type', + exclude_wireframe=False, faces=False, hide_color_by=False, + room_attr=(), face_attr=(), text_attr=False, grid_display_mode='Default', + hide_grid=False, output_format='vsf', output_file=None, + multiplier=True, no_ceil_adjacency=True, wireframe=True, mesh=True, + show_color_by=True, color_attr=True, show_grid=True +): + """Translate a Dragonfly Model file (.dfjson) to a VisualizationSet file (.vsf). + + This function can also optionally translate the Dragonfly Model to a .vtkjs file, + which can be visualized in the open source Visual ToolKit (VTK) platform. + + Args: + model_file: Path to a Dragonfly Model (DFJSON or DFpkl) file. + full_geometry: Boolean to note if the multipliers on each Story should + be passed along to the generated Honeybee Room objects or if full + geometry objects should be written for each story in the building. + ceil_adjacency: Boolean to indicate 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. + color_by: Text for the property that dictates the colors of the Model + geometry. Choose from: type, boundary_condition, none. If none, only + a wireframe of the Model will be generated (assuming the exclude_wireframe + option is not used). None is useful when the primary purpose of the + visualization is to display results in relation to the Model geometry + or display some room_attr or face_attr as an AnalysisGeometry or Text labels. + exclude_wireframe: Boolean to note whether a ContextGeometry dedicated to + the Model Wireframe (in DisplayLineSegment3D) should be included in + the output visualization. + faces: Boolean to note whether the colored model geometries should be + represented with DisplayMesh3D objects instead of DisplayFace3D objects. + Meshes can usually be rendered faster and they scale well for large models + but all geometry is triangulated (meaning that their wireframe in certain + platforms might not appear ideal). + hide_color_by: Boolean to note whether the color-by geometry should be + hidden or shown by default. Hiding the color-by geometry is useful + when the primary purpose of the visualization is to display grid_data + or room/face attributes but it is still desirable to have the option + to turn on the geometry. + room_attr: An optional text string of an attribute that the Model Rooms + have, which will be used to construct a visualization of this attribute + in the resulting VisualizationSet. A list of text can also + be passed and a separate VisualizationData will be added to the + AnalysisGeometry that represents the attribute in the resulting + VisualizationSet (or a separate ContextGeometry layer if text_attr + is True). Room attributes input here can have . that separates the nested + attributes from one another. For example, properties.energy.program_type. + face_attr: An optional text string of an attribute that the Model Faces + have, which will be used to construct a visualization of this attribute + in the resulting VisualizationSet. A list of text can also be passed and + a separate VisualizationData will be added to the AnalysisGeometry that ' + represents the attribute in the resulting VisualizationSet (or a separate ' + ContextGeometry layer if text_attr is True). Face attributes input + here can have . that separates the nested attributes from one another. + For example, properties.energy.construction. + text_attr: Boolean to note whether to note whether the input room_attr + and face_attr should be expressed as a colored AnalysisGeometry + or a ContextGeometry as text labels. + grid_display_mode: Text that dictates how the ContextGeometry for Model + SensorGrids should display in the resulting visualization. The Default + option will draw sensor points whenever there is no grid_data_path + and will not draw them at all when grid data is provided, assuming + the AnalysisGeometry of the grids is sufficient. Choose from: Default, + Points, Wireframe, Surface, SurfaceWithEdges, None. + hide_grid: Boolean to note whether the SensorGrid ContextGeometry should + be hidden or shown by default. + output_format: Text for the output format of the resulting VisualizationSet + File (.vsf). Choose from: vsf, json, pkl, vtkjs, html. Note that both + vsf and json refer to the the JSON version of the VisualizationSet + file and the distinction between the two is only for help in + coordinating file extensions (since both .vsf and .json can be + acceptable). Also note that ladybug-vtk must be installed in order + for the vtkjs or html options to be usable and the html format + refers to a web page with the vtkjs file embedded within it. + output_file: Optional file to output the string of the visualization + file contents. If None, the string will simply be returned from + this method. + """ + # load the model object and process simpler attributes + model_obj = Model.from_file(model_file) + room_attrs = [room_attr] if isinstance(room_attr, str) else room_attr + face_attrs = [face_attr] if isinstance(face_attr, str) else face_attr + wireframe = not exclude_wireframe + mesh = not faces + color_attr = not text_attr + + # load the room and face attributes + face_attributes = [] + for fa in face_attrs: + faa = FaceAttribute(name=fa, attrs=[fa], color=color_attr, text=text_attr) + face_attributes.append(faa) + room_attributes = [] + for ra in room_attrs: + raa = RoomAttribute(name=ra, attrs=[ra], color=color_attr, text=text_attr) + room_attributes.append(raa) + + # create the VisualizationSet + multiplier = not full_geometry + vis_set = model_obj.to_vis_set( + multiplier, ceil_adjacency, color_by=color_by, include_wireframe=wireframe, + use_mesh=mesh, hide_color_by=hide_color_by, room_attrs=room_attributes, + face_attrs=face_attributes, grid_display_mode=grid_display_mode, + hide_grid=hide_grid) + + # output the visualization in the correct format + output_format = output_format.lower() + if output_format in ('vsf', 'json'): + if output_file is None: + return json.dumps(vis_set.to_dict()) + elif isinstance(output_file, str): + with open(output_file, 'w') as of: + of.write(json.dumps(vis_set.to_dict())) + else: output_file.write(json.dumps(vis_set.to_dict())) - elif output_format == 'pkl': - if output_file.name != '': - out_folder, out_file = os.path.split(output_file.name) - vis_set.to_pkl(out_file, out_folder) - else: + elif output_format == 'pkl': + if output_file is None: + return pickle.dumps(vis_set.to_dict()) + elif isinstance(output_file, str): + with open(output_file, 'w') as of: output_file.write(pickle.dumps(vis_set.to_dict())) - elif output_format in ('vtkjs', 'html'): - if output_file.name == '': # get a temporary file - out_file = str(uuid.uuid4())[:6] - out_folder = tempfile.gettempdir() - else: - out_folder, out_file = os.path.split(output_file.name) - if out_file.endswith('.vtkjs'): - out_file = out_file[:-6] - elif out_file.endswith('.html'): - out_file = out_file[:-5] - try: - if output_format == 'vtkjs': - vis_set.to_vtkjs(output_folder=out_folder, file_name=out_file) - if output_format == 'html': - vis_set.to_html(output_folder=out_folder, file_name=out_file) - except AttributeError as ae: - raise AttributeError( - 'Ladybug-vtk must be installed in order to use --output-format ' - 'vtkjs.\n{}'.format(ae)) - if output_file.name == '': # load file contents to stdout - out_file_ext = out_file + '.' + output_format - out_file_path = os.path.join(out_folder, out_file_ext) - if output_format == 'html': - with open(out_file_path, encoding='utf-8') as of: - f_contents = of.read() - else: # vtkjs can only be read as binary - with open(out_file_path, 'rb') as of: - f_contents = of.read() - b = base64.b64encode(f_contents) - f_contents = b.decode('utf-8') - output_file.write(f_contents) + elif output_file.name == '': + output_file.write(pickle.dumps(vis_set.to_dict())) else: - raise ValueError('Unrecognized output-format "{}".'.format(output_format)) - except Exception as e: - _logger.exception('Failed to translate Model to VisualizationSet.\n{}'.format(e)) - sys.exit(1) + out_folder, out_file = os.path.split(output_file.name) + vis_set.to_pkl(out_file, out_folder) + elif output_format in ('vtkjs', 'html'): + if output_file is None or (not isinstance(output_file, str) + and output_file.name == ''): + # get a temporary file + out_file = str(uuid.uuid4())[:6] + out_folder = tempfile.gettempdir() + else: + f_path = output_file if isinstance(output_file, str) else output_file.name + out_folder, out_file = os.path.split(f_path) + if out_file.endswith('.vtkjs'): + out_file = out_file[:-6] + elif out_file.endswith('.html'): + out_file = out_file[:-5] + try: + if output_format == 'vtkjs': + vis_set.to_vtkjs(output_folder=out_folder, file_name=out_file) + if output_format == 'html': + vis_set.to_html(output_folder=out_folder, file_name=out_file) + except AttributeError as ae: + raise AttributeError( + 'Ladybug-vtk must be installed in order to use --output-format ' + 'vtkjs.\n{}'.format(ae)) + if output_file is None or (not isinstance(output_file, str) + and output_file.name == ''): + # load file contents + out_file_ext = out_file + '.' + output_format + out_file_path = os.path.join(out_folder, out_file_ext) + if output_format == 'html': + with open(out_file_path, encoding='utf-8') as of: + f_contents = of.read() + else: # vtkjs can only be read as binary + with open(out_file_path, 'rb') as of: + f_contents = of.read() + b = base64.b64encode(f_contents) + f_contents = b.decode('utf-8') + if output_file is None: + return f_contents + output_file.write(f_contents) else: - sys.exit(0) + raise ValueError('Unrecognized output-format "{}".'.format(output_format)) # add display sub-group to dragonfly CLI diff --git a/dragonfly_display/model.py b/dragonfly_display/model.py index 99bf25b..ca48e59 100644 --- a/dragonfly_display/model.py +++ b/dragonfly_display/model.py @@ -6,7 +6,7 @@ def model_to_vis_set( model, use_multiplier=True, solve_ceiling_adjacencies=False, color_by='type', include_wireframe=True, use_mesh=True, hide_color_by=False, room_attrs=None, face_attrs=None, - grid_display_mode='Default', hide_grid=True): + grid_display_mode='Default', hide_grid=False): """Translate a Dragonfly Model to a VisualizationSet. Args: @@ -62,7 +62,7 @@ def model_to_vis_set( * None hide_grid: Boolean to note whether the SensorGrid ContextGeometry should be - hidden or shown by default. (Default: True). + hidden or shown by default. (Default: False). Returns: A VisualizationSet object that represents the model. diff --git a/requirements.txt b/requirements.txt index 4c00c9e..68faf59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ honeybee-display>=0.3.6 -dragonfly-core>=1.42.18 +dragonfly-core>=1.47.30 diff --git a/tests/cli_test.py b/tests/cli_test.py new file mode 100644 index 0000000..5e3cdff --- /dev/null +++ b/tests/cli_test.py @@ -0,0 +1,45 @@ +"""Test cli.""" +import os +import time +from click.testing import CliRunner + +from ladybug.commandutil import run_command_function +from dragonfly_display.cli import model_to_vis_set_cli, model_to_vis_set + + +def test_model_to_vis_set_cli(): + """Test the model-to-vis command as it runs in the CLI.""" + input_model = './tests/json/model_with_doors_skylights.dfjson' + output_vis = './tests/json/model_with_doors_skylights.html' + runner = CliRunner() + t0 = time.time() + cmd_args = [input_model, '--output-format', 'html', '--output-file', output_vis] + result = runner.invoke(model_to_vis_set_cli, cmd_args) + run_time = time.time() - t0 + assert result.exit_code == 0 + assert run_time < 10 + assert os.path.isfile(output_vis) + os.remove(output_vis) + + +def test_model_to_vis_set(): + """Test the model_to_vis_set function that runs within the CLI.""" + input_model = './tests/json/model_with_doors_skylights.dfjson' + cmd_args = [input_model] + cmd_options = {'--output-format': 'vtkjs'} + vtkjs_str = run_command_function(model_to_vis_set, cmd_args, cmd_options) + + assert isinstance(vtkjs_str, str) + assert len(vtkjs_str) > 1000 + + cmd_options = { + '--color-by': 'type', + '--output-format': 'html', + '--room-attr': 'display_name', + '--text-attr': '' + } + output_vis = './tests/json/model_with_doors_skylights.html' + cmd_options['--output-file'] = output_vis + run_command_function(model_to_vis_set, cmd_args, cmd_options) + assert os.path.isfile(output_vis) + os.remove(output_vis)