coordinate_altitude/
lib.rs1use std::{fs::File, io::Write, path::PathBuf};
38
39use serde::{Deserialize, Serialize};
40
41pub type Res<T> = Result<T, Box<dyn std::error::Error>>;
43
44#[derive(Clone, Copy, PartialEq, Default, Debug, Serialize, Deserialize)]
46pub struct Coord {
47 pub latitude: f64,
49 pub longitude: f64,
51 #[serde(alias = "elevation")]
53 pub altitude: f64,
55}
56impl Coord {
57 pub fn new<F1: Into<f64>, F2: Into<f64>>(latitude: F1, longitude: F2) -> Self {
58 let latitude: f64 = latitude.into();
59 let longitude: f64 = longitude.into();
60
61 assert!((-90. ..=90.).contains(&latitude) && (-180. ..=180.).contains(&longitude));
62
63 Self {
64 latitude,
65 longitude,
66 altitude: 0.,
67 }
68 }
69 pub fn with_altitude<F: Into<f64>>(&self, altitude: F) -> Self {
71 Self {
72 altitude: altitude.into(),
73 ..*self
74 }
75 }
76
77 fn get_form(&self) -> String {
79 format!("{},{}", self.latitude, self.longitude)
80 }
81
82 pub fn fetch_altitude(&self) -> Option<Self> {
84 fetch_altitude(&[*self]).ok()?.first().copied()
85 }
86
87 pub fn add_altitude(&mut self) -> Res<()> {
89 let mut with_altitude = [*self];
90 add_altitude(&mut with_altitude)?;
91 self.altitude = with_altitude[0].altitude;
92
93 Ok(())
94 }
95}
96
97fn cache_path() -> PathBuf {
99 let cache_dir = dirs::cache_dir().unwrap().join("coordinate-altitude");
100 if !cache_dir.exists() {
101 std::fs::create_dir_all(&cache_dir).unwrap();
102 }
103 cache_dir.join("cache.json")
104}
105
106fn load_cache() -> Vec<Coord> {
108 let cache_content = std::fs::read_to_string(cache_path()).unwrap_or_default();
109 serde_json::from_str::<Vec<_>>(&cache_content)
112 .inspect_err(|e| eprintln!("parse error: {e:#?}"))
113 .unwrap_or_default()
114}
115
116fn save_cache(coords: &[Coord]) -> Res<()> {
118 let data =
119 serde_json::to_string(coords).inspect_err(|e| eprintln!("serialization error: {e:#?}"))?;
120 let mut cache_file = File::create(cache_path())?;
121 cache_file.write_all(data.as_bytes())?;
122 Ok(())
123}
124
125pub fn fetch_altitude(coords: &[Coord]) -> Res<Vec<Coord>> {
133 let mut cached_altitude_data = load_cache();
134
135 let mut cached_needed = Vec::new();
137 let mut to_fetch = Vec::new();
139 for coord in coords {
141 if let Some(cached) = cached_altitude_data.iter().find(|cached| {
143 let rounded_lat = (coord.latitude * 1000000.).round() / 1000000.; let rounded_lon = (coord.longitude * 1000000.).round() / 1000000.; cached.latitude == rounded_lat && cached.longitude == rounded_lon
146 }) {
147 cached_needed.push(*cached);
149 } else {
150 to_fetch.push(*coord);
152 }
153 }
154 if to_fetch.is_empty() {
159 return Ok(cached_needed);
160 }
161 let res = if let Some(got_resp) = fetch_altitude_get(&to_fetch) {
162 got_resp
163 } else {
164 fetch_altitude_post(&to_fetch)?
165 };
166 let res = &res[11..];
168 let res = &res[0..res.len() - 1];
170 let fetched =
173 serde_json::from_str::<Vec<_>>(res).inspect_err(|e| eprintln!("parse error: {e:#?}"))?;
174 cached_altitude_data.extend_from_slice(&fetched);
178 save_cache(&cached_altitude_data)?;
180
181 Ok([cached_needed, fetched].concat())
183}
184
185pub fn add_altitude(coords: &mut [Coord]) -> Res<()> {
187 let coords_with_altitude_data = fetch_altitude(coords)?;
188 for (i, coord) in coords.iter_mut().enumerate() {
189 coord.altitude = coords_with_altitude_data[i].altitude;
190 }
191 Ok(())
192}
193
194fn fetch_altitude_get(coords: &[Coord]) -> Option<String> {
196 let mut form = coords
198 .iter()
199 .fold(String::new(), |sum, cnt| sum + &cnt.get_form() + "|");
200 form.pop();
202 if form.as_bytes().len() > 1024 {
204 return None;
205 }
206 ureq::get("https://api.open-elevation.com/api/v1/lookup")
208 .query("locations", &form)
209 .call()
210 .inspect_err(|e| eprintln!("fetch error: {e:#?}"))
211 .ok()?
212 .into_string()
213 .ok()
214}
215
216fn fetch_altitude_post(coords: &[Coord]) -> Res<String> {
218 let data =
219 serde_json::to_string(coords).inspect_err(|e| eprintln!("serialization error: {e:#?}"))?;
220 let data = format!("{{\"locations\":{data}}}");
221 let res = ureq::post("https://api.open-elevation.com/api/v1/lookup")
224 .set("Accept", "application/json")
225 .set("Content-Type", "application/json")
226 .send_bytes(data.as_bytes())
227 .inspect_err(|e| eprintln!("fetch error: {e:#?}"))?
228 .into_string()?;
229 Ok(res)
230}
231
232impl<F1: Into<f64>, F2: Into<f64>> From<(F1, F2)> for Coord {
233 fn from(val: (F1, F2)) -> Self {
234 Self::new(val.0, val.1)
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn coord_new() {
244 let result = Coord::new(44., 2);
245 assert_eq!(
246 result,
247 Coord {
248 latitude: 44.,
249 longitude: 2.,
250 altitude: 0.
251 }
252 );
253 }
254 #[test]
255 fn coord_default() {
256 let result = Coord::default();
257 assert_eq!(
258 result,
259 Coord {
260 latitude: 0.,
261 longitude: 0.,
262 altitude: 0.
263 }
264 )
265 }
266
267 #[test]
268 fn coord_deser() {
269 let json = r#"{
270 "latitude": 32.2643,
271 "longitude": 20.333,
272 "elevation": 354
273 }"#;
274 let result = serde_json::from_str::<Coord>(json).unwrap();
275 assert_eq!(result, Coord::new(32.2643, 20.333).with_altitude(354));
276 }
277 #[test]
278 fn coord_ser() {
279 let coord = Coord::new(32.2643, 20.333).with_altitude(354);
280 let result = serde_json::to_string(&coord).unwrap();
281 let json = r#"{"latitude":32.2643,"longitude":20.333,"altitude":354.0}"#;
282 assert_eq!(result, json);
283 }
284
285 #[test]
286 fn locations_deser() {
287 let json = r#"
288 [
289 {
290 "latitude": 10,
291 "longitude": -10,
292 "altitude": 21
293 },
294 {
295 "latitude":20.3453,
296 "longitude": 28,
297 "elevation": 32
298 },
299 {
300 "latitude":41.161758,
301 "longitude":-8.583933,
302 "altitude":3798
303 }
304 ]"#;
305 let result = serde_json::from_str::<Vec<Coord>>(json).unwrap();
306 assert_eq!(
307 result,
308 vec![
309 Coord::new(10, -10).with_altitude(21),
310 Coord::new(20.3453, 28).with_altitude(32),
311 Coord::new(41.161758, -8.583933).with_altitude(3798),
312 ] );
314 }
315 #[test]
316 fn locations_ser() {
317 let json = r#"[{"latitude":10.0,"longitude":-10.0,"altitude":21.214},{"latitude":20.3453,"longitude":28.0,"altitude":32.0},{"latitude":41.161758,"longitude":-8.583933,"altitude":0.0}]"#;
318 let locations = vec![
319 Coord::new(10, -10).with_altitude(21.214),
320 Coord::new(20.3453, 28).with_altitude(32),
321 Coord::new(41.161758, -8.583933),
322 ];
323
324 let result = serde_json::to_string(&locations).unwrap();
325 assert_eq!(result, json);
326 }
327}