1use crate::area::Weight;
4use crate::geo::{BoundingBox, WkbGeometry};
5use crate::id::{SnapId, UnitId};
6
7#[derive(Debug, thiserror::Error)]
9pub enum SnapError {
10 #[error("unsupported stem role: {value:?}")]
12 UnsupportedStemRole {
13 value: String,
15 },
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum StemRole {
22 Mainstem,
24 Tributary,
26 Distributary,
28 Unknown,
30}
31
32impl std::fmt::Display for StemRole {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 StemRole::Mainstem => write!(f, "mainstem"),
36 StemRole::Tributary => write!(f, "tributary"),
37 StemRole::Distributary => write!(f, "distributary"),
38 StemRole::Unknown => write!(f, "unknown"),
39 }
40 }
41}
42
43impl std::str::FromStr for StemRole {
44 type Err = SnapError;
45
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 match s {
48 "mainstem" => Ok(StemRole::Mainstem),
49 "tributary" => Ok(StemRole::Tributary),
50 "distributary" => Ok(StemRole::Distributary),
51 "unknown" => Ok(StemRole::Unknown),
52 _ => Err(SnapError::UnsupportedStemRole {
53 value: s.to_owned(),
54 }),
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq)]
68pub struct SnapTarget {
69 id: SnapId,
70 unit_id: UnitId,
71 weight: Weight,
72 stem_role: Option<StemRole>,
73 bbox: Option<BoundingBox>,
74 geometry: WkbGeometry,
75}
76
77impl SnapTarget {
78 pub fn new(
83 id: SnapId,
84 unit_id: UnitId,
85 weight: Weight,
86 stem_role: Option<StemRole>,
87 bbox: Option<BoundingBox>,
88 geometry: WkbGeometry,
89 ) -> Self {
90 Self {
91 id,
92 unit_id,
93 weight,
94 stem_role,
95 bbox,
96 geometry,
97 }
98 }
99
100 pub fn id(&self) -> SnapId {
102 self.id
103 }
104
105 pub fn unit_id(&self) -> UnitId {
107 self.unit_id
108 }
109
110 pub fn weight(&self) -> Weight {
112 self.weight
113 }
114
115 pub fn stem_role(&self) -> Option<StemRole> {
117 self.stem_role
118 }
119
120 pub fn bbox(&self) -> Option<&BoundingBox> {
122 self.bbox.as_ref()
123 }
124
125 pub fn geometry(&self) -> &WkbGeometry {
128 &self.geometry
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 fn test_unit_id(raw: i64) -> UnitId {
137 UnitId::new(raw).unwrap()
138 }
139
140 fn test_snap_id(raw: i64) -> SnapId {
141 SnapId::new(raw).unwrap()
142 }
143
144 fn test_bbox() -> BoundingBox {
145 BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap()
146 }
147
148 fn test_wkb() -> WkbGeometry {
149 WkbGeometry::new(vec![0x01, 0x02, 0x03]).unwrap()
150 }
151
152 fn test_weight(raw: f32) -> Weight {
153 Weight::new(raw).unwrap()
154 }
155
156 #[test]
157 fn stem_role_variants_are_not_equal() {
158 assert_ne!(StemRole::Mainstem, StemRole::Tributary);
159 assert_ne!(StemRole::Tributary, StemRole::Distributary);
160 assert_ne!(StemRole::Mainstem, StemRole::Unknown);
161 }
162
163 #[test]
164 fn stem_role_can_be_copied_and_compared() {
165 let status = StemRole::Mainstem;
166 let copy = status;
167 assert_eq!(status, copy);
168
169 let tributary = StemRole::Tributary;
170 let copy2 = tributary;
171 assert_eq!(tributary, copy2);
172 }
173
174 #[test]
175 fn stem_role_parse_accepts_supported_values() {
176 assert_eq!("mainstem".parse::<StemRole>().unwrap(), StemRole::Mainstem);
177 assert_eq!(
178 "tributary".parse::<StemRole>().unwrap(),
179 StemRole::Tributary
180 );
181 assert_eq!(
182 "distributary".parse::<StemRole>().unwrap(),
183 StemRole::Distributary
184 );
185 assert_eq!("unknown".parse::<StemRole>().unwrap(), StemRole::Unknown);
186 }
187
188 #[test]
189 fn stem_role_distributary_roundtrips() {
190 let role: StemRole = "distributary".parse().unwrap();
191 assert_eq!(role, StemRole::Distributary);
192 assert_eq!(role.to_string(), "distributary");
193 }
194
195 #[test]
196 fn stem_role_parse_rejects_unknown_value() {
197 assert!(matches!(
198 "primary".parse::<StemRole>(),
199 Err(SnapError::UnsupportedStemRole { value }) if value == "primary"
200 ));
201 }
202
203 #[test]
204 fn snap_target_getters_return_expected_values() {
205 let snap_id = test_snap_id(7);
206 let unit_id = test_unit_id(3);
207 let weight = test_weight(0.75);
208 let stem_role = Some(StemRole::Mainstem);
209 let bbox = test_bbox();
210 let geometry = test_wkb();
211
212 let target = SnapTarget::new(
213 snap_id,
214 unit_id,
215 weight,
216 stem_role,
217 Some(bbox),
218 geometry.clone(),
219 );
220
221 assert_eq!(target.id(), snap_id);
222 assert_eq!(target.unit_id(), unit_id);
223 assert_eq!(target.weight(), weight);
224 assert_eq!(target.stem_role(), Some(StemRole::Mainstem));
225 assert_eq!(target.bbox(), Some(&bbox));
226 assert_eq!(target.geometry(), &geometry);
227 }
228
229 #[test]
230 fn unit_id_returns_unit_id_passed_to_constructor() {
231 let unit_id = test_unit_id(99);
232 let target = SnapTarget::new(
233 test_snap_id(1),
234 unit_id,
235 test_weight(1.0),
236 Some(StemRole::Tributary),
237 None,
238 test_wkb(),
239 );
240
241 assert_eq!(target.unit_id(), unit_id);
242 assert_eq!(target.bbox(), None);
243 }
244}