1use ifc_model::value::Value;
4use ifc_model::{EntityId, Model};
5
6use crate::error::{AlignmentError, AlignmentResult};
7use crate::horizontal::AlignmentUnits;
8use crate::slot;
9
10#[non_exhaustive]
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum VerticalSegmentType {
15 ConstantGradient,
19 CircularArc,
23 ParabolicArc,
26 Clothoid,
29 UserDefined,
31 NotDefined,
33 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#[derive(Debug, Clone, PartialEq)]
53pub struct VerticalSegment {
54 pub entity: EntityId,
56 pub start_dist_along: f64,
59 pub horizontal_length: f64,
61 pub start_height: f64,
63 pub start_gradient: f64,
65 pub end_gradient: f64,
67 pub radius_of_curvature: Option<f64>,
70 pub predefined_type: VerticalSegmentType,
72}
73
74pub 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 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 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 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}