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