use crate::chrono::naive::serde::ts_seconds::deserialize as from_ts;
use crate::chrono::NaiveDateTime;
use crate::DeserializeOwned;
use crate::GeocodingError;
use crate::InputBounds;
use crate::Point;
use crate::UA_STRING;
use crate::{Client, HeaderMap, HeaderValue, USER_AGENT};
use crate::{Deserialize, Serialize};
use crate::{Forward, Reverse};
use num_traits::Float;
use serde::Deserializer;
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::{Arc, Mutex};
macro_rules! add_optional_param {
($query:expr, $param:expr, $name:expr) => {
if let Some(p) = $param {
$query.push(($name, p))
}
};
}
#[derive(Default)]
pub struct Parameters<'a> {
pub language: Option<&'a str>,
pub countrycode: Option<&'a str>,
pub limit: Option<&'a str>,
}
impl<'a> Parameters<'a> {
fn as_query(&self) -> Vec<(&'a str, &'a str)> {
let mut query = vec![];
add_optional_param!(query, self.language, "language");
add_optional_param!(query, self.countrycode, "countrycode");
add_optional_param!(query, self.limit, "limit");
query
}
}
pub fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrInt {
String(String),
Int(i32),
}
match StringOrInt::deserialize(deserializer)? {
StringOrInt::String(s) => Ok(s),
StringOrInt::Int(i) => Ok(i.to_string()),
}
}
static XRL: &str = "x-ratelimit-remaining";
pub static NOBOX: Option<InputBounds<f64>> = None::<InputBounds<f64>>;
pub struct Opencage<'a> {
api_key: String,
client: Client,
endpoint: String,
pub parameters: Parameters<'a>,
remaining: Arc<Mutex<Option<i32>>>,
}
impl<'a> Opencage<'a> {
pub fn new(api_key: String) -> Self {
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static(UA_STRING));
let client = Client::builder()
.default_headers(headers)
.build()
.expect("Couldn't build a client!");
let parameters = Parameters::default();
Opencage {
api_key,
client,
parameters,
endpoint: "https://api.opencagedata.com/geocode/v1/json".to_string(),
remaining: Arc::new(Mutex::new(None)),
}
}
pub fn remaining_calls(&self) -> Option<i32> {
*self.remaining.lock().unwrap()
}
pub fn reverse_full<T>(&self, point: &Point<T>) -> Result<OpencageResponse<T>, GeocodingError>
where
T: Float + DeserializeOwned + Debug,
{
let q = format!(
"{}, {}",
(&point.y().to_f64().unwrap().to_string()),
&point.x().to_f64().unwrap().to_string()
);
let mut query = vec![
("q", q.as_str()),
("key", &self.api_key),
("no_annotations", "0"),
("no_record", "1"),
];
query.extend(self.parameters.as_query());
let resp = self
.client
.get(&self.endpoint)
.query(&query)
.send()?
.error_for_status()?;
if let Some(headers) = resp.headers().get::<_>(XRL) {
let mut lock = self.remaining.try_lock();
if let Ok(ref mut mutex) = lock {
let h = headers.to_str()?;
let h: i32 = h.parse()?;
**mutex = Some(h)
}
}
let res: OpencageResponse<T> = resp.json()?;
Ok(res)
}
pub fn forward_full<T, U>(
&self,
place: &str,
bounds: U,
) -> Result<OpencageResponse<T>, GeocodingError>
where
T: Float + DeserializeOwned + Debug,
U: Into<Option<InputBounds<T>>>,
{
let ann = String::from("0");
let record = String::from("1");
let bd;
let mut query = vec![
("q", place),
("key", &self.api_key),
("no_annotations", &ann),
("no_record", &record),
];
if let Some(bds) = bounds.into() {
bd = String::from(bds);
query.push(("bounds", &bd));
}
query.extend(self.parameters.as_query());
let resp = self
.client
.get(&self.endpoint)
.query(&query)
.send()?
.error_for_status()?;
if let Some(headers) = resp.headers().get::<_>(XRL) {
let mut lock = self.remaining.try_lock();
if let Ok(ref mut mutex) = lock {
let h = headers.to_str()?;
let h: i32 = h.parse()?;
**mutex = Some(h)
}
}
let res: OpencageResponse<T> = resp.json()?;
Ok(res)
}
}
impl<'a, T> Reverse<T> for Opencage<'a>
where
T: Float + DeserializeOwned + Debug,
{
fn reverse(&self, point: &Point<T>) -> Result<Option<String>, GeocodingError> {
let q = format!(
"{}, {}",
(&point.y().to_f64().unwrap().to_string()),
&point.x().to_f64().unwrap().to_string()
);
let mut query = vec![
("q", q.as_str()),
("key", &self.api_key),
("no_annotations", "1"),
("no_record", "1"),
];
query.extend(self.parameters.as_query());
let resp = self
.client
.get(&self.endpoint)
.query(&query)
.send()?
.error_for_status()?;
if let Some(headers) = resp.headers().get::<_>(XRL) {
let mut lock = self.remaining.try_lock();
if let Ok(ref mut mutex) = lock {
let h = headers.to_str()?;
let h: i32 = h.parse()?;
**mutex = Some(h)
}
}
let res: OpencageResponse<T> = resp.json()?;
let address = &res.results[0];
Ok(Some(address.formatted.to_string()))
}
}
impl<'a, T> Forward<T> for Opencage<'a>
where
T: Float + DeserializeOwned + Debug,
{
fn forward(&self, place: &str) -> Result<Vec<Point<T>>, GeocodingError> {
let mut query = vec![
("q", place),
("key", &self.api_key),
("no_annotations", "1"),
("no_record", "1"),
];
query.extend(self.parameters.as_query());
let resp = self
.client
.get(&self.endpoint)
.query(&query)
.send()?
.error_for_status()?;
if let Some(headers) = resp.headers().get::<_>(XRL) {
let mut lock = self.remaining.try_lock();
if let Ok(ref mut mutex) = lock {
let h = headers.to_str()?;
let h: i32 = h.parse()?;
**mutex = Some(h)
}
}
let res: OpencageResponse<T> = resp.json()?;
Ok(res
.results
.iter()
.map(|res| Point::new(res.geometry["lng"], res.geometry["lat"]))
.collect())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OpencageResponse<T>
where
T: Float,
{
pub documentation: String,
pub licenses: Vec<HashMap<String, String>>,
pub rate: Option<HashMap<String, i32>>,
pub results: Vec<Results<T>>,
pub status: Status,
pub stay_informed: HashMap<String, String>,
pub thanks: String,
pub timestamp: Timestamp,
pub total_results: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Results<T>
where
T: Float,
{
pub annotations: Option<Annotations<T>>,
pub bounds: Option<Bounds<T>>,
pub components: HashMap<String, serde_json::Value>,
pub confidence: i8,
pub formatted: String,
pub geometry: HashMap<String, T>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Annotations<T>
where
T: Float,
{
pub dms: Option<HashMap<String, String>>,
pub mgrs: Option<String>,
pub maidenhead: Option<String>,
pub mercator: Option<HashMap<String, T>>,
pub osm: Option<HashMap<String, String>>,
pub callingcode: i16,
pub currency: Option<Currency>,
pub flag: String,
pub geohash: String,
pub qibla: T,
pub sun: Sun,
pub timezone: Timezone,
pub what3words: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Currency {
pub alternate_symbols: Option<Vec<String>>,
pub decimal_mark: String,
pub html_entity: String,
pub iso_code: String,
#[serde(deserialize_with = "deserialize_string_or_int")]
pub iso_numeric: String,
pub name: String,
pub smallest_denomination: i16,
pub subunit: String,
pub subunit_to_unit: i16,
pub symbol: String,
pub symbol_first: i16,
pub thousands_separator: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Sun {
pub rise: HashMap<String, i64>,
pub set: HashMap<String, i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Timezone {
pub name: String,
pub now_in_dst: i16,
pub offset_sec: i32,
#[serde(deserialize_with = "deserialize_string_or_int")]
pub offset_string: String,
#[serde(deserialize_with = "deserialize_string_or_int")]
pub short_name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Status {
pub message: String,
pub code: i16,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Timestamp {
pub created_http: String,
#[serde(deserialize_with = "from_ts")]
pub created_unix: NaiveDateTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bounds<T>
where
T: Float,
{
pub northeast: HashMap<String, T>,
pub southwest: HashMap<String, T>,
}
#[cfg(test)]
mod test {
use super::*;
use crate::Coordinate;
#[test]
fn reverse_test() {
let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
let p = Point::new(2.12870, 41.40139);
let res = oc.reverse(&p);
assert_eq!(
res.unwrap(),
Some("Carrer de Calatrava, 68, 08017 Barcelona, Spain".to_string())
);
}
#[test]
fn reverse_test_with_params() {
let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
oc.parameters.language = Some("fr");
let p = Point::new(2.12870, 41.40139);
let res = oc.reverse(&p);
assert_eq!(
res.unwrap(),
Some("Carrer de Calatrava, 68, 08017 Barcelone, Espagne".to_string())
);
}
#[test]
fn forward_test() {
let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
let address = "Schwabing, München";
let res = oc.forward(&address);
assert_eq!(
res.unwrap(),
vec![Point(Coordinate {
x: 11.5884858,
y: 48.1700887
})]
);
}
#[test]
fn reverse_full_test() {
let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
oc.parameters.language = Some("fr");
let p = Point::new(2.12870, 41.40139);
let res = oc.reverse_full(&p).unwrap();
let first_result = &res.results[0];
assert_eq!(first_result.components["road"], "Carrer de Calatrava");
}
#[test]
fn forward_full_test() {
let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
let address = "UCL CASA";
let bbox = InputBounds {
minimum_lonlat: Point::new(-0.13806939125061035, 51.51989264641164),
maximum_lonlat: Point::new(-0.13427138328552246, 51.52319711775629),
};
let res = oc.forward_full(&address, bbox).unwrap();
let first_result = &res.results[0];
assert!(first_result.formatted.contains("UCL"));
}
#[test]
fn forward_full_test_floats() {
let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
let address = "UCL CASA";
let bbox = InputBounds::new(
Point::new(-0.13806939125061035, 51.51989264641164),
Point::new(-0.13427138328552246, 51.52319711775629),
);
let res = oc.forward_full(&address, bbox).unwrap();
let first_result = &res.results[0];
assert!(first_result
.formatted
.contains("UCL, 188 Tottenham Court Road"));
}
#[test]
fn forward_full_test_pointfrom() {
let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
let address = "UCL CASA";
let bbox = InputBounds::new(
Point::from((-0.13806939125061035, 51.51989264641164)),
Point::from((-0.13427138328552246, 51.52319711775629)),
);
let res = oc.forward_full(&address, bbox).unwrap();
let first_result = &res.results[0];
assert!(first_result
.formatted
.contains("UCL, 188 Tottenham Court Road"));
}
#[test]
fn forward_full_test_pointinto() {
let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
let address = "UCL CASA";
let bbox = InputBounds::new(
(-0.13806939125061035, 51.51989264641164),
(-0.13427138328552246, 51.52319711775629),
);
let res = oc.forward_full(&address, bbox).unwrap();
let first_result = &res.results[0];
assert!(first_result
.formatted
.contains("Tottenham Court Road, London"));
}
#[test]
fn forward_full_test_nobox() {
let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string());
let address = "Moabit, Berlin, Germany";
let res = oc.forward_full(&address, NOBOX).unwrap();
let first_result = &res.results[0];
assert_eq!(first_result.formatted, "Moabit, Berlin, Germany");
}
}