Skip to main content

ifc_alignment/vertical/
segment.rs

1//! IFC4x3 vertical alignment segment parameters.
2
3use ifc_model::value::Value;
4use ifc_model::{EntityId, Model};
5
6use crate::error::{AlignmentError, AlignmentResult};
7use crate::horizontal::AlignmentUnits;
8use crate::slot;
9
10/// `IfcAlignmentVerticalSegmentTypeEnum` member: the vertical curve law
11/// applied over one segment's distance-along span.
12#[non_exhaustive]
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum VerticalSegmentType {
15    /// `CONSTANTGRADIENT`: a straight grade line (`start_gradient` ==
16    /// `end_gradient`, no `radius_of_curvature`). The only kind exact
17    /// neutral lowering currently supports.
18    ConstantGradient,
19    /// `CIRCULARARC`: constant-radius vertical curve; requires
20    /// `radius_of_curvature`.
21    CircularArc,
22    /// `PARABOLICARC`: parabolic vertical curve; requires
23    /// `radius_of_curvature`.
24    ParabolicArc,
25    /// `CLOTHOID`: a clothoid-law vertical transition.
26    Clothoid,
27    /// `USERDEFINED`: an author-supplied law outside the enumerated set.
28    UserDefined,
29    /// `NOTDEFINED`: no vertical segment kind was declared.
30    NotDefined,
31    /// An enumeration token this crate does not recognise, preserved verbatim.
32    Other(String),
33}
34
35impl VerticalSegmentType {
36    pub(crate) fn source_name(&self) -> &str {
37        match self {
38            Self::CircularArc => "CIRCULARARC",
39            Self::Clothoid => "CLOTHOID",
40            Self::ConstantGradient => "CONSTANTGRADIENT",
41            Self::ParabolicArc => "PARABOLICARC",
42            Self::UserDefined => "USERDEFINED",
43            Self::NotDefined => "NOTDEFINED",
44            Self::Other(name) => name,
45        }
46    }
47}
48
49/// Resolved IFC4x3 `IfcAlignmentVerticalSegment` parameters in SI units.
50#[derive(Debug, Clone, PartialEq)]
51pub struct VerticalSegment {
52    /// The `IfcAlignmentVerticalSegment` entity this was read from.
53    pub entity: EntityId,
54    /// `StartDistAlong`: distance along the parent alignment where this
55    /// segment begins.
56    pub start_dist_along: f64,
57    /// `HorizontalLength`: the segment's span along the alignment.
58    pub horizontal_length: f64,
59    /// `StartHeight`: elevation at the segment start.
60    pub start_height: f64,
61    /// `StartGradient`.
62    pub start_gradient: f64,
63    /// `EndGradient`.
64    pub end_gradient: f64,
65    /// `RadiusOfCurvature`, required exactly for `CircularArc` and
66    /// `ParabolicArc` segments, absent otherwise.
67    pub radius_of_curvature: Option<f64>,
68    /// `PredefinedType`: which vertical curve law governs this segment.
69    pub predefined_type: VerticalSegmentType,
70}
71
72/// Read one `IfcAlignmentVerticalSegment` referenced by `id`.
73///
74/// Fails if `units` is non-finite or non-positive, `id` is missing or not an
75/// `IfcAlignmentVerticalSegment`, an attribute is missing or the wrong kind,
76/// `HorizontalLength` is negative, any value is non-finite, or
77/// `RadiusOfCurvature` is present/absent inconsistently with `PredefinedType`
78/// (required for `CircularArc`/`ParabolicArc`, forbidden otherwise).
79pub fn read_vertical_segment(
80    model: &Model,
81    id: EntityId,
82    units: AlignmentUnits,
83) -> AlignmentResult<VerticalSegment> {
84    validate_units(units)?;
85    let entity = model
86        .get(id)
87        .ok_or(AlignmentError::MissingEntity { entity: id })?;
88    if !entity
89        .type_name
90        .eq_ignore_ascii_case("IFCALIGNMENTVERTICALSEGMENT")
91    {
92        return Err(AlignmentError::WrongType {
93            entity: id,
94            expected: "IFCALIGNMENTVERTICALSEGMENT",
95            actual: entity.type_name.to_string(),
96        });
97    }
98    // IFC4X3_ADD2: inherited StartTag/EndTag are slots 0..1; this declaration
99    // contributes StartDistAlong through PredefinedType at slots 2..8.
100    let values = &entity.attributes;
101    let start_dist_along = length(
102        number(
103            values,
104            id,
105            slot::vertical::START_DIST_ALONG,
106            "StartDistAlong",
107        )?,
108        units,
109    );
110    let horizontal_length = length(
111        number(
112            values,
113            id,
114            slot::vertical::HORIZONTAL_LENGTH,
115            "HorizontalLength",
116        )?,
117        units,
118    );
119    let start_height = length(
120        number(values, id, slot::vertical::START_HEIGHT, "StartHeight")?,
121        units,
122    );
123    let start_gradient = number(values, id, slot::vertical::START_GRADIENT, "StartGradient")?;
124    let end_gradient = number(values, id, slot::vertical::END_GRADIENT, "EndGradient")?;
125    let radius_of_curvature = optional_number(
126        values,
127        id,
128        slot::vertical::RADIUS_OF_CURVATURE,
129        "RadiusOfCurvature",
130    )?
131    .map(|value| length(value, units));
132    let predefined_type = parse_type(enum_name(
133        values,
134        id,
135        slot::vertical::PREDEFINED_TYPE,
136        "PredefinedType",
137    )?);
138
139    let finite = [
140        start_dist_along,
141        horizontal_length,
142        start_height,
143        start_gradient,
144        end_gradient,
145    ]
146    .into_iter()
147    .all(f64::is_finite)
148        && radius_of_curvature.is_none_or(f64::is_finite);
149    if !finite || horizontal_length < 0.0 {
150        return Err(AlignmentError::InvalidSegment {
151            entity: id,
152            detail: "vertical parameters must be finite and horizontal length non-negative",
153        });
154    }
155    let needs_radius = matches!(
156        predefined_type,
157        VerticalSegmentType::CircularArc | VerticalSegmentType::ParabolicArc
158    );
159    if needs_radius != radius_of_curvature.is_some() {
160        return Err(AlignmentError::InvalidSegment {
161            entity: id,
162            detail: "radius is required exactly for circular and parabolic vertical segments",
163        });
164    }
165
166    Ok(VerticalSegment {
167        entity: id,
168        start_dist_along,
169        horizontal_length,
170        start_height,
171        start_gradient,
172        end_gradient,
173        radius_of_curvature,
174        predefined_type,
175    })
176}
177
178fn parse_type(name: &str) -> VerticalSegmentType {
179    match name.to_ascii_uppercase().as_str() {
180        "CONSTANTGRADIENT" => VerticalSegmentType::ConstantGradient,
181        "CIRCULARARC" => VerticalSegmentType::CircularArc,
182        "PARABOLICARC" => VerticalSegmentType::ParabolicArc,
183        "CLOTHOID" => VerticalSegmentType::Clothoid,
184        "USERDEFINED" => VerticalSegmentType::UserDefined,
185        "NOTDEFINED" => VerticalSegmentType::NotDefined,
186        _ => VerticalSegmentType::Other(name.to_string()),
187    }
188}
189
190fn validate_units(units: AlignmentUnits) -> AlignmentResult<()> {
191    if !units.length_to_metres.is_finite() || units.length_to_metres <= 0.0 {
192        return Err(AlignmentError::InvalidUnits {
193            detail: "length factor must be finite and positive",
194        });
195    }
196    Ok(())
197}
198
199fn length(value: f64, units: AlignmentUnits) -> f64 {
200    value * units.length_to_metres
201}
202
203fn number(values: &[Value], id: EntityId, slot: usize, name: &'static str) -> AlignmentResult<f64> {
204    match values.get(slot) {
205        Some(Value::Real(value)) => Ok(*value),
206        Some(Value::Integer(value)) => Ok(*value as f64),
207        _ => Err(AlignmentError::InvalidAttribute {
208            entity: id,
209            index: slot,
210            name,
211        }),
212    }
213}
214
215fn optional_number(
216    values: &[Value],
217    id: EntityId,
218    slot: usize,
219    name: &'static str,
220) -> AlignmentResult<Option<f64>> {
221    match values.get(slot) {
222        Some(Value::Null) => Ok(None),
223        Some(Value::Real(value)) => Ok(Some(*value)),
224        Some(Value::Integer(value)) => Ok(Some(*value as f64)),
225        _ => Err(AlignmentError::InvalidAttribute {
226            entity: id,
227            index: slot,
228            name,
229        }),
230    }
231}
232
233fn enum_name<'a>(
234    values: &'a [Value],
235    id: EntityId,
236    slot: usize,
237    name: &'static str,
238) -> AlignmentResult<&'a str> {
239    match values.get(slot) {
240        Some(Value::Enum(value)) => Ok(value),
241        _ => Err(AlignmentError::InvalidAttribute {
242            entity: id,
243            index: slot,
244            name,
245        }),
246    }
247}