model.rs (5623B)
1 //! Provider-owned locality result models. 2 3 use crate::Error; 4 5 /// A geographic point in decimal degrees. 6 #[derive(Clone, Copy, Debug, PartialEq)] 7 pub struct Point { 8 latitude: f64, 9 longitude: f64, 10 } 11 12 impl Point { 13 /// Creates a finite point within the WGS84 latitude/longitude bounds. 14 pub fn new(latitude: f64, longitude: f64) -> Result<Self, Error> { 15 if !latitude.is_finite() 16 || !longitude.is_finite() 17 || !(-90.0..=90.0).contains(&latitude) 18 || !(-180.0..=180.0).contains(&longitude) 19 { 20 return Err(Error::InvalidPoint); 21 } 22 Ok(Self { 23 latitude, 24 longitude, 25 }) 26 } 27 28 /// Returns the latitude in decimal degrees. 29 #[must_use] 30 pub const fn latitude(self) -> f64 { 31 self.latitude 32 } 33 34 /// Returns the longitude in decimal degrees. 35 #[must_use] 36 pub const fn longitude(self) -> f64 { 37 self.longitude 38 } 39 } 40 41 /// One deterministic locality candidate returned by GeoNames. 42 #[derive(Clone, Debug, PartialEq)] 43 pub struct Candidate { 44 feature_id: u64, 45 name: String, 46 admin1_id: Option<String>, 47 admin1_name: Option<String>, 48 country_id: String, 49 country_name: Option<String>, 50 point: Point, 51 display_name: String, 52 } 53 54 impl Candidate { 55 pub(crate) fn from_provider_row( 56 feature_id: u64, 57 name: String, 58 admin1_id: Option<String>, 59 admin1_name: Option<String>, 60 country_id: String, 61 country_name: Option<String>, 62 point: Point, 63 ) -> Self { 64 let mut display_parts = vec![name.clone()]; 65 if let Some(admin1_name) = &admin1_name { 66 display_parts.push(admin1_name.clone()); 67 } 68 display_parts.push(country_name.clone().unwrap_or_else(|| country_id.clone())); 69 Self { 70 feature_id, 71 name, 72 admin1_id, 73 admin1_name, 74 country_id, 75 country_name, 76 point, 77 display_name: display_parts.join(", "), 78 } 79 } 80 81 /// Returns the stable GeoNames feature identifier. 82 #[must_use] 83 pub const fn feature_id(&self) -> u64 { 84 self.feature_id 85 } 86 87 /// Returns the canonical locality name. 88 #[must_use] 89 pub fn name(&self) -> &str { 90 &self.name 91 } 92 93 /// Returns the opaque first-level administrative identifier. 94 #[must_use] 95 pub fn admin1_id(&self) -> Option<&str> { 96 self.admin1_id.as_deref() 97 } 98 99 /// Returns the first-level administrative name when present. 100 #[must_use] 101 pub fn admin1_name(&self) -> Option<&str> { 102 self.admin1_name.as_deref() 103 } 104 105 /// Returns the ISO-like country identifier stored by the asset. 106 #[must_use] 107 pub fn country_id(&self) -> &str { 108 &self.country_id 109 } 110 111 /// Returns the country name when present. 112 #[must_use] 113 pub fn country_name(&self) -> Option<&str> { 114 self.country_name.as_deref() 115 } 116 117 /// Returns the candidate coordinate. 118 #[must_use] 119 pub const fn point(&self) -> Point { 120 self.point 121 } 122 123 /// Returns the deterministic human-readable label. 124 #[must_use] 125 pub fn display_name(&self) -> &str { 126 &self.display_name 127 } 128 } 129 130 /// One country and its deterministic center point. 131 #[derive(Clone, Debug, PartialEq)] 132 pub struct Country { 133 id: String, 134 name: Option<String>, 135 center: Point, 136 } 137 138 impl Country { 139 pub(crate) fn from_provider_row(id: String, name: Option<String>, center: Point) -> Self { 140 Self { id, name, center } 141 } 142 143 /// Returns the country identifier stored by the asset. 144 #[must_use] 145 pub fn id(&self) -> &str { 146 &self.id 147 } 148 149 /// Returns the country name when present. 150 #[must_use] 151 pub fn name(&self) -> Option<&str> { 152 self.name.as_deref() 153 } 154 155 /// Returns the average coordinate of the country's locality rows. 156 #[must_use] 157 pub const fn center(&self) -> Point { 158 self.center 159 } 160 } 161 162 #[cfg(test)] 163 mod tests { 164 use super::{Candidate, Point}; 165 use crate::Error; 166 167 #[test] 168 fn points_enforce_finite_geographic_bounds() { 169 assert_eq!( 170 Point::new(48.4284, -123.3656), 171 Ok(Point { 172 latitude: 48.4284, 173 longitude: -123.3656, 174 }) 175 ); 176 for (latitude, longitude) in [ 177 (f64::NAN, 0.0), 178 (0.0, f64::INFINITY), 179 (-90.1, 0.0), 180 (90.1, 0.0), 181 (0.0, -180.1), 182 (0.0, 180.1), 183 ] { 184 assert_eq!(Point::new(latitude, longitude), Err(Error::InvalidPoint)); 185 } 186 } 187 188 #[test] 189 fn candidates_expose_provider_values_without_public_fields() { 190 let point = Point::new(48.4284, -123.3656).expect("valid point"); 191 let candidate = Candidate::from_provider_row( 192 617_4041, 193 "Victoria".to_owned(), 194 Some("BC".to_owned()), 195 Some("British Columbia".to_owned()), 196 "CA".to_owned(), 197 Some("Canada".to_owned()), 198 point, 199 ); 200 assert_eq!(candidate.feature_id(), 617_4041); 201 assert_eq!(candidate.name(), "Victoria"); 202 assert_eq!(candidate.admin1_id(), Some("BC")); 203 assert_eq!(candidate.admin1_name(), Some("British Columbia")); 204 assert_eq!(candidate.country_id(), "CA"); 205 assert_eq!(candidate.country_name(), Some("Canada")); 206 assert_eq!(candidate.point(), point); 207 assert_eq!( 208 candidate.display_name(), 209 "Victoria, British Columbia, Canada" 210 ); 211 } 212 }