usgs_earthquake_api/
lib.rs

1//! # Usgs Earthquake API
2//!
3//! This crate provides a simple client for interacting with the
4//! [USGS Earthquake API](https://earthquake.usgs.gov/fdsnws/event/1/).
5//!
6//! Features:
7//! - Filter earthquakes by time range (`start_time`, `end_time`)
8//! - Filter by magnitude range (`min_magnitude`, `max_magnitude`)
9//! - Filter by alert level (`AlertLevel`)
10//! - Order results (`OrderBy`)
11//! - Filter earthquakes by country code (using `country_boundaries` dataset).
12//!
13//! ## Example
14//! ```rust,no_run
15//! use usgs_client::{UsgsClient, AlertLevel, OrderBy};
16//!
17//! #[tokio::main]
18//! async fn main() {
19//!     use usgs_earthquake_api::OrderBy;
20//! let client = UsgsClient::new();
21//!     let result = client
22//!         .query()
23//!         .filter_by_country_code("TR")
24//!         .start_time(2024, 1, 1, 0, 0)
25//!         .end_time(2024, 12, 31, 23, 59)
26//!         .min_magnitude(4.0)
27//!         .alert_level(AlertLevel::All)
28//!         .order_by(OrderBy::Time)
29//!         .fetch()
30//!         .await;
31//!
32//!     match result {
33//!         Ok(res) => println!("Total earthquakes: {}", res.features.len()),
34//!         Err(e) => eprintln!("Error: {}", e),
35//!     }
36//! }
37//! ```
38
39mod error;
40mod models;
41
42use std::fmt::Display;
43use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
44use country_boundaries::{CountryBoundaries, LatLon, BOUNDARIES_ODBL_360X180};
45use reqwest::Client;
46use error::error::UsgsError;
47use crate::models::models::{EarthquakeResponse, EarthquakeFeatures};
48
49fn local_time_as_utc() -> NaiveDateTime {
50	Utc::now().naive_utc()
51}
52
53fn local_time_to_utc(time: NaiveDateTime) -> NaiveDateTime {
54	let timezone = Local.from_local_datetime(&time).unwrap();
55	let utc = timezone.with_timezone(&Utc);
56	println!("{}", utc.naive_utc().to_string());
57	utc.naive_utc()
58}
59
60fn generate_custom_time(year: i32, month: u32, day: u32, hour: u32, min: u32) -> NaiveDateTime {
61	let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
62	let time = NaiveTime::from_hms_opt(hour, min, 00).unwrap();
63	NaiveDateTime::new(date, time)
64}
65
66
67/// USGS earthquake alert levels.
68#[derive(Debug)]
69pub enum AlertLevel {
70	/// Low alert level
71	Green,
72
73	/// Moderate alert level
74	Yellow,
75
76	/// High alert level
77	Orange,
78
79	/// Very high alert level
80	Red,
81
82	/// All alert levels
83	All
84}
85
86pub enum OrderBy {
87	/// Order by time descending
88	Time,
89
90	/// Order by time ascending
91	TimeAsc,
92
93	/// Order by magnitude descending
94	Magnitude,
95
96	/// Order by magnitude ascending
97	MagnitudeAsc
98}
99
100
101/// Main USGS API client.
102///
103/// Handles API requests and creates queries.
104pub struct UsgsClient {
105	/// Base URL of the USGS API
106	pub base_url: String,
107
108	/// HTTP client
109	pub client: Client,
110}
111
112
113impl UsgsClient {
114	/// Creates a new [`UsgsClient`].
115	pub fn new() -> Self {
116		Self {
117			base_url: "https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson".to_string(),
118			client: Client::new(),
119		}
120	}
121
122	/// Starts a new [`UsgsQuery`] with default parameters.
123	pub fn query(&self) -> UsgsQuery<'_> {
124		UsgsQuery {
125			client: &self.client,
126			base_url: self.base_url.clone(),
127			country_code: "US".to_string(),
128			start_time: None,
129			end_time: local_time_as_utc(),
130			min_magnitude: 0.0,
131			max_magnitude: 10.0,
132			alert_level: AlertLevel::All,
133			order_by: OrderBy::Time,
134		}
135	}
136}
137
138/// Query builder for the USGS API.
139///
140/// Allows filtering and customizing request parameters.
141pub struct UsgsQuery<'a> {
142	client: & 'a Client,
143	base_url: String,
144	country_code: String,
145	start_time: Option<NaiveDateTime>,
146	end_time: NaiveDateTime,
147	min_magnitude: f32,
148	max_magnitude: f32,
149	alert_level: AlertLevel,
150	order_by: OrderBy,
151}
152
153//TODO: Add other queries from USGS API document.
154impl<'a> UsgsQuery<'a> {
155
156	/// Filters earthquakes by country code (e.g., `"TR"`, `"US"`).
157	pub fn filter_by_country_code(mut self, country_code: &str) -> Self {
158		self.country_code = country_code.to_string();
159		self
160	}
161
162	/// Sets the start time for the query.
163	pub fn start_time(mut self, year: i32, month: u32, day: u32, hour: u32, min: u32) -> Self {
164		self.start_time =  Some(local_time_to_utc(generate_custom_time(year, month, day, hour, min)));
165		self
166	}
167
168	/// Sets the end time for the query.
169	pub fn end_time(mut self, year: i32, month: u32, day: u32, hour: u32, min: u32) -> Self {
170		self.end_time = local_time_to_utc(generate_custom_time(year, month, day, hour, min));
171		self
172	}
173
174	/// Sets the minimum magnitude filter.
175	pub fn min_magnitude(mut self, min: f32) -> Self {
176		self.min_magnitude = min;
177		self
178	}
179
180	/// Sets the maximum magnitude filter.
181	pub fn max_magnitude(mut self, max: f32) -> Self {
182		self.max_magnitude = max;
183		self
184	}
185
186	/// Sets the alert level filter.
187	pub fn alert_level(mut self, level: AlertLevel) -> Self {
188		self.alert_level = level;
189		self
190	}
191
192	/// Sets the ordering method for the query.
193	pub fn order_by(mut self, order_by: OrderBy) -> Self {
194		self.order_by = order_by;
195		self
196	}
197
198	/// Executes the query against the USGS API.
199	///
200	/// # Returns
201	/// `Result<EarthquakeResponse, UsgsError>`
202	pub async fn fetch(self) -> Result<EarthquakeResponse, UsgsError> {
203
204		if self.start_time.is_none() {
205			return Err(UsgsError::EmptyStartTime)
206		}
207
208		let start_time = self.start_time.unwrap();
209
210		if start_time > self.end_time {
211			return Err(UsgsError::InvalidStartTime);
212		}
213
214		if start_time > local_time_as_utc() {
215			return Err(UsgsError::StartTimeInFuture)
216		}
217		
218		if self.min_magnitude < 0.0 {
219			return Err(UsgsError::MinimumMagnitude)
220		}
221		
222		if self.max_magnitude > 10.0 {
223			return Err(UsgsError::MaximumMagnitude)
224		}
225
226
227		let mut url = format!("{}&starttime={}&endtime={}&minmagnitude={}&maxmagnitude={}&alertlevel={}&orderby={}"
228		                     ,self.base_url, start_time, self.end_time, self.min_magnitude, self.max_magnitude, self.alert_level.to_string(), self.order_by.to_string());
229
230		if self.alert_level.to_string() == "all" {
231			url = format!("{}&starttime={}&endtime={}&minmagnitude={}&maxmagnitude={}&orderby={}"
232			                  ,self.base_url, start_time.and_utc(), self.end_time, self.min_magnitude, self.max_magnitude, self.order_by.to_string());
233		}
234
235		let response = self.client.get(&url).send().await?;
236		let mut body: EarthquakeResponse = response.json().await?;
237		if !self.country_code.is_empty() {
238			let boundaries = CountryBoundaries::from_reader(BOUNDARIES_ODBL_360X180).expect("Failed to parse BOUNDARIES_ODBL_360X180");
239			let target_code = &self.country_code;
240			let filtered_features: Vec<EarthquakeFeatures> = body.features.into_iter()
241				.filter(|eq| {
242					let coordinates = &eq.geometry.coordinates;
243					let lon = coordinates[0] as f64;
244					let lat = coordinates[1] as f64;
245					let country_codes = boundaries.ids(LatLon::new(lat, lon).expect("Failed to parse LatLon"));
246					country_codes.contains(&&**target_code)
247				})
248			.collect();
249
250			body.features = filtered_features;
251			body.metadata.count = body.features.len() as u32;
252		}
253		Ok(body)
254
255	}
256}
257
258impl Display for AlertLevel {
259	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260		let level = match self {
261			AlertLevel::Green => "green",
262			AlertLevel::Yellow => "yellow",
263			AlertLevel::Orange => "orange",
264			AlertLevel::Red => "red",
265			AlertLevel::All => "all"
266		};
267		write!(f, "{}", level)
268	}
269}
270
271
272impl Display for OrderBy {
273	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274		let s = match self {
275			OrderBy::Time => "time",
276			OrderBy::TimeAsc => "time-asc",
277			OrderBy::Magnitude => "magnitude",
278			OrderBy::MagnitudeAsc => "magnitude-asc",
279		};
280		write!(f, "{}", s)
281	}
282}