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,
22 ParabolicArc,
25 Clothoid,
28 UserDefined,
30 NotDefined,
32 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#[derive(Debug, Clone, PartialEq)]
52pub struct VerticalSegment {
53 pub entity: EntityId,
55 pub start_dist_along: f64,
58 pub horizontal_length: f64,
60 pub start_height: f64,
62 pub start_gradient: f64,
64 pub end_gradient: f64,
66 pub radius_of_curvature: Option<f64>,
69 pub predefined_type: VerticalSegmentType,
71}
72
73pub 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 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 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 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}