Skip to main content

ifc_alignment/cant/
segment.rs

1//! IFC4x3 cant 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/// `IfcAlignmentCantSegmentTypeEnum` member: the cant transition law applied
11/// over one segment's distance-along span.
12#[non_exhaustive]
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum CantSegmentType {
15    /// `BLOSSCURVE`: a bloss (cubic) transition between cant values.
16    BlossCurve,
17    /// `CONSTANTCANT`: cant held level across the segment.
18    ConstantCant,
19    /// `COSINECURVE`: a cosine-shaped transition between cant values.
20    CosineCurve,
21    /// `HELMERTCURVE`: a Helmert (parabolic) transition between cant values.
22    HelmertCurve,
23    /// `LINEARTRANSITION`: a straight-line ramp between cant values.
24    LinearTransition,
25    /// `SINECURVE`: a sine-shaped transition between cant values.
26    SineCurve,
27    /// `VIENNESEBEND`: a cant swing whose bank angle follows a
28    /// closed-form quartic.
29    ///
30    /// Cant evaluation resolves this exactly. It is the one type that also
31    /// needs the parent `IfcAlignmentCant.RailHeadDistance`, because the
32    /// spec defines the swing through a bank angle rather than a cant
33    /// value directly.
34    VienneseBend,
35    /// `USERDEFINED`: an author-supplied law outside the enumerated set.
36    UserDefined,
37    /// `NOTDEFINED`: no cant transition law was declared.
38    NotDefined,
39    /// An enumeration token this crate does not recognise, preserved verbatim.
40    Other(String),
41}
42
43/// Borrowed projection of one nested segment of an `IfcAlignmentCant` layout
44/// (`IfcAlignmentCantSegment`), with lengths already reduced to metres.
45#[derive(Debug, Clone, PartialEq)]
46pub struct CantSegment {
47    /// The `IfcAlignmentCantSegment` entity this was read from.
48    pub entity: EntityId,
49    /// `StartDistAlong`: distance along the parent alignment where this
50    /// segment begins.
51    pub start_dist_along: f64,
52    /// `HorizontalLength`: the segment's span along the alignment.
53    pub horizontal_length: f64,
54    /// `StartCantLeft`: cant applied to the left rail at the segment start.
55    pub start_cant_left: f64,
56    /// `EndCantLeft`: cant applied to the left rail at the segment end, when
57    /// authored. Absence is legal only when `end_cant_right` is also absent.
58    pub end_cant_left: Option<f64>,
59    /// `StartCantRight`: cant applied to the right rail at the segment start.
60    pub start_cant_right: f64,
61    /// `EndCantRight`: cant applied to the right rail at the segment end,
62    /// when authored. Absence is legal only when `end_cant_left` is also
63    /// absent.
64    pub end_cant_right: Option<f64>,
65    /// `PredefinedType`: which transition law governs this segment.
66    pub predefined_type: CantSegmentType,
67}
68
69impl CantSegment {
70    /// The `PredefinedType` enumeration token this segment was read from.
71    pub(crate) fn predefined_type_name(&self) -> &str {
72        match &self.predefined_type {
73            CantSegmentType::BlossCurve => "BLOSSCURVE",
74            CantSegmentType::ConstantCant => "CONSTANTCANT",
75            CantSegmentType::CosineCurve => "COSINECURVE",
76            CantSegmentType::HelmertCurve => "HELMERTCURVE",
77            CantSegmentType::LinearTransition => "LINEARTRANSITION",
78            CantSegmentType::SineCurve => "SINECURVE",
79            CantSegmentType::VienneseBend => "VIENNESEBEND",
80            CantSegmentType::UserDefined => "USERDEFINED",
81            CantSegmentType::NotDefined => "NOTDEFINED",
82            CantSegmentType::Other(name) => name,
83        }
84    }
85}
86
87/// Read one `IfcAlignmentCantSegment` referenced by `id`.
88///
89/// Fails if `id` is missing, is not an `IfcAlignmentCantSegment`, an
90/// attribute is missing or the wrong kind, `HorizontalLength` is negative,
91/// any value is non-finite, or exactly one of `EndCantLeft`/`EndCantRight`
92/// is supplied without the other.
93pub fn read_cant_segment(
94    model: &Model,
95    id: EntityId,
96    units: AlignmentUnits,
97) -> AlignmentResult<CantSegment> {
98    validate_units(units)?;
99    let entity = model
100        .get(id)
101        .ok_or(AlignmentError::MissingEntity { entity: id })?;
102    if !entity
103        .type_name
104        .eq_ignore_ascii_case("IFCALIGNMENTCANTSEGMENT")
105    {
106        return Err(AlignmentError::WrongType {
107            entity: id,
108            expected: "IFCALIGNMENTCANTSEGMENT",
109            actual: entity.type_name.to_string(),
110        });
111    }
112    // IFC4X3_ADD2: inherited StartTag/EndTag are slots 0..1; this declaration
113    // contributes StartDistAlong through PredefinedType at slots 2..8.
114    let values = &entity.attributes;
115    let start_dist_along = length(
116        number(values, id, slot::cant::START_DIST_ALONG, "StartDistAlong")?,
117        units,
118    );
119    let horizontal_length = length(
120        number(
121            values,
122            id,
123            slot::cant::HORIZONTAL_LENGTH,
124            "HorizontalLength",
125        )?,
126        units,
127    );
128    let start_cant_left = length(
129        number(values, id, slot::cant::START_CANT_LEFT, "StartCantLeft")?,
130        units,
131    );
132    let end_cant_left = optional_number(values, id, slot::cant::END_CANT_LEFT, "EndCantLeft")?
133        .map(|value| length(value, units));
134    let start_cant_right = length(
135        number(values, id, slot::cant::START_CANT_RIGHT, "StartCantRight")?,
136        units,
137    );
138    let end_cant_right = optional_number(values, id, slot::cant::END_CANT_RIGHT, "EndCantRight")?
139        .map(|value| length(value, units));
140    let predefined_type = parse_type(enum_name(
141        values,
142        id,
143        slot::cant::PREDEFINED_TYPE,
144        "PredefinedType",
145    )?);
146
147    let finite = [
148        start_dist_along,
149        horizontal_length,
150        start_cant_left,
151        start_cant_right,
152    ]
153    .into_iter()
154    .all(f64::is_finite)
155        && end_cant_left.is_none_or(f64::is_finite)
156        && end_cant_right.is_none_or(f64::is_finite);
157    if !finite || horizontal_length < 0.0 {
158        return Err(AlignmentError::InvalidSegment {
159            entity: id,
160            detail: "cant parameters must be finite and horizontal length non-negative",
161        });
162    }
163    let ends_are_paired = end_cant_left.is_some() == end_cant_right.is_some();
164    if !ends_are_paired {
165        return Err(AlignmentError::InvalidSegment {
166            entity: id,
167            detail: "left and right end cant must both be supplied or both omitted",
168        });
169    }
170
171    Ok(CantSegment {
172        entity: id,
173        start_dist_along,
174        horizontal_length,
175        start_cant_left,
176        end_cant_left,
177        start_cant_right,
178        end_cant_right,
179        predefined_type,
180    })
181}
182
183fn parse_type(name: &str) -> CantSegmentType {
184    match name.to_ascii_uppercase().as_str() {
185        "BLOSSCURVE" => CantSegmentType::BlossCurve,
186        "CONSTANTCANT" => CantSegmentType::ConstantCant,
187        "COSINECURVE" => CantSegmentType::CosineCurve,
188        "HELMERTCURVE" => CantSegmentType::HelmertCurve,
189        "LINEARTRANSITION" => CantSegmentType::LinearTransition,
190        "SINECURVE" => CantSegmentType::SineCurve,
191        "VIENNESEBEND" => CantSegmentType::VienneseBend,
192        "USERDEFINED" => CantSegmentType::UserDefined,
193        "NOTDEFINED" => CantSegmentType::NotDefined,
194        _ => CantSegmentType::Other(name.to_string()),
195    }
196}
197
198fn validate_units(units: AlignmentUnits) -> AlignmentResult<()> {
199    if !units.length_to_metres.is_finite() || units.length_to_metres <= 0.0 {
200        return Err(AlignmentError::InvalidUnits {
201            detail: "length factor must be finite and positive",
202        });
203    }
204    Ok(())
205}
206
207fn length(value: f64, units: AlignmentUnits) -> f64 {
208    value * units.length_to_metres
209}
210
211fn number(values: &[Value], id: EntityId, slot: usize, name: &'static str) -> AlignmentResult<f64> {
212    match values.get(slot) {
213        Some(Value::Real(value)) => Ok(*value),
214        Some(Value::Integer(value)) => Ok(*value as f64),
215        _ => Err(AlignmentError::InvalidAttribute {
216            entity: id,
217            index: slot,
218            name,
219        }),
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        Some(Value::Real(value)) => Ok(Some(*value)),
232        Some(Value::Integer(value)) => Ok(Some(*value as f64)),
233        _ => Err(AlignmentError::InvalidAttribute {
234            entity: id,
235            index: slot,
236            name,
237        }),
238    }
239}
240
241fn enum_name<'a>(
242    values: &'a [Value],
243    id: EntityId,
244    slot: usize,
245    name: &'static str,
246) -> AlignmentResult<&'a str> {
247    match values.get(slot) {
248        Some(Value::Enum(value)) => Ok(value),
249        _ => Err(AlignmentError::InvalidAttribute {
250            entity: id,
251            index: slot,
252            name,
253        }),
254    }
255}