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