Skip to main content

ifc_georef/authoring/
mod.rs

1//! Transactional authoring of the georeferencing entities.
2//!
3//! # Derived attributes are written as `*`, not `$`
4//!
5//! `IfcGeometricRepresentationSubContext` redeclares four inherited
6//! attributes as DERIVE: they are taken from the parent context, not
7//! supplied here. STEP spells a derived attribute as an asterisk, and
8//! `$` would instead claim the value is absent -- a different
9//! statement, and one that loses the parent's precision and world
10//! coordinate system.
11
12use ifc_model::{Entity, EntityId, Transaction, Value};
13
14use crate::error::{GeorefError, GeorefResult};
15
16mod operation;
17
18pub use operation::{
19    create_geographic_crs, create_map_conversion, create_map_conversion_scaled,
20    create_rigid_operation, create_well_known_text, GeographicCrsDraft, MapConversionDraft,
21};
22
23fn invalid(entity: &'static str, attribute: &'static str, value: impl Into<String>) -> GeorefError {
24    GeorefError::AuthoringInvalid {
25        entity,
26        attribute,
27        value: value.into(),
28    }
29}
30
31fn optional_text(value: Option<&str>) -> Value {
32    value.map_or(Value::Null, |t| Value::Text(t.into()))
33}
34
35/// Stage an `IfcDirection`.
36///
37/// # Errors
38///
39/// Refuses ratios that are not 2 or 3 long, which the schema types as
40/// `LIST [2:3]`, and any non-finite or all-zero ratio: a direction of
41/// zero length has no direction.
42pub fn create_direction(tx: &mut Transaction, ratios: &[f64]) -> GeorefResult<EntityId> {
43    if !(2..=3).contains(&ratios.len()) {
44        return Err(invalid(
45            "IFCDIRECTION",
46            "DirectionRatios",
47            format!("{} ratios", ratios.len()),
48        ));
49    }
50    if ratios.iter().any(|r| !r.is_finite()) {
51        return Err(invalid("IFCDIRECTION", "DirectionRatios", "not finite"));
52    }
53    if ratios.iter().all(|r| *r == 0.0) {
54        return Err(invalid("IFCDIRECTION", "DirectionRatios", "zero length"));
55    }
56    Ok(tx.create(Entity::new(
57        "IFCDIRECTION",
58        vec![Value::List(
59            ratios.iter().copied().map(Value::Real).collect(),
60        )],
61    )))
62}
63
64/// Authored fields for `IfcProjectedCRS`.
65#[derive(Debug, Clone, Copy, Default)]
66pub struct ProjectedCrsDraft<'a> {
67    /// `Name`, the CRS identifier such as `EPSG:25832`.
68    pub name: &'a str,
69    /// `Description`.
70    pub description: Option<&'a str>,
71    /// `GeodeticDatum`.
72    pub geodetic_datum: Option<&'a str>,
73    /// `VerticalDatum`.
74    pub vertical_datum: Option<&'a str>,
75    /// `MapProjection`.
76    pub map_projection: Option<&'a str>,
77    /// `MapZone`.
78    pub map_zone: Option<&'a str>,
79    /// `MapUnit`, an `IfcNamedUnit` reference.
80    pub map_unit: Option<EntityId>,
81}
82
83/// Stage an `IfcProjectedCRS`.
84///
85/// `Name` is optional in the schema but required here: the reader
86/// resolves a CRS by name, and an unnamed one cannot be matched to
87/// the projection it claims to use.
88///
89/// # Errors
90///
91/// Refuses a blank name.
92pub fn create_projected_crs(
93    tx: &mut Transaction,
94    draft: ProjectedCrsDraft<'_>,
95) -> GeorefResult<EntityId> {
96    if draft.name.trim().is_empty() {
97        return Err(invalid("IFCPROJECTEDCRS", "Name", draft.name));
98    }
99    Ok(tx.create(Entity::new(
100        "IFCPROJECTEDCRS",
101        vec![
102            Value::Text(draft.name.into()),
103            optional_text(draft.description),
104            optional_text(draft.geodetic_datum),
105            optional_text(draft.vertical_datum),
106            optional_text(draft.map_projection),
107            optional_text(draft.map_zone),
108            draft.map_unit.map_or(Value::Null, Value::Ref),
109        ],
110    )))
111}
112
113/// Stage an `IfcGeometricRepresentationContext`.
114///
115/// # Errors
116///
117/// Refuses a coordinate space dimension outside 1..=3 and a
118/// non-finite precision.
119pub fn create_representation_context(
120    tx: &mut Transaction,
121    context_type: Option<&str>,
122    dimension: i64,
123    precision: Option<f64>,
124    world_coordinate_system: EntityId,
125    true_north: Option<EntityId>,
126) -> GeorefResult<EntityId> {
127    if !(1..=3).contains(&dimension) {
128        return Err(invalid(
129            "IFCGEOMETRICREPRESENTATIONCONTEXT",
130            "CoordinateSpaceDimension",
131            dimension.to_string(),
132        ));
133    }
134    if precision.is_some_and(|p| !p.is_finite()) {
135        return Err(invalid(
136            "IFCGEOMETRICREPRESENTATIONCONTEXT",
137            "Precision",
138            "not finite",
139        ));
140    }
141    Ok(tx.create(Entity::new(
142        "IFCGEOMETRICREPRESENTATIONCONTEXT",
143        vec![
144            // ContextIdentifier at 0, ContextType at 1.
145            Value::Null,
146            optional_text(context_type),
147            Value::Integer(dimension),
148            precision.map_or(Value::Null, Value::Real),
149            Value::Ref(world_coordinate_system),
150            true_north.map_or(Value::Null, Value::Ref),
151        ],
152    )))
153}
154
155/// Stage an `IfcGeometricRepresentationSubContext`.
156///
157/// The four inherited geometry attributes are written as
158/// `Value::Derived`: the schema computes them from `ParentContext`,
159/// and writing `$` instead would claim the parent's precision and
160/// world coordinate system are simply absent.
161///
162/// # Errors
163///
164/// Enforces the schema's two WHERE rules. ParentNoSub: a subcontext
165/// cannot parent another subcontext, since the derived attributes
166/// resolve one level only. UserTargetProvided: a USERDEFINED target
167/// view without a name states a custom view and then fails to name
168/// it, leaving nothing for a consumer to match on.
169pub fn create_representation_subcontext(
170    tx: &mut Transaction,
171    parent: EntityId,
172    parent_is_subcontext: bool,
173    context_identifier: Option<&str>,
174    target_view: &str,
175    user_defined_target_view: Option<&str>,
176) -> GeorefResult<EntityId> {
177    if parent_is_subcontext {
178        return Err(invalid(
179            "IFCGEOMETRICREPRESENTATIONSUBCONTEXT",
180            "ParentContext",
181            "a subcontext cannot parent a subcontext",
182        ));
183    }
184    if target_view.trim().is_empty() {
185        return Err(invalid(
186            "IFCGEOMETRICREPRESENTATIONSUBCONTEXT",
187            "TargetView",
188            target_view,
189        ));
190    }
191    let named = user_defined_target_view.is_some_and(|v| !v.trim().is_empty());
192    if target_view == "USERDEFINED" && !named {
193        return Err(invalid(
194            "IFCGEOMETRICREPRESENTATIONSUBCONTEXT",
195            "UserDefinedTargetView",
196            "required when TargetView is USERDEFINED",
197        ));
198    }
199    Ok(tx.create(Entity::new(
200        "IFCGEOMETRICREPRESENTATIONSUBCONTEXT",
201        vec![
202            optional_text(context_identifier),
203            Value::Null,
204            // CoordinateSpaceDimension, Precision, WorldCoordinateSystem
205            // and TrueNorth: all DERIVE from ParentContext.
206            Value::Derived,
207            Value::Derived,
208            Value::Derived,
209            Value::Derived,
210            Value::Ref(parent),
211            Value::Null,
212            Value::Enum(target_view.into()),
213            optional_text(user_defined_target_view),
214        ],
215    )))
216}