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,
27 UserDefined,
29 NotDefined,
31 Other(String),
33}
34
35impl VerticalSegmentType {
36 pub(crate) fn source_name(&self) -> &str {
37 match self {
38 Self::CircularArc => "CIRCULARARC",
39 Self::Clothoid => "CLOTHOID",
40 Self::ConstantGradient => "CONSTANTGRADIENT",
41 Self::ParabolicArc => "PARABOLICARC",
42 Self::UserDefined => "USERDEFINED",
43 Self::NotDefined => "NOTDEFINED",
44 Self::Other(name) => name,
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq)]
51pub struct VerticalSegment {
52 pub entity: EntityId,
54 pub start_dist_along: f64,
57 pub horizontal_length: f64,
59 pub start_height: f64,
61 pub start_gradient: f64,
63 pub end_gradient: f64,
65 pub radius_of_curvature: Option<f64>,
68 pub predefined_type: VerticalSegmentType,
70}
71
72pub fn read_vertical_segment(
80 model: &Model,
81 id: EntityId,
82 units: AlignmentUnits,
83) -> AlignmentResult<VerticalSegment> {
84 validate_units(units)?;
85 let entity = model
86 .get(id)
87 .ok_or(AlignmentError::MissingEntity { entity: id })?;
88 if !entity
89 .type_name
90 .eq_ignore_ascii_case("IFCALIGNMENTVERTICALSEGMENT")
91 {
92 return Err(AlignmentError::WrongType {
93 entity: id,
94 expected: "IFCALIGNMENTVERTICALSEGMENT",
95 actual: entity.type_name.to_string(),
96 });
97 }
98 let values = &entity.attributes;
101 let start_dist_along = length(
102 number(
103 values,
104 id,
105 slot::vertical::START_DIST_ALONG,
106 "StartDistAlong",
107 )?,
108 units,
109 );
110 let horizontal_length = length(
111 number(
112 values,
113 id,
114 slot::vertical::HORIZONTAL_LENGTH,
115 "HorizontalLength",
116 )?,
117 units,
118 );
119 let start_height = length(
120 number(values, id, slot::vertical::START_HEIGHT, "StartHeight")?,
121 units,
122 );
123 let start_gradient = number(values, id, slot::vertical::START_GRADIENT, "StartGradient")?;
124 let end_gradient = number(values, id, slot::vertical::END_GRADIENT, "EndGradient")?;
125 let radius_of_curvature = optional_number(
126 values,
127 id,
128 slot::vertical::RADIUS_OF_CURVATURE,
129 "RadiusOfCurvature",
130 )?
131 .map(|value| length(value, units));
132 let predefined_type = parse_type(enum_name(
133 values,
134 id,
135 slot::vertical::PREDEFINED_TYPE,
136 "PredefinedType",
137 )?);
138
139 let finite = [
140 start_dist_along,
141 horizontal_length,
142 start_height,
143 start_gradient,
144 end_gradient,
145 ]
146 .into_iter()
147 .all(f64::is_finite)
148 && radius_of_curvature.is_none_or(f64::is_finite);
149 if !finite || horizontal_length < 0.0 {
150 return Err(AlignmentError::InvalidSegment {
151 entity: id,
152 detail: "vertical parameters must be finite and horizontal length non-negative",
153 });
154 }
155 let needs_radius = matches!(
156 predefined_type,
157 VerticalSegmentType::CircularArc | VerticalSegmentType::ParabolicArc
158 );
159 if needs_radius != radius_of_curvature.is_some() {
160 return Err(AlignmentError::InvalidSegment {
161 entity: id,
162 detail: "radius is required exactly for circular and parabolic vertical segments",
163 });
164 }
165
166 Ok(VerticalSegment {
167 entity: id,
168 start_dist_along,
169 horizontal_length,
170 start_height,
171 start_gradient,
172 end_gradient,
173 radius_of_curvature,
174 predefined_type,
175 })
176}
177
178fn parse_type(name: &str) -> VerticalSegmentType {
179 match name.to_ascii_uppercase().as_str() {
180 "CONSTANTGRADIENT" => VerticalSegmentType::ConstantGradient,
181 "CIRCULARARC" => VerticalSegmentType::CircularArc,
182 "PARABOLICARC" => VerticalSegmentType::ParabolicArc,
183 "CLOTHOID" => VerticalSegmentType::Clothoid,
184 "USERDEFINED" => VerticalSegmentType::UserDefined,
185 "NOTDEFINED" => VerticalSegmentType::NotDefined,
186 _ => VerticalSegmentType::Other(name.to_string()),
187 }
188}
189
190fn validate_units(units: AlignmentUnits) -> AlignmentResult<()> {
191 if !units.length_to_metres.is_finite() || units.length_to_metres <= 0.0 {
192 return Err(AlignmentError::InvalidUnits {
193 detail: "length factor must be finite and positive",
194 });
195 }
196 Ok(())
197}
198
199fn length(value: f64, units: AlignmentUnits) -> f64 {
200 value * units.length_to_metres
201}
202
203fn number(values: &[Value], id: EntityId, slot: usize, name: &'static str) -> AlignmentResult<f64> {
204 match values.get(slot) {
205 Some(Value::Real(value)) => Ok(*value),
206 Some(Value::Integer(value)) => Ok(*value as f64),
207 _ => Err(AlignmentError::InvalidAttribute {
208 entity: id,
209 index: slot,
210 name,
211 }),
212 }
213}
214
215fn optional_number(
216 values: &[Value],
217 id: EntityId,
218 slot: usize,
219 name: &'static str,
220) -> AlignmentResult<Option<f64>> {
221 match values.get(slot) {
222 Some(Value::Null) => Ok(None),
223 Some(Value::Real(value)) => Ok(Some(*value)),
224 Some(Value::Integer(value)) => Ok(Some(*value as f64)),
225 _ => Err(AlignmentError::InvalidAttribute {
226 entity: id,
227 index: slot,
228 name,
229 }),
230 }
231}
232
233fn enum_name<'a>(
234 values: &'a [Value],
235 id: EntityId,
236 slot: usize,
237 name: &'static str,
238) -> AlignmentResult<&'a str> {
239 match values.get(slot) {
240 Some(Value::Enum(value)) => Ok(value),
241 _ => Err(AlignmentError::InvalidAttribute {
242 entity: id,
243 index: slot,
244 name,
245 }),
246 }
247}