use {
ureq::get,
serde::Deserialize,
chrono::{NaiveDateTime, Utc},
crate::{
Event, ReportStatus,
sources::{Source, SourceInfo, SourceId, QueryPlan, opt_to_str},
DateTimeUtc, Result, Error
},
};
pub fn fixed_datetime_from_timestamp(ts: i64) -> Result<DateTimeUtc> {
Ok(DateTimeUtc::from_utc(NaiveDateTime::from_timestamp_millis(ts)
.ok_or(Error::TimeNone)?, Utc))
}
pub struct USGSSource;
impl USGSSource {
pub fn new() -> Self {
Self{}
}
}
impl SourceInfo for USGSSource {
const SOURCE_ID: SourceId = "USGS";
const LATEST_API_URL: &'static str = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson";
const HISTORY_API_URL: &'static str = "https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson";
}
fn request<'a, P>(base: &str, queries: P) -> Result<Vec<Event>>
where P: IntoIterator<Item = (&'a str, &'a str)> {
let resp = get(base)
.query_pairs(queries);
let resp = resp
.call()?
.into_string().map_err(|e| Error::InvalidResponse { source: e })?;
let raw_data: USGSRawData = serde_json::from_str(&resp)?;
let mut events: Vec<Event> = raw_data.features
.into_iter().filter_map(|i| i.try_into().ok()).collect();
events.sort_by(|a, b| b.time.cmp(&a.time));
Ok(events)
}
impl Source for USGSSource {
fn get_latest(&self) -> Result<Vec<Event>> {
request(Self::LATEST_API_URL, [])
}
fn run_query(&self, query: &QueryPlan) -> Result<Vec<Event>> {
request(Self::HISTORY_API_URL, [
("starttime", opt_to_str(query.since.map(|v| v.to_rfc3339())).as_str()),
("endtime", opt_to_str(query.until.map(|v| v.to_rfc3339())).as_str()),
("minlongitude", opt_to_str(query.min_lon).as_str()),
("maxlongitude", opt_to_str(query.max_lon).as_str()),
("minlatitude", opt_to_str(query.min_lat).as_str()),
("maxlatitude", opt_to_str(query.max_lat).as_str()),
("mindepth", opt_to_str(query.min_depth).as_str()),
("maxdepth", opt_to_str(query.max_depth).as_str()),
("minmagnitude", opt_to_str(query.min_mag).as_str()),
("maxmagnitude", opt_to_str(query.max_mag).as_str()),
("offset", (query.page_size * (query.page - 1) + 1).to_string().as_str()),
("limit", query.page_size.to_string().as_str()),
])
}
}
#[derive(Deserialize)]
struct Geometry {
coordinates: Vec<f64>,
}
#[derive(Deserialize)]
struct Properties {
mag: f64,
place: Option<String>,
time: i64,
updated: i64,
url: String,
status: String,
}
#[derive(Deserialize)]
struct USGSRawItem {
properties: Properties,
geometry: Geometry,
id: String,
}
#[derive(Deserialize)]
struct USGSRawData {
features: Vec<USGSRawItem>,
}
impl TryFrom<USGSRawItem> for Event {
type Error = Error;
fn try_from(value: USGSRawItem) -> Result<Self> {
Ok(Self{
id: value.id,
url: value.properties.url,
source: USGSSource::SOURCE_ID,
time: fixed_datetime_from_timestamp(value.properties.time)?,
updated: fixed_datetime_from_timestamp(value.properties.updated)?,
status: match value.properties.status.as_str() {
"automatic" => ReportStatus::Automatic,
"reviewed" => ReportStatus::Reviewed,
"deleted" => ReportStatus::Deleted,
_ => return Err(Error::UnexpectedValue),
},
longitude: value.geometry.coordinates[0],
latitude: value.geometry.coordinates[1],
depth: value.geometry.coordinates[2],
magnitude: value.properties.mag,
place: opt_to_str(value.properties.place),
})
}
}
#[cfg(test)]
mod test {
use crate::{Source, USGSSource, DateTimeF, Result};
#[test]
fn test_get_latest() -> Result<()> {
Ok(())
}
#[test]
fn test_query() -> Result<()> {
let since = DateTimeF::parse_from_rfc3339("2023-01-01T00:00:00Z")?.into();
let until = DateTimeF::parse_from_rfc3339("2023-01-02T00:00:00Z")?.into();
let events = USGSSource::new().query()
.since(&since).until(&until).min_mag(5.5)
.run()?;
assert_eq!(
vec!["us7000j1u6", "us7000j1g5"],
events.iter().map(|e| e.id.as_str()).collect::<Vec<&str>>()
);
Ok(())
}
}