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

141 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-12 13:34 +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 >>> svg_adjust_dict = { 

44 >>> "2eyxpyOx95m90jmsXLOuR0": { 

45 >>> {"space_data": 

46 >>> "0Lt8gR_E9ESeGH5uY_g9e9": { 

47 >>> "color": "#FF0000", 

48 >>> "text": 'my_text' 

49 >>> }, 

50 >>> "17JZcMFrf5tOftUTidA0d3": { 

51 >>> "color": "#FF0000", 

52 >>> "text": 'my_text2' 

53 >>> }, 

54 >>> "2RSCzLOBz4FAK$_wE8VckM": { 

55 >>> "color": "#FF0000", 

56 >>> "text": 'my_text3' 

57 >>> }, 

58 >>> }, 

59 >>> } 

60 >>> } 

61 >>> create_svg_floor_plan_plot( 

62 >>> path_to_ifc_file, target_path, svg_adjust_dict) 

63 """ 

64 svg_path = convert_ifc_to_svg(ifc_file_class_inst, target_path) 

65 split_svg_by_storeys(svg_path) 

66 modify_svg_elements(svg_adjust_dict, target_path) 

67 combine_svgs_complete( 

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

69 

70 

71def convert_ifc_to_svg(ifc_file_instance: IfcFileClass, 

72 target_path: Path) -> Path: 

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

74 settings = ifcopenshell.geom.settings( 

75 INCLUDE_CURVES=True, 

76 EXCLUDE_SOLIDS_AND_SURFACES=False, 

77 APPLY_DEFAULT_MATERIALS=True, 

78 DISABLE_TRIANGULATION=True 

79 ) 

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

81 svg_target_path = target_path / svg_file_name 

82 

83 sr = ifcopenshell.geom.serializers.svg( 

84 str(svg_target_path), settings) 

85 

86 file = ifc_file_instance.file 

87 sr.setFile(file) 

88 sr.setSectionHeightsFromStoreys() 

89 

90 sr.setDrawDoorArcs(True) 

91 sr.setPrintSpaceAreas(True) 

92 # sr.setPrintSpaceNames(True) 

93 sr.setBoundingRectangle(1024., 576.) 

94 sr.setScale(1.5 / 100) 

95 # sr.setWithoutStoreys(True) 

96 # sr.setPolygonal(True) 

97 # sr.setUseNamespace(True) 

98 # sr.setAlwaysProject(True) 

99 # sr.setScale(1 / 200) 

100 # sr.setAutoElevation(False) 

101 # sr.setAutoSection(True) 

102 # sr.setPrintSpaceNames(False) 

103 # sr.setPrintSpaceAreas(False) 

104 # sr.setDrawDoorArcs(False) 

105 # sr.setNoCSS(True) 

106 sr.writeHeader() 

107 

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

109 settings, 

110 file, 

111 with_progress=True, 

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

113 "IfcMember", "IfcExternalSpatialElement", 

114 "IfcBuildingElementProxy"), 

115 num_threads=8 

116 ): 

117 sr.write(elem) 

118 

119 sr.finalize() 

120 

121 return svg_target_path 

122 

123 

124def split_svg_by_storeys(svg: Path): 

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

126 with open(svg) as svg_file: 

127 svg_data = svg_file.read() 

128 

129 file_dir = svg.parent 

130 

131 # Define namespaces 

132 namespaces = { 

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

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

135 } 

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

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

138 

139 # Create SVG ElementTree 

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

141 

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

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

144 

145 # Find all 'IfcBuildingStorey' elements 

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

147 namespaces) 

148 

149 for building_storey in all_storeys: 

150 # Make a deep copy of the entire tree 

151 tree_story = copy.deepcopy(tree) 

152 root = tree_story.getroot() 

153 

154 # Find the corresponding storey element in the copied tree 

155 copied_storeys = tree_story.findall( 

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

157 

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

159 for storey_to_rm in copied_storeys: 

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

161 "data-guid"): 

162 root.remove(storey_to_rm) 

163 

164 # Save the resulting SVG for the current storey 

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

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

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

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

169 

170 

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

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

173 

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

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

176 dictionary that holds the relevant data. 

177 

178 Args: 

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

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

181 floor plan. 

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

183 """ 

184 # namespace 

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

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

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

188 

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

190 spaces_data = storey_data["space_data"] 

191 # get file path for SVG file 

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

193 tree = ET.parse(file_path) 

194 root = tree.getroot() 

195 

196 # reset opacity to 0.7 for better colors 

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

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

199 if style_element is not None: 

200 # Get the text content of the style element 

201 style_content = style_element.text 

202 

203 # Replace the desired style content 

204 style_content = style_content.replace( 

205 'fill-opacity: .2;', 

206 'fill-opacity: 0.7;') 

207 

208 # Update the text content of the style element 

209 style_element.text = style_content 

210 all_space_text_elements = root.findall( 

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

212 namespaces=ns) 

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

214 color = adjust_data['color'] 

215 text = adjust_data['text'] 

216 path_elements = root.findall( 

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

218 namespaces=ns) 

219 if path_elements is not None: 

220 for path_element in path_elements: 

221 if path_element is not None: 

222 path_element.set( 

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

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

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

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

227 text_elements = root.findall( 

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

229 namespaces=ns) 

230 if text_elements is not None: 

231 for text_element in text_elements: 

232 all_space_text_elements.remove(text_element) 

233 if text_element is not None: 

234 att = text_element.attrib 

235 text_element.clear() 

236 tspan_element = ET.SubElement( 

237 text_element, "tspan") 

238 style = tspan_element.get('style') 

239 if style: 

240 style += ";fill:#FFFFFF" 

241 else: 

242 style = "fill:#FFFFFF" 

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

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

245 tspan_element.set('style', style) 

246 tspan_element.text = text 

247 text_element.attrib = att 

248 

249 # for spaces without data add a placeholder 

250 for text_element in all_space_text_elements: 

251 if text_element is not None: 

252 att = text_element.attrib 

253 text_element.clear() 

254 tspan_element = ET.SubElement( 

255 text_element, "tspan") 

256 style = tspan_element.get('style') 

257 if style: 

258 style += ";fill:#FFFFFF" 

259 else: 

260 style = "fill:#FFFFFF" 

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

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

263 tspan_element.set('style', style) 

264 tspan_element.text = "-" 

265 text_element.attrib = att 

266 

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

268 

269 

270def combine_two_svgs( 

271 main_svg_path: Path, color_svg_path: Path, output_svg_path: Path): 

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

273 

274 Args: 

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

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

277 output_svg_path: Path to the output SVG file. 

278 

279 Returns: 

280 str: Combined SVG content as a string. 

281 """ 

282 from reportlab.graphics import renderSVG 

283 from reportlab.graphics.shapes import Drawing, Group 

284 from svglib.svglib import svg2rlg 

285 # Load the main SVG file 

286 main_svg = svg2rlg(main_svg_path) 

287 

288 # Load the color mapping SVG file 

289 color_svg = svg2rlg(color_svg_path) 

290 

291 # Get the dimensions of the main SVG 

292 main_width = main_svg.width 

293 main_height = main_svg.height 

294 

295 # Get the dimensions of the color mapping SVG 

296 color_width = color_svg.width 

297 color_height = color_svg.height 

298 

299 # Calculate the position to place the color mapping SVG 

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

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

302 

303 # Create a new drawing with the combined width 

304 combined_width = main_width + color_width + 10 

305 combined_height = max(main_height, color_height) 

306 drawing = Drawing(combined_width, combined_height) 

307 

308 # Add the main SVG to the drawing 

309 drawing.add(main_svg) 

310 

311 # Create a group to hold the color mapping SVG 

312 color_group = Group(color_svg) 

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

314 drawing.add(color_group) 

315 

316 # Save the combined SVG 

317 renderSVG.drawToFile(drawing, output_svg_path) 

318 

319 

320def combine_svgs_complete( 

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

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

323 for guid in storey_guids: 

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

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

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

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

328 combine_two_svgs(svg_file, color_mapping_file, output_svg_file) 

329 

330 # cleanup 

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

332 try: 

333 file.unlink() 

334 except FileNotFoundError: 

335 logger.warning( 

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

337 f"couldn't be removed.") 

338 except OSError as e: 

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