1use std::collections::{HashMap, HashSet};
7
8use crate::csv_cell::{escape_csv_cell, CsvCellOptions};
9use crate::model::{build_export_model, fmt_num, EntityRow, ExportModel};
10
11#[derive(Clone, Copy, PartialEq, Eq)]
13pub enum CsvMode {
14 Entities,
16 Properties,
18 Quantities,
20 SpatialHierarchy,
22}
23
24pub struct CsvOptions {
26 pub delimiter: String,
28 pub include_properties: bool,
30}
31
32impl Default for CsvOptions {
33 fn default() -> Self {
34 Self { delimiter: ",".to_string(), include_properties: false }
35 }
36}
37
38fn escape(value: &str, delimiter: &str) -> String {
47 escape_csv_cell(
51 value,
52 &CsvCellOptions {
53 delimiter,
54 exempt_numbers: true,
55 quote_whitespace_padded: false,
56 },
57 )
58}
59
60fn join(values: &[String], delimiter: &str) -> String {
61 values.join(delimiter)
62}
63
64pub fn export_csv(content: &[u8], mode: CsvMode, opts: &CsvOptions) -> String {
66 let model = build_export_model(content);
67 match mode {
68 CsvMode::Entities => entities_csv(&model, opts),
69 CsvMode::Properties => properties_csv(&model, opts),
70 CsvMode::Quantities => quantities_csv(&model, opts),
71 CsvMode::SpatialHierarchy => spatial_csv(content, &model, opts),
72 }
73}
74
75fn spatial_csv(content: &[u8], model: &ExportModel, opts: &CsvOptions) -> String {
77 let d = &opts.delimiter;
78 let by_id: HashMap<u32, &EntityRow> = model.entities.iter().map(|e| (e.express_id, e)).collect();
79 let (children, project) = crate::ifc5::spatial_children(content);
80
81 let (mut proj_gid, mut proj_name) = (String::new(), String::new());
83 if let Some(pid) = project {
84 if let Some(e) = crate::ifc5::decode_one(content, pid) {
85 proj_gid = e.get(0).and_then(|a| a.as_string()).unwrap_or("").to_string();
86 proj_name = e.get(2).and_then(|a| a.as_string()).unwrap_or("").to_string();
87 }
88 }
89
90 let info = |id: u32| -> (String, String, String) {
91 if Some(id) == project {
92 (proj_gid.clone(), proj_name.clone(), "IfcProject".to_string())
93 } else if let Some(e) = by_id.get(&id) {
94 (
95 e.global_id.clone().unwrap_or_default(),
96 e.name.clone().unwrap_or_default(),
97 e.ifc_type.clone(),
98 )
99 } else {
100 (String::new(), String::new(), String::new())
101 }
102 };
103
104 let headers = ["expressId", "globalId", "name", "type", "parentId", "level"];
105 let mut lines = vec![join(&headers.iter().map(|h| escape(h, d)).collect::<Vec<_>>(), d)];
106
107 let mut visited = HashSet::new();
108 let mut stack: Vec<(u32, Option<u32>, usize)> = Vec::new();
109 if let Some(pid) = project {
110 stack.push((pid, None, 0));
111 }
112 while let Some((id, parent, level)) = stack.pop() {
113 if !visited.insert(id) {
114 continue;
115 }
116 let (gid, name, ty) = info(id);
117 let row = vec![
118 escape(&id.to_string(), d),
119 escape(&gid, d),
120 escape(&name, d),
121 escape(&ty, d),
122 escape(&parent.map(|p| p.to_string()).unwrap_or_default(), d),
123 escape(&level.to_string(), d),
124 ];
125 lines.push(join(&row, d));
126 if let Some(kids) = children.get(&id) {
127 for &k in kids.iter().rev() {
129 if !visited.contains(&k) {
130 stack.push((k, Some(id), level + 1));
131 }
132 }
133 }
134 }
135 lines.join("\n")
136}
137
138fn entities_csv(model: &ExportModel, opts: &CsvOptions) -> String {
139 let d = &opts.delimiter;
140 let mut headers: Vec<String> = ["expressId", "globalId", "name", "type", "description", "objectType", "hasGeometry"]
141 .iter()
142 .map(|s| s.to_string())
143 .collect();
144
145 let mut flat_cols: Vec<(String, String)> = Vec::new();
147 if opts.include_properties {
148 let mut seen = std::collections::HashSet::new();
149 for e in &model.entities {
150 for ps in &e.property_sets {
151 for p in &ps.properties {
152 let key = (ps.name.clone(), p.name.clone());
153 if seen.insert(key.clone()) {
154 flat_cols.push(key);
155 }
156 }
157 }
158 }
159 for (pset, prop) in &flat_cols {
160 headers.push(format!("{pset}_{prop}"));
161 }
162 }
163
164 let mut lines = Vec::with_capacity(model.entities.len() + 1);
165 lines.push(join(&headers.iter().map(|h| escape(h, d)).collect::<Vec<_>>(), d));
166
167 for e in &model.entities {
168 let mut row = vec![
169 escape(&e.express_id.to_string(), d),
170 escape(e.global_id.as_deref().unwrap_or(""), d),
171 escape(e.name.as_deref().unwrap_or(""), d),
172 escape(&e.ifc_type, d),
173 escape(e.description.as_deref().unwrap_or(""), d),
174 escape(e.object_type.as_deref().unwrap_or(""), d),
175 escape(if e.has_geometry { "true" } else { "false" }, d),
176 ];
177 if opts.include_properties {
178 for (pset, prop) in &flat_cols {
179 let v = e.lookup(pset, prop).unwrap_or_default();
180 row.push(escape(&v, d));
181 }
182 }
183 lines.push(join(&row, d));
184 }
185 lines.join("\n")
186}
187
188fn properties_csv(model: &ExportModel, opts: &CsvOptions) -> String {
189 let d = &opts.delimiter;
190 let headers = ["entityId", "globalId", "entityName", "entityType", "psetName", "propName", "value", "type"];
191 let mut lines = vec![join(&headers.iter().map(|h| escape(h, d)).collect::<Vec<_>>(), d)];
192
193 for e in &model.entities {
194 for ps in &e.property_sets {
195 for p in &ps.properties {
196 let row = vec![
197 escape(&e.express_id.to_string(), d),
198 escape(e.global_id.as_deref().unwrap_or(""), d),
199 escape(e.name.as_deref().unwrap_or(""), d),
200 escape(&e.ifc_type, d),
201 escape(&ps.name, d),
202 escape(&p.name, d),
203 escape(&p.value, d),
204 escape(&p.value_type, d),
205 ];
206 lines.push(join(&row, d));
207 }
208 }
209 }
210 lines.join("\n")
211}
212
213fn quantities_csv(model: &ExportModel, opts: &CsvOptions) -> String {
214 let d = &opts.delimiter;
215 let headers = ["entityId", "globalId", "entityName", "entityType", "qsetName", "quantityName", "value", "type"];
216 let mut lines = vec![join(&headers.iter().map(|h| escape(h, d)).collect::<Vec<_>>(), d)];
217
218 for e in &model.entities {
219 for qs in &e.quantity_sets {
220 for q in &qs.quantities {
221 let row = vec![
222 escape(&e.express_id.to_string(), d),
223 escape(e.global_id.as_deref().unwrap_or(""), d),
224 escape(e.name.as_deref().unwrap_or(""), d),
225 escape(&e.ifc_type, d),
226 escape(&qs.name, d),
227 escape(&q.name, d),
228 escape(&fmt_num(q.value), d),
229 escape(q.kind, d),
230 ];
231 lines.push(join(&row, d));
232 }
233 }
234 }
235 lines.join("\n")
236}
237
238#[cfg(test)]
239#[path = "csv_tests.rs"]
240mod tests;