# ********************************************************************************* # URBANopt™, Copyright (c) 2019-2022, Alliance for Sustainable Energy, LLC, and other # contributors. All rights reserved. # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # Redistributions of source code must retain the above copyright notice, this list # of conditions and the following disclaimer. # Redistributions in binary form must reproduce the above copyright notice, this # list of conditions and the following disclaimer in the documentation and/or other # materials provided with the distribution. # Neither the name of the copyright holder nor the names of its contributors may be # used to endorse or promote products derived from this software without specific # prior written permission. # Redistribution of this software, without modification, must refer to the software # by the same designation. Redistribution of a modified version of this software # (i) may not refer to the modified version by the same designation, or by any # confusingly similar designation, and (ii) must refer to the underlying software # originally provided by Alliance as “URBANopt”. Except to comply with the foregoing, # the term “URBANopt”, or any confusingly similar designation may not be used to # refer to any modified version of this software or any modified version of the # underlying software originally provided by Alliance without the prior written # consent of Alliance. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED # OF THE POSSIBILITY OF SUCH DAMAGE. # ********************************************************************************* require 'urbanopt/geojson/feature' module URBANopt module GeoJSON class Building < URBANopt::GeoJSON::Feature ## # Used to initialize the feature. This method is inherited from the Feature class. def initialize(feature = {}) super(feature) @id = feature[:properties][:id] @name = feature[:properties][:name] @detailed_model_filename = feature[:properties][:detailed_model_filename] @floor_area = feature[:properties][:floor_area] @number_of_stories = feature[:properties][:number_of_stories] @number_of_stories_above_ground = feature[:properties][:number_of_stories_above_ground] @footprint_area = feature[:properties][:footprint_area] @template = feature[:properties][:template] @building_type = feature[:properties][:building_type] @system_type = feature[:properties][:system_type] @weekday_start_time = feature[:properties][:weekday_start_time] @weekday_duration = feature[:properties][:weekday_duration] @weekend_start_time = feature[:properties][:weekend_start_time] @weekend_duration = feature[:properties][:weekend_duration] @mixed_type_1 = feature[:properties][:mixed_type_1] @mixed_type_1_percentage = feature[:properties][:mixed_type_1_percentage] @mixed_type_2 = feature[:properties][:mixed_type_2] @mixed_type_2_percentage = feature[:properties][:mixed_type_2_percentage] @mixed_type_3 = feature[:properties][:mixed_type_3] @mixed_type_3_percentage = feature[:properties][:mixed_type_3_percentage] @mixed_type_4 = feature[:properties][:mixed_type_4] @mixed_type_4_percentage = feature[:properties][:mixed_type_4_percentage] end ## # Used to describe the Building feature type using the base method from the Feature class. def feature_type 'Building' end ## # Returns the building_properties schema. def schema_file return File.join(File.dirname(__FILE__), 'schema', 'building_properties.json') end ## # This method creates a building for a given feature specified in the # feature_json as per the create_method. # # Returns an array of instances of +OpenStudio::Model::Space+ . # # [Parameters] # * +create_method+ - _Type:Symbol_ - +:space_per_floor+ or +:space_per_building+ methods can be # used. # * +model+ - _Type:String_ - An instance of +OpenStudio::Model::Model+_ . # * +origin_lat_lon+ - _Type:Float_ - An instance of +OpenStudio::PointLatLon+ indicating the latitude and longitude of the origin. # * +runner+ - _Type:String_ - An instance of +OpenStudio::Measure::OSRunner+ for the measure run. # * +zoning+ - _Type:Boolean_ - Value is +true+ if utilizing detailed zoning, else # +false+ Zoning is set to False by default. # * +scaled_footprint_area+ - Used to scale the footprint area using the floor area. 0 by # default (no scaling). # * +other_building+ - _Type:URBANopt::GeoJSON::Feature - Optional, allow the user to pass in a different building to process. This is used for creating the other buildings for shading. def create_building(create_method, model, origin_lat_lon, runner, zoning = false, scaled_footprint_area = 0, other_building = @feature_json) number_of_stories = other_building[:properties][:number_of_stories] number_of_stories_above_ground = other_building[:properties][:number_of_stories_above_ground] number_of_stories_below_ground = other_building[:properties][:number_of_stories_below_ground] number_of_residential_units = other_building[:properties][:number_of_residential_units] if zoning surface_elevation = other_building[:properties][:surface_elevation] roof_elevation = other_building[:properties][:roof_elevation] floor_to_floor_height = other_building[:properties][:floor_to_floor_height] else maximum_roof_height = other_building[:properties][:maximum_roof_height] end if number_of_stories_above_ground.nil? number_of_stories_above_ground = number_of_stories number_of_stories_below_ground = 0 else number_of_stories_below_ground = number_of_stories - number_of_stories_above_ground end floor_to_floor_height = zoning ? 3.6 : 3 if number_of_stories_above_ground && number_of_stories_above_ground > 0 && maximum_roof_height && !zoning floor_to_floor_height = maximum_roof_height / number_of_stories_above_ground floor_to_floor_height = OpenStudio.convert(floor_to_floor_height, 'ft', 'm').get end if create_method == :space_per_floor || create_method == :spaces_per_floor if number_of_residential_units model.getBuilding.setStandardsNumberOfLivingUnits(number_of_residential_units) end model.getBuilding.setStandardsNumberOfStories(number_of_stories) model.getBuilding.setStandardsNumberOfAboveGroundStories(number_of_stories_above_ground) model.getBuilding.setNominalFloortoFloorHeight(floor_to_floor_height) end spaces = [] if create_method == :space_per_floor || create_method == :spaces_per_floor (-number_of_stories_below_ground + 1..number_of_stories_above_ground).each do |story_number| new_spaces = create_space_per_floor(story_number, floor_to_floor_height, model, origin_lat_lon, runner, zoning, scaled_footprint_area) spaces.concat(new_spaces) end elsif create_method == :space_per_building spaces = create_space_per_building(-number_of_stories_below_ground * floor_to_floor_height, number_of_stories_above_ground * floor_to_floor_height, model, origin_lat_lon, runner, zoning, other_building) end return spaces end alias create_other_building create_building ## # Return the features multi polygon in an array of the form coordinate pairs in double nested Array. # # [Parameters] # * +origin_lat_lon+ - _Type:Float_ - An instance of +OpenStudio::PointLatLon+ indicating the latitude and longitude of the origin. # * +runner+ - _Type:String_ - An instance of +Openstudio::Measure::OSRunner+ for the measure run. # * +zoning+ - _Type:Boolean_ - Should be true if you'd like to utilize aspects of function that are specific to zoning. def feature_points(origin_lat_lon, runner, zoning) feature_points = [] multi_polygons = get_multi_polygons(@feature_json) multi_polygons.each do |multi_polygon| multi_polygon.each do |polygon| elevation = 0 floor_print = URBANopt::GeoJSON::Helper.floor_print_from_polygon(polygon, elevation, origin_lat_lon, runner, zoning) floor_print.each do |point| feature_points << point end # Subsequent polygons are holes, and are not supported. break end end return feature_points end ## # Return the points of the other building object. This method is similar to feature_points, but accepts # the other building and other height. # # [Parameters] # * +other_building+ - _Type:Array_ - Array of points. # * +other_height+ - _Type:Double_ - Value of the other height from which to create the floor prints. # * +origin_lat_lon+ - _Type:Float_ - An instance of +OpenStudio::PointLatLon+ indicating the latitude and longitude of the origin. # * +runner+ - _Type:String_ - An instance of +Openstudio::Measure::OSRunner+ for the measure run. # * +zoning+ - _Type:Boolean_ - Should be true if you'd like to utilize aspects of function that are specific to zoning. def other_points(other_building, other_height, origin_lat_lon, runner, zoning) other_points = [] multi_polygons = get_multi_polygons(other_building) multi_polygons.each do |multi_polygon| multi_polygon.each do |polygon| floor_print = URBANopt::GeoJSON::Helper.floor_print_from_polygon(polygon, other_height, origin_lat_lon, runner, zoning) floor_print.each do |point| other_points << point end # Subsequent polygons are holes, and are not supported. break end end return other_points end ## # This method is used to create the surrounding buildings as shading objects. # # Returns an array of instances of +OpenStudio::Model::Space+. # # [Parameters] # * +other_building_type+ - _Type:String_ - Describes the surrounding buildings. Supports 'None', 'ShadingOnly' options. # * +other_buildings+ - _Type:URBANopt::GeoJSON::FeatureCollection_ - List of all surrounding features to include (self will be ignored if present in list). # * +model+ - _Type:OpenStudio::Model::Model_ - An instance of an OpenStudio Model. # * +origin_lat_lon+ - _Type:Float_ - An instance of +OpenStudio::PointLatLon+ indicating the latitude and longitude of the origin. # * +runner+ - _Type:String_ - An instance of +Openstudio::Measure::OSRunner+ for the measure run. # * +zoning+ - _Type:Boolean_ - Value is +True+ if utilizing detailed zoning, else +False+. Zoning is set to False by default. def create_other_buildings(other_building_type, other_buildings, model, origin_lat_lon, runner, zoning = false) building_features = {} building_features[:features] = [] if other_buildings[:features].nil? runner.registerWarning("No features found in #{other_buildings}") return [] else # remove non-buildings from the other_buildings list of all project features # since this is for shading, keep District Systems as well other_buildings[:features].each do |f| if f[:properties][:type] == 'Building' || f[:properties][:type] == 'District System' building_features[:features] << f end end end other_spaces = URBANopt::GeoJSON::Helper.process_other_buildings( self, other_building_type, building_features, model, origin_lat_lon, runner, zoning ) return other_spaces end ## # This method loops through all the spaces of the building and for each Wall # surface with Outdoors boundary condition, sets the window to wall ratio as per # the specified value or as a default value of 0.3. # # Returns an array of instances of +OpenStudio::Model::Space+ with windows. # # [Parameters] # * +spaces+ - _Type:Array_ - Contains instances of +OpenStudio::Model::Space+ . def create_windows(spaces) window_to_wall_ratio = @feature_json[:properties][:window_to_wall_ratio] if window_to_wall_ratio.nil? window_to_wall_ratio = 0.3 end spaces.each do |space| space.surfaces.each do |surface| if surface.surfaceType == 'Wall' && surface.outsideBoundaryCondition == 'Outdoors' surface.setWindowToWallRatio(window_to_wall_ratio) end end end end ## # Used to calculate the perimeter from the floor polygon of a Feature. Returns the perimeter # value. # # [Parameters] # * +feature+ - An instance of URBANopt::GeoJSON::Feature # def calculate_perimeter(feature) model = OpenStudio::Model::Model.new runner = OpenStudio::Measure::OSRunner.new(OpenStudio::WorkflowJSON.new) origin_lat_lon = nil origin_lat_lon = feature.create_origin_lat_lon(runner) spaces = feature.create_building(:space_per_building, model, origin_lat_lon, runner) surfaces = spaces[0].surfaces ground_surface = nil surfaces.each do |surface| boundary_condition = surface.outsideBoundaryCondition if boundary_condition == 'Ground' ground_surface = surface end end vertices = ground_surface.vertices n = vertices.size perimeter = 0 for i in (0..n - 1) do i vertex_1 = nil vertex_2 = nil if i == n - 1 vertex_1 = vertices[n - 1] vertex_2 = vertices[0] else vertex_1 = vertices[i] vertex_2 = vertices[i + 1] end length = OpenStudio::Vector3d.new(vertex_2 - vertex_1).length perimeter += length end perimeter = OpenStudio.convert(perimeter, 'm', 'ft').get perimeter = perimeter.round(4) return perimeter end ## # Convert to a Hash equivalent for JSON serialization. # Excludes attributes with nil value. ## def to_hash result = {} result[:id] = @id if @id result[:name] = @name if @name result[:detailed_model_filename] = @detailed_model_filename if @detailed_model_filename result[:floor_area] = @floor_area if @floor_area result[:number_of_stories] = @number_of_stories if @number_of_stories result[:number_of_stories_above_ground] = @number_of_stories_above_ground if @number_of_stories_above_ground result[:footprint_area] = @footprint_area if @footprint_area result[:template] = @template if @template result[:building_type] = @building_type if @building_type result[:system_type] = @system_type if @system_type result[:weekday_start_time] = @weekday_start_time if @weekday_start_time result[:weekday_duration] = @weekday_duration if @weekday_duration result[:weekend_start_time] = @weekend_start_time if @weekend_start_time result[:weekend_duration] = @weekend_duration if @weekend_duration result[:mixed_type_1] = @mixed_type_1 if @mixed_type_1 result[:mixed_type_1_percentage] = @mixed_type_1_percentage if @mixed_type_1_percentage result[:mixed_type_2] = @mixed_type_2 if @mixed_type_2 result[:mixed_type_2_percentage] = @mixed_type_2_percentage if @mixed_type_2_percentage result[:mixed_type_3] = @mixed_type_3 if @mixed_type_3 result[:mixed_type_3_percentage] = @mixed_type_3_percentage if @mixed_type_3_percentage result[:mixed_type_4] = @mixed_type_4 if @mixed_type_4 result[:mixed_type_4_percentage] = @mixed_type_4_percentage if @mixed_type_4_percentage return result end private ## # Returns an array of instances of +OpenStudio::Model::Space+ per building. # # [Parameters] # * +min_elevation+ - _Type:Integer_ - Indicates minimum elevation across all buildings. # * +max_elevation+ - _Type:Integer_ - Indicates maximum elevation across all buildings. # * +model+ - _Type:String_ - An instance of +OpenStudio::Model::Model+ . # * +origin_lat_lon+ - _Type:Float_ - An instance of +OpenStudio::PointLatLon+ indicating the latidude and longitude of the origin. # * +runner+ - _Type:String_ - An instance of +Openstudio::Measure::OSRunner+ for the measure run. # * +zoning+ - _Type:Boolean_ - Value is +true+ if utilizing detailed zoning, else # +false+. Zoning is set to False by default. # rubocop:disable Style/OptionalArguments, Style/CommentedKeyword def create_space_per_building(min_elevation, max_elevation, model, origin_lat_lon, runner, zoning = false, other_building) #:doc: # rubocop: enable Style/OptionalArguments, Style/CommentedKeyword if other_building geometry = other_building[:geometry] properties = other_building[:properties] else geometry = @feature_json[:geometry] properties = @feature_json[:properties] end if zoning name = properties[:id] else name = properties[:name] end floor_prints = [] multi_polygons = get_multi_polygons(other_building) multi_polygons.each do |multi_polygon| if multi_polygon.size > 1 runner.registerWarning('Ignoring holes in polygon') end multi_polygon.each do |polygon| floor_print = URBANopt::GeoJSON::Helper.floor_print_from_polygon(polygon, min_elevation, origin_lat_lon, runner, zoning) if floor_print floor_prints << floor_print else runner.registerWarning("Cannot get floor print for building '#{name}'") end break end end result = [] floor_prints.each do |floor_print| space = OpenStudio::Model::Space.fromFloorPrint(floor_print, max_elevation - min_elevation, model) if space.empty? runner.registerWarning('Cannot create building space') next end space = space.get space.setName("Building #{name} Space") thermal_zone = OpenStudio::Model::ThermalZone.new(model) thermal_zone.setName("Building #{name} ThermalZone") space.setThermalZone(thermal_zone) result << space end return result end ## # Returns an array of instances of +OpenStudio::Model::Space+ per floor. # # [Parameters] # * +feature+ - _Type:String_ - An instance of Feature class built off of the GeoJSON file. # * +story_number+ - _Type:Integer_ - Number of floors in the building. # * +floor_to_floor_height+ - _Type:Integer_ - Height of the building stories. # * +model+ - _Type:String_ - An instance of +OpenStudio::Model::Model+. # * +origin_lat_lon+ - _Type:Float_ - An instance of +OpenStudio::PointLatLon+ indicating the # origin's latitude and longitude. # * +runner+ - _Type:String_ - An instance of +Openstudio::Measure::OSRunner+ for the measure run. # * +zoning+ - _Type:Boolean_ - Value is +true+ if utilizing detailed zoning, else # +false+. Zoning is set to False by default. # rubocop:disable Style/CommentedKeyword def create_space_per_floor(story_number, floor_to_floor_height, model, origin_lat_lon, runner, zoning = false, scaled_footprint_area) #:doc: # rubocop:enable Style/CommentedKeyword begin if other_building geometry = other_building[:geometry] properties = other_building[:properties] else geometry = @feature_json[:geometry] properties = @feature_json[:properties] end rescue StandardError end floor_prints = [] multi_polygons = get_multi_polygons multi_polygons.each do |multi_polygon| if story_number == 1 && multi_polygon.size > 1 runner.registerWarning('Ignoring holes in polygon') end multi_polygon.each do |polygon| elevation = (story_number - 1) * floor_to_floor_height floor_print = URBANopt::GeoJSON::Helper.floor_print_from_polygon(polygon, elevation, origin_lat_lon, runner, zoning, scaled_footprint_area) if floor_print if zoning this_floor_prints = URBANopt::GeoJSON::Zoning.divide_floor_print(floor_print, 4.0, runner) floor_prints.concat(this_floor_prints) else floor_prints << floor_print end else runner.registerWarning("Cannot create story #{story_number}") end # Subsequent polygons are holes, and are not supported. break end end spaces = [] floor_prints.each do |floor_print| space = OpenStudio::Model::Space.fromFloorPrint(floor_print, floor_to_floor_height, model) if space.empty? runner.registerWarning("Cannot create space for story #{story_number}") next end space = space.get space.setName("Building Story #{story_number} Space") space.surfaces.each do |surface| if surface.surfaceType == 'Wall' if story_number < 1 surface.setOutsideBoundaryCondition('Ground') end end end spaces << space end building_story = OpenStudio::Model::BuildingStory.new(model) building_story.setName("Building Story #{story_number}") building_story.setNominalZCoordinate(story_number * floor_to_floor_height) building_story.setNominalFloortoFloorHeight(floor_to_floor_height) spaces.each do |space| space.setBuildingStory(building_story) thermal_zone = OpenStudio::Model::ThermalZone.new(model) thermal_zone.setName("Building Story #{story_number} ThermalZone") space.setThermalZone(thermal_zone) end return spaces end end end end