use reqwest::Url;
use serde::Deserialize;
use super::url::build_endpoint_url;
use super::validation::validate_coordinates as validate_coordinate_pair;
use crate::json_sanitize::sanitize_open_meteo_floats;
use crate::{Client, Error, Result};
const MAX_ELEVATION_COORDINATES: usize = 100;
impl Client {
pub async fn elevation<I>(&self, points: I) -> Result<Vec<Option<f32>>>
where
I: IntoIterator<Item = (f64, f64)>,
{
let points = points.into_iter().collect::<Vec<_>>();
let url = build_elevation_url(self, &points)?;
let body = self.execute(self.http.get(url)).await?;
decode_elevation_json(&body)
}
}
pub(crate) fn build_elevation_url(client: &Client, points: &[(f64, f64)]) -> Result<Url> {
validate_coordinates(points)?;
let latitude = points
.iter()
.map(|(latitude, _)| latitude.to_string())
.collect::<Vec<_>>()
.join(",");
let longitude = points
.iter()
.map(|(_, longitude)| longitude.to_string())
.collect::<Vec<_>>()
.join(",");
build_endpoint_url(
&client.elevation_base,
"elevation_base_url",
"v1/elevation",
client.api_key.as_deref(),
vec![("latitude", latitude), ("longitude", longitude)],
)
}
fn decode_elevation_json(bytes: &[u8]) -> Result<Vec<Option<f32>>> {
let sanitized = sanitize_open_meteo_floats(bytes);
let raw: RawElevationResponse = serde_json::from_slice(sanitized.as_ref())?;
Ok(raw.elevation)
}
fn validate_coordinates(points: &[(f64, f64)]) -> Result<()> {
if points.is_empty() {
return Err(Error::InvalidParam {
field: "coordinates",
reason: "set at least one coordinate pair".into(),
});
}
if points.len() > MAX_ELEVATION_COORDINATES {
return Err(Error::InvalidParam {
field: "coordinates",
reason: format!("must contain at most {MAX_ELEVATION_COORDINATES} coordinate pairs"),
});
}
for (latitude, longitude) in points {
validate_coordinate_pair(*latitude, *longitude)?;
}
Ok(())
}
#[derive(Debug, Deserialize)]
struct RawElevationResponse {
elevation: Vec<Option<f32>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_elevation_url_with_multiple_coordinates() {
let client = Client::builder()
.elevation_base_url("https://example.com/openmeteo?token=abc")
.unwrap()
.build()
.unwrap();
let url = build_elevation_url(&client, &[(52.52, 13.41), (47.3769, 8.5417)]).unwrap();
assert_eq!(
url.as_str(),
"https://example.com/openmeteo/v1/elevation?token=abc&latitude=52.52%2C47.3769&longitude=13.41%2C8.5417"
);
}
#[test]
fn build_elevation_url_with_api_key() {
let client = Client::builder()
.elevation_base_url("https://example.com")
.unwrap()
.api_key("secret")
.build()
.unwrap();
let url = build_elevation_url(&client, &[(52.52, 13.41)]).unwrap();
assert_eq!(
url.as_str(),
"https://example.com/v1/elevation?latitude=52.52&longitude=13.41&apikey=secret"
);
}
#[test]
fn rejects_empty_elevation_coordinates() {
let client = Client::new();
let err = build_elevation_url(&client, &[]).unwrap_err();
assert!(matches!(
err,
Error::InvalidParam {
field: "coordinates",
..
}
));
}
#[test]
fn rejects_too_many_elevation_coordinates() {
let client = Client::new();
let points = vec![(0.0, 0.0); MAX_ELEVATION_COORDINATES + 1];
let err = build_elevation_url(&client, &points).unwrap_err();
assert!(matches!(
err,
Error::InvalidParam {
field: "coordinates",
..
}
));
}
#[test]
fn rejects_invalid_elevation_coordinate() {
let client = Client::new();
let err = build_elevation_url(&client, &[(91.0, 0.0)]).unwrap_err();
assert!(matches!(
err,
Error::InvalidParam {
field: "latitude",
..
}
));
}
#[test]
fn decodes_elevation_json() {
let elevations = decode_elevation_json(br#"{"elevation":[38.0,409.0]}"#).unwrap();
assert_eq!(elevations, vec![Some(38.0), Some(409.0)]);
}
#[test]
fn decodes_elevation_nan_as_missing_value() {
let elevations = decode_elevation_json(br#"{"elevation":[0.0,nan,0.0]}"#).unwrap();
assert_eq!(elevations, vec![Some(0.0), None, Some(0.0)]);
}
}