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
« 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
6import ifcopenshell.geom
8from bim2sim.kernel.ifc_file import IfcFileClass
10est_time = 10
11aggregate_model = True
12logger = logging.getLogger(__name__)
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.
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.
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)
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")
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)
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
84 sr = ifcopenshell.geom.serializers.svg(
85 str(svg_target_path), settings)
87 file = ifc_file_instance.file
88 sr.setFile(file)
89 sr.setSectionHeightsFromStoreys()
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()
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)
120 sr.finalize()
122 return svg_target_path
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()
130 file_dir = svg.parent
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")
140 # Create SVG ElementTree
141 tree = ET.ElementTree(ET.fromstring(svg_data))
143 # Extract the <style> element from the original SVG
144 style_element = tree.find(".//svg:style", namespaces)
146 # Find all 'IfcBuildingStorey' elements
147 all_storeys = tree.findall(".//svg:g[@class='IfcBuildingStorey']",
148 namespaces)
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()
155 # Find the corresponding storey element in the copied tree
156 copied_storeys = tree_story.findall(
157 ".//svg:g[@class='IfcBuildingStorey']", namespaces)
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)
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)
172def modify_svg_elements(svg_adjust_dict: dict, path: Path):
173 """Adjusts SVG floor plan for based on input data.
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.
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'}
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()
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
204 # Replace the desired style content
205 style_content = style_content.replace(
206 'fill-opacity: .2;',
207 'fill-opacity: 0.7;')
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
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
268 tree.write(Path(f"{path}/{storey_guid}_modified.svg"))
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.
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.
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)
289 # Load the color mapping SVG file
290 color_svg = svg2rlg(color_svg_path)
292 # Get the dimensions of the main SVG
293 main_width = main_svg.width
294 main_height = main_svg.height
296 # Get the dimensions of the color mapping SVG
297 color_width = color_svg.width
298 color_height = color_svg.height
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
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)
309 # Add the main SVG to the drawing
310 drawing.add(main_svg)
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)
317 # Save the combined SVG
318 renderSVG.drawToFile(drawing, output_svg_path)
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)
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}")