coordinate_altitude/
lib.rs

1//! Get altitude/elevation data for geographical coordinates of planet Earth
2//!
3//! # WARNING
4//!
5//! every latitude, longitude data will be rounded to 6 decimal places accuracy by [open-elevation api](https://open-elevation.com)
6//!
7//! # Usage
8//!
9//! ```rust
10//! use coordinate_altitude::*;
11//!
12//! // fetch altitude for a single `Coord`
13//! // could also be a tuple for example, and later converted into Coord
14//! // let coord: (f64, f64) = (34.324, 1.88832);
15//! // let coord: Coord = coord.into();
16//! // coordinate as a `Coord`
17//! let coord = Coord::new(34.324, 1.88832);
18//! // and finally fetch altitude for `coord`
19//! let coord: Option<Coord> = coord.fetch_altitude();
20//! println!("coordinate: {coord:?}");
21//!
22//!
23//! // add altitude for a `Vec<Coord>`
24//! let mut coords: Vec<Coord> = vec![   
25//!     (58.2926289, 134.3025286).into(),   // Sheep Mountain
26//!     (7.4894883, 80.8144869).into(),     // Sri Lanka
27//!     Coord::new(47.0745464, 12.6938825), // Großglockner
28//! ];
29//! add_altitude(&mut coords);
30//! println!("coordinates: {coords:#?}");
31//! ```
32//!
33//! - you could also add altitude for a single `Coord`
34//! - or fetch altitude for a `Vec<Coord>`
35//! - you can easily `impl From<YourWayOfStoringCoordinates> for Coord`, see tests/integ.rs
36
37use std::{fs::File, io::Write, path::PathBuf};
38
39use serde::{Deserialize, Serialize};
40
41/// universal error type
42pub type Res<T> = Result<T, Box<dyn std::error::Error>>;
43
44/// a geographical coordinate of planet Earth
45#[derive(Clone, Copy, PartialEq, Default, Debug, Serialize, Deserialize)]
46pub struct Coord {
47    /// y
48    pub latitude: f64,
49    /// x
50    pub longitude: f64,
51    /// elevation above sea-level
52    #[serde(alias = "elevation")]
53    // #[serde(alias = "elevation", skip_serializing)]
54    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    /// create new [`Coord`] from `self` with `altitude` added
70    pub fn with_altitude<F: Into<f64>>(&self, altitude: F) -> Self {
71        Self {
72            altitude: altitude.into(),
73            ..*self
74        }
75    }
76
77    /// get in style that's required for GET form
78    fn get_form(&self) -> String {
79        format!("{},{}", self.latitude, self.longitude)
80    }
81
82    /// create new [`Coord`] from `self` with altitude fetched if any
83    pub fn fetch_altitude(&self) -> Option<Self> {
84        fetch_altitude(&[*self]).ok()?.first().copied()
85    }
86
87    /// fetch altitude and add to `self`
88    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
97/// cache file path
98fn 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
106/// load cached data into a `Vec<Coord>` if any error occurs: `vec![]`
107fn load_cache() -> Vec<Coord> {
108    let cache_content = std::fs::read_to_string(cache_path()).unwrap_or_default();
109    // eprintln!("cache: {cache_content:?}");
110
111    serde_json::from_str::<Vec<_>>(&cache_content)
112        .inspect_err(|e| eprintln!("parse error: {e:#?}"))
113        .unwrap_or_default()
114}
115
116/// save these `coords` to cache
117fn 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
125/// # Usage
126/// ```rust
127/// use coordinate_altitude::*;
128/// let coords: Vec<Coord> = vec![(34.23, 32).into(), (8.87354, 67.124).into()];
129/// let coords: Res<Vec<Coord>> = fetch_altitude(&coords);
130/// println!("coordinates: {coords:?}");
131/// ```
132pub fn fetch_altitude(coords: &[Coord]) -> Res<Vec<Coord>> {
133    let mut cached_altitude_data = load_cache();
134
135    // the ones we need and are already cached
136    let mut cached_needed = Vec::new();
137    // the ones not cached
138    let mut to_fetch = Vec::new();
139    // eprintln!("to get: {coords:#?}");
140    for coord in coords {
141        // eprintln!("coord: {coord:?}");
142        if let Some(cached) = cached_altitude_data.iter().find(|cached| {
143            let rounded_lat = (coord.latitude * 1000000.).round() / 1000000.; // rounding to 6 decimal, as https://open-elevation.com is this accurate
144            let rounded_lon = (coord.longitude * 1000000.).round() / 1000000.; // rounding to 6 decimal, as https://open-elevation.com is this accurate
145            cached.latitude == rounded_lat && cached.longitude == rounded_lon
146        }) {
147            // cached
148            cached_needed.push(*cached);
149        } else {
150            // to be fetched
151            to_fetch.push(*coord);
152        }
153    }
154    // eprintln!("cached: {cached_altitude_data:#?}");
155    // eprintln!("to fetch: {to_fetch:#?}");
156
157    // don't fetch if there's nothing to fetch
158    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    // leading: ""results": {"
167    let res = &res[11..];
168    // trailing: "}"
169    let res = &res[0..res.len() - 1];
170    // eprintln!("response: {res:?}");
171
172    let fetched =
173        serde_json::from_str::<Vec<_>>(res).inspect_err(|e| eprintln!("parse error: {e:#?}"))?;
174    // eprintln!("fetched: {fetched:#?}");
175
176    // fetched is added to cached to be cached later
177    cached_altitude_data.extend_from_slice(&fetched);
178    // eprintln!("cached: {cached_altitude_data:#?}");
179    save_cache(&cached_altitude_data)?;
180
181    // the ones we need: from cache and fetched too
182    Ok([cached_needed, fetched].concat())
183}
184
185/// add altitude to existing `coords`
186pub 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
194/// fetch altitude with GET method
195fn fetch_altitude_get(coords: &[Coord]) -> Option<String> {
196    // eg: "83.32,38.2|21.23,128.534|"
197    let mut form = coords
198        .iter()
199        .fold(String::new(), |sum, cnt| sum + &cnt.get_form() + "|");
200    // trailing '|'
201    form.pop();
202    // GET api doesn't support bigger than this
203    if form.as_bytes().len() > 1024 {
204        return None;
205    }
206    // eprintln!("sending: {form:?}");
207    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
216/// fetch altitude with POST method
217fn 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    // eprintln!("sending: {data:?}");
222
223    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            ] // .into()
313        );
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}