dizhen 0.1.0

Library to retrieve seismic data
Documentation
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))
}

/** United States Geological Survey data source.
 *
 * Limit max 20000 records per request.
 *
 * Raw data are in GeoJSON, which we for simplicity just hand wrote a
 * Deserialize conversion.
 */
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>>
// ureq::Request::query_pairs
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,
	//ids: 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,
			// returned result is in milliseconds
			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<()> {
		//USGSSource::new().get_latest()?;
		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(())
	}
}