Skip to main content

hfx_core/
catchment.rs

1//! Drainage-unit domain type.
2
3use crate::area::AreaKm2;
4use crate::geo::{BoundingBox, OutletCoord, WkbGeometry};
5use crate::id::UnitId;
6use crate::level::Level;
7
8/// A single drainage unit in an HFX dataset.
9///
10/// Every field is validated at construction time via the primitive newtypes
11/// ([`UnitId`], [`Level`], [`AreaKm2`], [`OutletCoord`], [`BoundingBox`],
12/// [`WkbGeometry`]); `CatchmentUnit`
13/// itself performs no additional validation.
14#[derive(Debug, Clone, PartialEq)]
15pub struct CatchmentUnit {
16    id: UnitId,
17    level: Level,
18    parent_id: Option<UnitId>,
19    area: AreaKm2,
20    upstream_area: Option<AreaKm2>,
21    outlet: OutletCoord,
22    bbox: BoundingBox,
23    geometry: WkbGeometry,
24    source_id: Option<String>,
25    level_label: Option<String>,
26}
27
28impl CatchmentUnit {
29    /// Construct a `CatchmentUnit` from its constituent validated fields.
30    ///
31    /// All arguments are already domain-typed, so no further validation is
32    /// performed here — invalid states are unrepresentable by construction.
33    #[allow(clippy::too_many_arguments)]
34    pub fn new(
35        id: UnitId,
36        level: Level,
37        parent_id: Option<UnitId>,
38        area: AreaKm2,
39        upstream_area: Option<AreaKm2>,
40        outlet: OutletCoord,
41        bbox: BoundingBox,
42        geometry: WkbGeometry,
43        source_id: Option<String>,
44        level_label: Option<String>,
45    ) -> Self {
46        Self {
47            id,
48            level,
49            parent_id,
50            area,
51            upstream_area,
52            outlet,
53            bbox,
54            geometry,
55            source_id,
56            level_label,
57        }
58    }
59
60    /// Return the unit's unique identifier.
61    pub fn id(&self) -> UnitId {
62        self.id
63    }
64
65    /// Return the dataset-local level of this unit.
66    pub fn level(&self) -> Level {
67        self.level
68    }
69
70    /// Return the optional containing coarser unit ID.
71    pub fn parent_id(&self) -> Option<UnitId> {
72        self.parent_id
73    }
74
75    /// Return the local drainage area of this unit in km².
76    pub fn area(&self) -> AreaKm2 {
77        self.area
78    }
79
80    /// Return the total upstream contributing area in km², if known.
81    ///
82    /// `None` indicates the value is absent in the source hydrofabric.
83    pub fn upstream_area(&self) -> Option<AreaKm2> {
84        self.upstream_area
85    }
86
87    /// Return the unit outlet coordinate.
88    pub fn outlet(&self) -> OutletCoord {
89        self.outlet
90    }
91
92    /// Return a reference to the axis-aligned bounding box of the catchment.
93    pub fn bbox(&self) -> &BoundingBox {
94        &self.bbox
95    }
96
97    /// Return a reference to the WKB geometry of the catchment polygon.
98    pub fn geometry(&self) -> &WkbGeometry {
99        &self.geometry
100    }
101
102    /// Return the optional source-fabric identifier.
103    pub fn source_id(&self) -> Option<&str> {
104        self.source_id.as_deref()
105    }
106
107    /// Return the optional source-fabric level label.
108    pub fn level_label(&self) -> Option<&str> {
109        self.level_label.as_deref()
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    fn test_unit_id(raw: i64) -> UnitId {
118        UnitId::new(raw).unwrap()
119    }
120
121    fn test_level(raw: i16) -> Level {
122        Level::new(raw).unwrap()
123    }
124
125    fn test_outlet() -> OutletCoord {
126        OutletCoord::new(0.0, 0.0).unwrap()
127    }
128
129    fn test_bbox() -> BoundingBox {
130        BoundingBox::new(-10.0, -5.0, 10.0, 5.0).unwrap()
131    }
132
133    fn test_wkb() -> WkbGeometry {
134        WkbGeometry::new(vec![0x01, 0x02, 0x03]).unwrap()
135    }
136
137    fn test_area(km2: f32) -> AreaKm2 {
138        AreaKm2::new(km2).unwrap()
139    }
140
141    #[test]
142    fn valid_catchment_unit_getters_return_expected_values() {
143        let id = test_unit_id(42);
144        let level = test_level(1);
145        let parent_id = Some(test_unit_id(7));
146        let area = test_area(100.0);
147        let upstream_area = Some(test_area(500.0));
148        let outlet = test_outlet();
149        let bbox = test_bbox();
150        let geometry = test_wkb();
151
152        let unit = CatchmentUnit::new(
153            id,
154            level,
155            parent_id,
156            area,
157            upstream_area,
158            outlet,
159            bbox,
160            geometry.clone(),
161            Some("src-42".to_string()),
162            Some("l1".to_string()),
163        );
164
165        assert_eq!(unit.id(), id);
166        assert_eq!(unit.level(), level);
167        assert_eq!(unit.parent_id(), parent_id);
168        assert_eq!(unit.area(), area);
169        assert_eq!(unit.upstream_area(), upstream_area);
170        assert_eq!(unit.outlet(), outlet);
171        assert_eq!(unit.bbox(), &bbox);
172        assert_eq!(unit.geometry(), &geometry);
173        assert_eq!(unit.source_id(), Some("src-42"));
174        assert_eq!(unit.level_label(), Some("l1"));
175    }
176
177    #[test]
178    fn upstream_area_none_returns_none() {
179        let unit = CatchmentUnit::new(
180            test_unit_id(1),
181            test_level(0),
182            None,
183            test_area(50.0),
184            None,
185            test_outlet(),
186            test_bbox(),
187            test_wkb(),
188            None,
189            None,
190        );
191
192        assert_eq!(unit.upstream_area(), None);
193    }
194
195    #[test]
196    fn upstream_area_some_returns_some() {
197        let up_area = test_area(999.9);
198        let unit = CatchmentUnit::new(
199            test_unit_id(1),
200            test_level(0),
201            None,
202            test_area(50.0),
203            Some(up_area),
204            test_outlet(),
205            test_bbox(),
206            test_wkb(),
207            None,
208            None,
209        );
210
211        assert_eq!(unit.upstream_area(), Some(up_area));
212    }
213}