use {
std::str::FromStr,
ureq::get,
chrono::FixedOffset,
serde::Deserialize,
once_cell::sync::Lazy,
crate::{
Event, ReportStatus,
sources::{Source, SourceInfo, SourceId, QueryPlan, opt_to_str},
DateTimeF, Result, Error
},
};
static EAST8: Lazy<FixedOffset> = Lazy::new(|| FixedOffset::east_opt(8 * 60 * 60).unwrap());
pub struct CENCSource;
impl CENCSource {
pub fn new() -> Self {
Self{}
}
}
impl SourceInfo for CENCSource {
const SOURCE_ID: SourceId = "CENC";
const LATEST_API_URL: &'static str = "https://www.ceic.ac.cn/ajax/speedsearch";
const HISTORY_API_URL: &'static str = "https://www.ceic.ac.cn/ajax/search";
}
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)
.call()?
.into_string().map_err(|e| Error::InvalidResponse { source: e })?;
let resp = &resp[1..resp.len() - 1];
let raw_data: CENCRawData = serde_json::from_str(resp)?;
let mut events: Vec<Event> = raw_data.shuju
.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 CENCSource {
fn get_latest(&self) -> Result<Vec<Event>> {
request(Self::LATEST_API_URL, [("num", "1"), ("page", "1")])
}
fn run_query(&self, query: &QueryPlan) -> Result<Vec<Event>> {
request(Self::HISTORY_API_URL, [
("start", opt_to_str(query.since.map(|v| v.with_timezone(&*EAST8).to_rfc3339())).as_str()),
("end", opt_to_str(query.until.map(|v| v.with_timezone(&*EAST8).to_rfc3339())).as_str()),
("jingdu1", opt_to_str(query.min_lon).as_str()),
("jingdu2", opt_to_str(query.max_lon).as_str()),
("weidu1", opt_to_str(query.min_lat).as_str()),
("weidu2", opt_to_str(query.max_lat).as_str()),
("height1", opt_to_str(query.min_depth).as_str()),
("height2", opt_to_str(query.max_depth).as_str()),
("zhenji1", opt_to_str(query.min_mag).as_str()),
("zhenji2", opt_to_str(query.max_mag).as_str()),
("page", &query.page.to_string()),
])
}
}
#[allow(non_snake_case, dead_code)]
#[derive(serde::Deserialize, Debug)]
struct CENCRawItem {
id: String,
CATA_ID: String,
SAVE_TIME: String,
O_TIME: String,
EPI_LAT: String,
EPI_LON: String,
EPI_DEPTH: f64,
AUTO_FLAG: String,
EQ_TYPE: String,
O_TIME_FRA: String,
M: String,
M_MS: String,
M_MS7: String,
M_ML: String,
M_MB: String,
M_MB2: String,
SUM_STN: String,
LOC_STN: String,
LOCATION_C: String,
LOCATION_S: String,
CATA_TYPE: String,
SYNC_TIME: String,
IS_DEL: String,
EQ_CATA_TYPE: String,
NEW_DID: String,
}
#[derive(Deserialize)]
struct CENCRawData {
shuju: Vec<CENCRawItem>,
}
impl TryFrom<CENCRawItem> for Event {
type Error = Error;
fn try_from(value: CENCRawItem) -> Result<Self> {
let mut date = value.O_TIME.clone();
date = date.replace(' ', "T");
date.push_str("+08:00");
let time = DateTimeF::parse_from_rfc3339(&date)?.into();
date = value.SYNC_TIME.clone();
date = date.replace(' ', "T");
date.push_str("+08:00");
let updated = DateTimeF::parse_from_rfc3339(&date)?.into();
Ok(Self{
id: value.NEW_DID.clone(),
time,
updated,
url: format!("https://news.cenc.ac.cn/{}.html", value.NEW_DID),
source: CENCSource::SOURCE_ID,
status: if value.AUTO_FLAG == "M" {
ReportStatus::Reviewed
} else if value.IS_DEL.is_empty() {
ReportStatus::Automatic
} else {
ReportStatus::Deleted
},
magnitude: f64::from_str(&value.M)?,
longitude: f64::from_str(&value.EPI_LON)?,
latitude: f64::from_str(&value.EPI_LAT)?,
depth: value.EPI_DEPTH,
place: value.LOCATION_C,
})
}
}
#[cfg(test)]
mod test {
use crate::{Source, CENCSource, DateTimeF, Result};
#[test]
fn test_get_latest() -> Result<()> {
CENCSource::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 = CENCSource::new().query()
.since(&since).until(&until)
.run()?;
assert_eq!(
vec!["CD20230102161429", "CC20230102023507", "CC20230102022429"],
events.iter().map(|e| e.id.as_str()).collect::<Vec<&str>>()
);
Ok(())
}
}