Coverage for bim2sim/utilities/svg_utils.py: 11%

141 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 17:09 +0000

1import copy 

2import logging 

3import xml.etree.ElementTree as ET 

4from pathlib import Path 

5 

6import ifcopenshell.geom 

7 

8from bim2sim.kernel.ifc_file import IfcFileClass 

9 

10est_time = 10 

11aggregate_model = True 

12logger = logging.getLogger(__name__) 

13 

14 

15def create_svg_floor_plan_plot( 

16 ifc_file_class_inst: IfcFileClass, 

17 target_path: Path, 

18 svg_adjust_dict: dict, 

19 result_str: str): 

20 """Creates an SVG floor plan plot for every storey and adjust its design. 

21 

22 This function first creates an SVG floor plan for the provided IFC file 

23 based on IfcConvert, then it splits the SVG floor plan into one file per 

24 storey. In the last step the floor plans for each storey can be adjusted 

25 regarding their background color and the text. This is useful to create a 

26 heatmap that e.g. shows the highest temperature in the specific room 

27 and colorize the rooms based on the data. 

28 

29 Args: 

30 svg_adjust_dict: nexted dict that holds guid of storey, spaces and the 

31 attributes for "color" and "text" to overwrite existing data in the 

32 floor plan. See example for more information 

33 ifc_file_class_inst: bim2sim IfcFileClass instance 

34 target_path: Path to store the SVG files 

35 result_str (str): name of the results plotted (used for file naming) 

36 

37 Example: 

38 # create nested dict, where "2eyxpyOx95m90jmsXLOuR0" is the storey guid 

39 # and "0Lt8gR_E9ESeGH5uY_g9e9", "17JZcMFrf5tOftUTidA0d3" and 

40 path_to_ifc_file = Path("my_path_to_ifc_folder/AC20-FZK-Haus.ifc") 

41 ifc_file_instance = ifcopenshell.open(path_to_ifc_file) 

42 target_path = Path("my_target_path") 

43 

44 svg_adjust_dict = { 

45 "2eyxpyOx95m90jmsXLOuR0": { 

46 {"space_data": 

47 "0Lt8gR_E9ESeGH5uY_g9e9": { 

48 "color": "#FF0000", 

49 "text": 'my_text' 

50 }, 

51 "17JZcMFrf5tOftUTidA0d3": { 

52 "color": "#FF0000", 

53 "text": 'my_text2' 

54 }, 

55 "2RSCzLOBz4FAK$_wE8VckM": { 

56 "color": "#FF0000", 

57 "text": 'my_text3' 

58 }, 

59 }, 

60 } 

61 } 

62 create_svg_floor_plan_plot( 

63 path_to_ifc_file, target_path, svg_adjust_dict) 

64 """ 

65 svg_path = convert_ifc_to_svg(ifc_file_class_inst, target_path) 

66 split_svg_by_storeys(svg_path) 

67 modify_svg_elements(svg_adjust_dict, target_path) 

68 combine_svgs_complete( 

69 target_path, list(svg_adjust_dict.keys()), result_str) 

70 

71 

72def convert_ifc_to_svg(ifc_file_instance: IfcFileClass, 

73 target_path: Path) -> Path: 

74 """Create an SVG floor plan based on the given IFC file using IfcConvert""" 

75 settings = ifcopenshell.geom.settings( 

76 INCLUDE_CURVES=True, 

77 EXCLUDE_SOLIDS_AND_SURFACES=False, 

78 APPLY_DEFAULT_MATERIALS=True, 

79 DISABLE_TRIANGULATION=True 

80 ) 

81 svg_file_name = ifc_file_instance.ifc_file_name[:-4] + '.svg' 

82 svg_target_path = target_path / svg_file_name 

83 

84 sr = ifcopenshell.geom.serializers.svg( 

85 str(svg_target_path), settings) 

86 

87 file = ifc_file_instance.file 

88 sr.setFile(file) 

89 sr.setSectionHeightsFromStoreys() 

90 

91 sr.setDrawDoorArcs(True) 

92 sr.setPrintSpaceAreas(True) 

93 # sr.setPrintSpaceNames(True) 

94 sr.setBoundingRectangle(1024., 576.) 

95 sr.setScale(1.5 / 100) 

96 # sr.setWithoutStoreys(True) 

97 # sr.setPolygonal(True) 

98 # sr.setUseNamespace(True) 

99 # sr.setAlwaysProject(True) 

100 # sr.setScale(1 / 200) 

101 # sr.setAutoElevation(False) 

102 # sr.setAutoSection(True) 

103 # sr.setPrintSpaceNames(False) 

104 # sr.setPrintSpaceAreas(False) 

105 # sr.setDrawDoorArcs(False) 

106 # sr.setNoCSS(True) 

107 sr.writeHeader() 

108 

109 for progress, elem in ifcopenshell.geom.iterate( 

110 settings, 

111 file, 

112 with_progress=True, 

113 exclude=("IfcOpeningElement", "IfcStair", "IfcSite", "IfcSlab", 

114 "IfcMember", "IfcExternalSpatialElement", 

115 "IfcBuildingElementProxy"), 

116 num_threads=8 

117 ): 

118 sr.write(elem) 

119 

120 sr.finalize() 

121 

122 return svg_target_path 

123 

124 

125def split_svg_by_storeys(svg: Path): 

126 """Splits the SVG of one building into single SVGs for each storey.""" 

127 with open(svg) as svg_file: 

128 svg_data = svg_file.read() 

129 

130 file_dir = svg.parent 

131 

132 # Define namespaces 

133 namespaces = { 

134 "svg": "http://www.w3.org/2000/svg", 

135 "xlink": "http://www.w3.org/1999/xlink" 

136 } 

137 ET.register_namespace("", "http://www.w3.org/2000/svg") 

138 ET.register_namespace("xlink", "http://www.w3.org/1999/xlink") 

139 

140 # Create SVG ElementTree 

141 tree = ET.ElementTree(ET.fromstring(svg_data)) 

142 

143 # Extract the <style> element from the original SVG 

144 style_element = tree.find(".//svg:style", namespaces) 

145 

146 # Find all 'IfcBuildingStorey' elements 

147 all_storeys = tree.findall(".//svg:g[@class='IfcBuildingStorey']", 

148 namespaces) 

149 

150 for building_storey in all_storeys: 

151 # Make a deep copy of the entire tree 

152 tree_story = copy.deepcopy(tree) 

153 root = tree_story.getroot() 

154 

155 # Find the corresponding storey element in the copied tree 

156 copied_storeys = tree_story.findall( 

157 ".//svg:g[@class='IfcBuildingStorey']", namespaces) 

158 

159 # Remove all other storeys except the one we want to keep 

160 for storey_to_rm in copied_storeys: 

161 if storey_to_rm.get("data-guid") != building_storey.get( 

162 "data-guid"): 

163 root.remove(storey_to_rm) 

164 

165 # Save the resulting SVG for the current storey 

166 storey_guid = building_storey.get("data-guid") 

167 with open(f"{file_dir}/{storey_guid}.svg", "wb") as f: 

168 # Use a custom Serializer, to prevent 'ns0'-prefix 

169 tree_story.write(f, encoding="utf-8", xml_declaration=True) 

170 

171 

172def modify_svg_elements(svg_adjust_dict: dict, path: Path): 

173 """Adjusts SVG floor plan for based on input data. 

174 

175 Based on the inputs, you can colorize the different spaces in the SVG 

176 and/or add text to the space for each storey. The input is a nested 

177 dictionary that holds the relevant data. 

178 

179 Args: 

180 svg_adjust_dict: nexted dict that holds guid of storey, spaces and the 

181 attributes for "color" and "text" to overwrite existing data in the 

182 floor plan. 

183 path: Path where the basic SVG files are stored. 

184 """ 

185 # namespace 

186 ET.register_namespace("", "http://www.w3.org/2000/svg") 

187 ET.register_namespace("xlink", "http://www.w3.org/1999/xlink") 

188 ns = {'svg': 'http://www.w3.org/2000/svg'} 

189 

190 for storey_guid, storey_data in svg_adjust_dict.items(): 

191 spaces_data = storey_data["space_data"] 

192 # get file path for SVG file 

193 file_path = Path(f"{path}/{storey_guid}.svg") 

194 tree = ET.parse(file_path) 

195 root = tree.getroot() 

196 

197 # reset opacity to 0.7 for better colors 

198 namespace = {'svg': 'http://www.w3.org/2000/svg'} 

199 style_element = root.find('.//svg:style', namespace) 

200 if style_element is not None: 

201 # Get the text content of the style element 

202 style_content = style_element.text 

203 

204 # Replace the desired style content 

205 style_content = style_content.replace( 

206 'fill-opacity: .2;', 

207 'fill-opacity: 0.7;') 

208 

209 # Update the text content of the style element 

210 style_element.text = style_content 

211 all_space_text_elements = root.findall( 

212 f'.//svg:g[@class="IfcSpace"]/svg:text', 

213 namespaces=ns) 

214 for space_guid, adjust_data in spaces_data.items(): 

215 color = adjust_data['color'] 

216 text = adjust_data['text'] 

217 path_elements = root.findall( 

218 f".//svg:g[@data-guid='{space_guid}']/svg:path", 

219 namespaces=ns) 

220 if path_elements is not None: 

221 for path_element in path_elements: 

222 if path_element is not None: 

223 path_element.set( 

224 'style', f'fill: {color};') 

225 # TODO set spacearea and space name to false in convert, store 

226 # short space name in tz mapping and then in svg dict, add instead 

227 # replace the string of zone name \n consumption here 

228 text_elements = root.findall( 

229 f".//svg:g[@data-guid='{space_guid}']/svg:text", 

230 namespaces=ns) 

231 if text_elements is not None: 

232 for text_element in text_elements: 

233 all_space_text_elements.remove(text_element) 

234 if text_element is not None: 

235 att = text_element.attrib 

236 text_element.clear() 

237 tspan_element = ET.SubElement( 

238 text_element, "tspan") 

239 style = tspan_element.get('style') 

240 if style: 

241 style += ";fill:#FFFFFF" 

242 else: 

243 style = "fill:#FFFFFF" 

244 style += ";font-weight:bold" 

245 style += ";font-size:22px" 

246 tspan_element.set('style', style) 

247 tspan_element.text = text 

248 text_element.attrib = att 

249 

250 # for spaces without data add a placeholder 

251 for text_element in all_space_text_elements: 

252 if text_element is not None: 

253 att = text_element.attrib 

254 text_element.clear() 

255 tspan_element = ET.SubElement( 

256 text_element, "tspan") 

257 style = tspan_element.get('style') 

258 if style: 

259 style += ";fill:#FFFFFF" 

260 else: 

261 style = "fill:#FFFFFF" 

262 style += ";font-weight:bold" 

263 style += ";font-size:22px" 

264 tspan_element.set('style', style) 

265 tspan_element.text = "-" 

266 text_element.attrib = att 

267 

268 tree.write(Path(f"{path}/{storey_guid}_modified.svg")) 

269 

270 

271def combine_two_svgs( 

272 main_svg_path: Path, color_svg_path: Path, output_svg_path: Path): 

273 """Combines the content of a child SVG file into a parent SVG file. 

274 

275 Args: 

276 main_svg_path (Path): Path to the parent SVG file. 

277 color_svg_path (Path): Path to the child SVG file. 

278 output_svg_path: Path to the output SVG file. 

279 

280 Returns: 

281 str: Combined SVG content as a string. 

282 """ 

283 from reportlab.graphics import renderSVG 

284 from reportlab.graphics.shapes import Drawing, Group 

285 from svglib.svglib import svg2rlg 

286 # Load the main SVG file 

287 main_svg = svg2rlg(main_svg_path) 

288 

289 # Load the color mapping SVG file 

290 color_svg = svg2rlg(color_svg_path) 

291 

292 # Get the dimensions of the main SVG 

293 main_width = main_svg.width 

294 main_height = main_svg.height 

295 

296 # Get the dimensions of the color mapping SVG 

297 color_width = color_svg.width 

298 color_height = color_svg.height 

299 

300 # Calculate the position to place the color mapping SVG 

301 color_x = main_width + 10 # Add some spacing between the SVGs 

302 color_y = (main_height - color_height) / 2 # Center vertically 

303 

304 # Create a new drawing with the combined width 

305 combined_width = main_width + color_width + 10 

306 combined_height = max(main_height, color_height) 

307 drawing = Drawing(combined_width, combined_height) 

308 

309 # Add the main SVG to the drawing 

310 drawing.add(main_svg) 

311 

312 # Create a group to hold the color mapping SVG 

313 color_group = Group(color_svg) 

314 color_group.translate(color_x, color_y) # Position the color mapping SVG 

315 drawing.add(color_group) 

316 

317 # Save the combined SVG 

318 renderSVG.drawToFile(drawing, output_svg_path) 

319 

320 

321def combine_svgs_complete( 

322 file_path: Path, storey_guids: list, result_str: str) -> None: 

323 """Add color mapping svg to floor plan svg.""" 

324 for guid in storey_guids: 

325 original_svg = file_path / f"{guid}.svg" 

326 svg_file = file_path / f"{guid}_modified.svg" 

327 color_mapping_file = file_path / f"color_mapping_{guid}.svg" 

328 output_svg_file = file_path / f"Floor_plan_{result_str}_{guid}.svg" 

329 combine_two_svgs(svg_file, color_mapping_file, output_svg_file) 

330 

331 # cleanup 

332 for file in [original_svg, svg_file, color_mapping_file]: 

333 try: 

334 file.unlink() 

335 except FileNotFoundError: 

336 logger.warning( 

337 f"{file.name} in path {file.parent} not found and thus " 

338 f"couldn't be removed.") 

339 except OSError as e: 

340 logger.warning(f"Error: {e.filename} - {e.strerror}")