1use ifc_model::{Entity, EntityId, Model, Transaction, Value};
26
27use crate::authoring::invalid;
28use crate::GeorefResult;
29
30#[derive(Debug, Clone, Copy, Default)]
32#[non_exhaustive]
33pub struct GeographicCrsDraft<'a> {
34 pub name: Option<&'a str>,
36 pub description: Option<&'a str>,
38 pub geodetic_datum: Option<&'a str>,
40 pub prime_meridian: Option<&'a str>,
42 pub angle_unit: Option<EntityId>,
44 pub height_unit: Option<EntityId>,
46}
47
48impl<'a> GeographicCrsDraft<'a> {
49 #[must_use]
51 pub fn new() -> Self {
52 Self::default()
53 }
54
55 #[must_use]
57 pub const fn name(mut self, value: &'a str) -> Self {
58 self.name = Some(value);
59 self
60 }
61
62 #[must_use]
64 pub const fn description(mut self, value: &'a str) -> Self {
65 self.description = Some(value);
66 self
67 }
68
69 #[must_use]
71 pub const fn geodetic_datum(mut self, value: &'a str) -> Self {
72 self.geodetic_datum = Some(value);
73 self
74 }
75
76 #[must_use]
78 pub const fn prime_meridian(mut self, value: &'a str) -> Self {
79 self.prime_meridian = Some(value);
80 self
81 }
82
83 #[must_use]
85 pub const fn angle_unit(mut self, value: EntityId) -> Self {
86 self.angle_unit = Some(value);
87 self
88 }
89
90 #[must_use]
92 pub const fn height_unit(mut self, value: EntityId) -> Self {
93 self.height_unit = Some(value);
94 self
95 }
96}
97
98pub fn create_geographic_crs(
108 tx: &mut Transaction,
109 draft: GeographicCrsDraft<'_>,
110) -> GeorefResult<EntityId> {
111 if let Some(name) = draft.name {
112 if name.trim().is_empty() {
113 return Err(invalid("IFCGEOGRAPHICCRS", "Name", name));
114 }
115 }
116
117 Ok(tx.create(Entity::new(
118 "IFCGEOGRAPHICCRS",
119 vec![
120 draft.name.map_or(Value::Null, |v| Value::Text(v.into())),
121 draft
122 .description
123 .map_or(Value::Null, |v| Value::Text(v.into())),
124 draft
125 .geodetic_datum
126 .map_or(Value::Null, |v| Value::Text(v.into())),
127 draft
128 .prime_meridian
129 .map_or(Value::Null, |v| Value::Text(v.into())),
130 draft.angle_unit.map_or(Value::Null, Value::Ref),
131 draft.height_unit.map_or(Value::Null, Value::Ref),
132 ],
133 )))
134}
135
136#[derive(Debug, Clone, Copy)]
138#[non_exhaustive]
139pub struct MapConversionDraft {
140 pub source_crs: EntityId,
142 pub target_crs: EntityId,
144 pub eastings: f64,
146 pub northings: f64,
148 pub orthogonal_height: f64,
150 pub x_axis: Option<(f64, f64)>,
155 pub scale: Option<f64>,
157}
158
159impl MapConversionDraft {
160 #[must_use]
163 pub const fn new(
164 source_crs: EntityId,
165 target_crs: EntityId,
166 eastings: f64,
167 northings: f64,
168 orthogonal_height: f64,
169 ) -> Self {
170 Self {
171 source_crs,
172 target_crs,
173 eastings,
174 northings,
175 orthogonal_height,
176 x_axis: None,
177 scale: None,
178 }
179 }
180
181 #[must_use]
184 pub const fn x_axis(mut self, value: (f64, f64)) -> Self {
185 self.x_axis = Some(value);
186 self
187 }
188
189 #[must_use]
191 pub const fn scale(mut self, value: f64) -> Self {
192 self.scale = Some(value);
193 self
194 }
195}
196
197fn resolved_type(tx: &Transaction, model: &Model, target: EntityId) -> Option<String> {
198 tx.edits()
199 .iter()
200 .rev()
201 .find_map(|edit| match edit {
202 ifc_model::Edit::Create { id, entity } if *id == target => {
203 Some(entity.type_name.as_ref().to_owned())
204 }
205 _ => None,
206 })
207 .or_else(|| model.get(target).map(|e| e.type_name.as_ref().to_owned()))
208}
209
210fn check_conversion(
211 entity: &'static str,
212 tx: &Transaction,
213 model: &Model,
214 draft: &MapConversionDraft,
215) -> GeorefResult<()> {
216 let finite = [
217 ("Eastings", draft.eastings),
218 ("Northings", draft.northings),
219 ("OrthogonalHeight", draft.orthogonal_height),
220 ];
221 for (attribute, value) in finite {
222 if !value.is_finite() {
223 return Err(invalid(entity, attribute, value.to_string()));
224 }
225 }
226
227 match resolved_type(tx, model, draft.target_crs).as_deref() {
228 Some("IFCPROJECTEDCRS") => {}
229 Some(other) => {
230 return Err(invalid(
231 entity,
232 "TargetCRS",
233 format!("expected IFCPROJECTEDCRS, found {other}"),
234 ))
235 }
236 None => {
237 return Err(invalid(
238 entity,
239 "TargetCRS",
240 "target does not resolve to a staged entity",
241 ))
242 }
243 }
244
245 if let Some((abscissa, ordinate)) = draft.x_axis {
246 if !abscissa.is_finite() || !ordinate.is_finite() {
247 return Err(invalid(
248 entity,
249 "XAxisAbscissa",
250 format!("expected a finite direction, got ({abscissa}, {ordinate})"),
251 ));
252 }
253 if abscissa == 0.0 && ordinate == 0.0 {
254 return Err(invalid(
255 entity,
256 "XAxisAbscissa",
257 "a zero-length vector gives no rotation",
258 ));
259 }
260 }
261
262 if let Some(scale) = draft.scale {
267 if !positive_finite(scale) {
268 return Err(invalid(
269 entity,
270 "Scale",
271 format!("expected a positive finite scale, got {scale}"),
272 ));
273 }
274 }
275
276 Ok(())
277}
278
279fn positive_finite(value: f64) -> bool {
281 value.is_finite() && value > 0.0
282}
283
284fn conversion_attributes(draft: &MapConversionDraft) -> Vec<Value> {
285 let (abscissa, ordinate) = match draft.x_axis {
286 Some((a, o)) => (Value::Real(a), Value::Real(o)),
287 None => (Value::Null, Value::Null),
288 };
289 vec![
290 Value::Ref(draft.source_crs),
291 Value::Ref(draft.target_crs),
292 Value::Real(draft.eastings),
293 Value::Real(draft.northings),
294 Value::Real(draft.orthogonal_height),
295 abscissa,
296 ordinate,
297 draft.scale.map_or(Value::Null, Value::Real),
298 ]
299}
300
301pub fn create_map_conversion(
310 tx: &mut Transaction,
311 model: &Model,
312 draft: MapConversionDraft,
313) -> GeorefResult<EntityId> {
314 check_conversion("IFCMAPCONVERSION", tx, model, &draft)?;
315 Ok(tx.create(Entity::new(
316 "IFCMAPCONVERSION",
317 conversion_attributes(&draft),
318 )))
319}
320
321pub fn create_map_conversion_scaled(
332 tx: &mut Transaction,
333 model: &Model,
334 draft: MapConversionDraft,
335 factors: (f64, f64, f64),
336) -> GeorefResult<EntityId> {
337 check_conversion("IFCMAPCONVERSIONSCALED", tx, model, &draft)?;
338
339 let (x, y, z) = factors;
342 for (attribute, value) in [("FactorX", x), ("FactorY", y), ("FactorZ", z)] {
343 if !positive_finite(value) {
344 return Err(invalid(
345 "IFCMAPCONVERSIONSCALED",
346 attribute,
347 format!("expected a positive finite factor, got {value}"),
348 ));
349 }
350 }
351
352 let mut attributes = conversion_attributes(&draft);
353 attributes.extend([Value::Real(x), Value::Real(y), Value::Real(z)]);
354 Ok(tx.create(Entity::new("IFCMAPCONVERSIONSCALED", attributes)))
355}
356
357pub fn create_rigid_operation(
375 tx: &mut Transaction,
376 source_crs: EntityId,
377 target_crs: EntityId,
378 coordinates: (f64, f64),
379 height: Option<f64>,
380) -> GeorefResult<EntityId> {
381 rigid_operation(
382 tx,
383 source_crs,
384 target_crs,
385 coordinates,
386 height,
387 "IFCLENGTHMEASURE",
388 )
389}
390
391pub fn create_angular_rigid_operation(
402 tx: &mut Transaction,
403 source_crs: EntityId,
404 target_crs: EntityId,
405 coordinates: (f64, f64),
406 height: Option<f64>,
407) -> GeorefResult<EntityId> {
408 rigid_operation(
409 tx,
410 source_crs,
411 target_crs,
412 coordinates,
413 height,
414 "IFCPLANEANGLEMEASURE",
415 )
416}
417
418fn rigid_operation(
419 tx: &mut Transaction,
420 source_crs: EntityId,
421 target_crs: EntityId,
422 coordinates: (f64, f64),
423 height: Option<f64>,
424 measure: &'static str,
425) -> GeorefResult<EntityId> {
426 let (first, second) = coordinates;
427 for (attribute, value) in [("FirstCoordinate", first), ("SecondCoordinate", second)] {
428 if !value.is_finite() {
429 return Err(invalid("IFCRIGIDOPERATION", attribute, value.to_string()));
430 }
431 }
432 if let Some(value) = height {
433 if !value.is_finite() {
434 return Err(invalid("IFCRIGIDOPERATION", "Height", value.to_string()));
435 }
436 }
437 let typed = |value: f64| Value::Typed {
438 type_name: measure.into(),
439 value: Box::new(Value::Real(value)),
440 };
441
442 Ok(tx.create(Entity::new(
443 "IFCRIGIDOPERATION",
444 vec![
445 Value::Ref(source_crs),
446 Value::Ref(target_crs),
447 typed(first),
448 typed(second),
449 height.map_or(Value::Null, Value::Real),
450 ],
451 )))
452}
453
454pub fn create_well_known_text(
466 tx: &mut Transaction,
467 model: &Model,
468 well_known_text: &str,
469 coordinate_reference_system: EntityId,
470) -> GeorefResult<EntityId> {
471 const ENTITY: &str = "IFCWELLKNOWNTEXT";
472 const CRS_TYPES: &[&str] = &[
473 "IFCPROJECTEDCRS",
474 "IFCGEOGRAPHICCRS",
475 "IFCCOORDINATEREFERENCESYSTEM",
476 ];
477 if well_known_text.trim().is_empty() {
478 return Err(invalid(ENTITY, "WellKnownText", well_known_text));
479 }
480 let actual = tx
481 .edits()
482 .iter()
483 .rev()
484 .find_map(|edit| match edit {
485 ifc_model::Edit::Create { id, entity } if *id == coordinate_reference_system => {
486 Some(entity.type_name.as_ref().to_owned())
487 }
488 _ => None,
489 })
490 .or_else(|| {
491 model
492 .get(coordinate_reference_system)
493 .map(|entity| entity.type_name.as_ref().to_owned())
494 })
495 .unwrap_or_default();
496 if !CRS_TYPES
497 .iter()
498 .any(|kind| actual.eq_ignore_ascii_case(kind))
499 {
500 return Err(invalid(ENTITY, "CoordinateReferenceSystem", actual));
501 }
502 Ok(tx.create(Entity::new(
503 ENTITY,
504 vec![
505 Value::Text(well_known_text.into()),
506 Value::Ref(coordinate_reference_system),
507 ],
508 )))
509}