openmeteo_rs/endpoints/
elevation.rs1use reqwest::Url;
4use serde::Deserialize;
5
6use super::url::build_endpoint_url;
7use super::validation::validate_coordinates as validate_coordinate_pair;
8use crate::json_sanitize::sanitize_open_meteo_floats;
9use crate::{Client, Error, Result};
10
11const MAX_ELEVATION_COORDINATES: usize = 100;
12
13impl Client {
14 pub async fn elevation<I>(&self, points: I) -> Result<Vec<Option<f32>>>
35 where
36 I: IntoIterator<Item = (f64, f64)>,
37 {
38 let points = points.into_iter().collect::<Vec<_>>();
39 let url = build_elevation_url(self, &points)?;
40 let body = self.execute(self.http.get(url)).await?;
41 decode_elevation_json(&body)
42 }
43}
44
45pub(crate) fn build_elevation_url(client: &Client, points: &[(f64, f64)]) -> Result<Url> {
46 validate_coordinates(points)?;
47
48 let latitude = points
49 .iter()
50 .map(|(latitude, _)| latitude.to_string())
51 .collect::<Vec<_>>()
52 .join(",");
53 let longitude = points
54 .iter()
55 .map(|(_, longitude)| longitude.to_string())
56 .collect::<Vec<_>>()
57 .join(",");
58
59 build_endpoint_url(
60 &client.elevation_base,
61 "elevation_base_url",
62 "v1/elevation",
63 client.api_key.as_deref(),
64 vec![("latitude", latitude), ("longitude", longitude)],
65 )
66}
67
68fn decode_elevation_json(bytes: &[u8]) -> Result<Vec<Option<f32>>> {
69 let sanitized = sanitize_open_meteo_floats(bytes);
70 let raw: RawElevationResponse = serde_json::from_slice(sanitized.as_ref())?;
71 Ok(raw.elevation)
72}
73
74fn validate_coordinates(points: &[(f64, f64)]) -> Result<()> {
75 if points.is_empty() {
76 return Err(Error::InvalidParam {
77 field: "coordinates",
78 reason: "set at least one coordinate pair".into(),
79 });
80 }
81
82 if points.len() > MAX_ELEVATION_COORDINATES {
83 return Err(Error::InvalidParam {
84 field: "coordinates",
85 reason: format!("must contain at most {MAX_ELEVATION_COORDINATES} coordinate pairs"),
86 });
87 }
88
89 for (latitude, longitude) in points {
90 validate_coordinate_pair(*latitude, *longitude)?;
91 }
92
93 Ok(())
94}
95
96#[derive(Debug, Deserialize)]
97struct RawElevationResponse {
98 elevation: Vec<Option<f32>>,
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn build_elevation_url_with_multiple_coordinates() {
107 let client = Client::builder()
108 .elevation_base_url("https://example.com/openmeteo?token=abc")
109 .unwrap()
110 .build()
111 .unwrap();
112
113 let url = build_elevation_url(&client, &[(52.52, 13.41), (47.3769, 8.5417)]).unwrap();
114
115 assert_eq!(
116 url.as_str(),
117 "https://example.com/openmeteo/v1/elevation?token=abc&latitude=52.52%2C47.3769&longitude=13.41%2C8.5417"
118 );
119 }
120
121 #[test]
122 fn build_elevation_url_with_api_key() {
123 let client = Client::builder()
124 .elevation_base_url("https://example.com")
125 .unwrap()
126 .api_key("secret")
127 .build()
128 .unwrap();
129
130 let url = build_elevation_url(&client, &[(52.52, 13.41)]).unwrap();
131
132 assert_eq!(
133 url.as_str(),
134 "https://example.com/v1/elevation?latitude=52.52&longitude=13.41&apikey=secret"
135 );
136 }
137
138 #[test]
139 fn rejects_empty_elevation_coordinates() {
140 let client = Client::new();
141 let err = build_elevation_url(&client, &[]).unwrap_err();
142
143 assert!(matches!(
144 err,
145 Error::InvalidParam {
146 field: "coordinates",
147 ..
148 }
149 ));
150 }
151
152 #[test]
153 fn rejects_too_many_elevation_coordinates() {
154 let client = Client::new();
155 let points = vec![(0.0, 0.0); MAX_ELEVATION_COORDINATES + 1];
156 let err = build_elevation_url(&client, &points).unwrap_err();
157
158 assert!(matches!(
159 err,
160 Error::InvalidParam {
161 field: "coordinates",
162 ..
163 }
164 ));
165 }
166
167 #[test]
168 fn rejects_invalid_elevation_coordinate() {
169 let client = Client::new();
170 let err = build_elevation_url(&client, &[(91.0, 0.0)]).unwrap_err();
171
172 assert!(matches!(
173 err,
174 Error::InvalidParam {
175 field: "latitude",
176 ..
177 }
178 ));
179 }
180
181 #[test]
182 fn decodes_elevation_json() {
183 let elevations = decode_elevation_json(br#"{"elevation":[38.0,409.0]}"#).unwrap();
184 assert_eq!(elevations, vec![Some(38.0), Some(409.0)]);
185 }
186
187 #[test]
188 fn decodes_elevation_nan_as_missing_value() {
189 let elevations = decode_elevation_json(br#"{"elevation":[0.0,nan,0.0]}"#).unwrap();
190 assert_eq!(elevations, vec![Some(0.0), None, Some(0.0)]);
191 }
192}