use crate::api::client::{endpoints, GdeltClient};
use crate::error::Result;
use serde::{Deserialize, Serialize};
use tracing::instrument;
#[derive(Debug, Clone)]
pub struct GeoApi {
client: GdeltClient,
}
impl GeoApi {
pub fn new(client: GdeltClient) -> Self {
Self { client }
}
#[instrument(skip(self))]
pub async fn search(&self, params: GeoSearchParams) -> Result<GeoSearchResponse> {
let url = self.build_search_url(¶ms);
match self.client.get_json(&url).await {
Ok(response) => Ok(response),
Err(crate::error::GdeltError::Api { message, status_code: Some(404) }) => {
Err(crate::error::GdeltError::Api {
message: format!(
"GEO API returned 404. The endpoint may be unavailable or deprecated. \
Original error: {}. \
Alternative: Use 'gdelt doc search' with location filters.",
message
),
status_code: Some(404),
})
}
Err(e) => Err(e),
}
}
#[instrument(skip(self))]
pub async fn points(&self, params: GeoPointsParams) -> Result<GeoPointsResponse> {
let url = self.build_points_url(¶ms);
self.client.get_json(&url).await
}
#[instrument(skip(self))]
pub async fn heatmap(&self, params: GeoHeatmapParams) -> Result<GeoHeatmapResponse> {
let url = self.build_heatmap_url(¶ms);
self.client.get_json(&url).await
}
#[instrument(skip(self))]
pub async fn aggregate(&self, params: GeoAggregateParams) -> Result<GeoAggregateResponse> {
let url = self.build_aggregate_url(¶ms);
self.client.get_json(&url).await
}
fn build_search_url(&self, params: &GeoSearchParams) -> String {
let max_points = params.max_points.unwrap_or(250).to_string();
let mut query_params = vec![
("query", params.query.as_str()),
("mode", "PointData"),
("format", "json"),
("maxpoints", &max_points),
];
if let Some(ref timespan) = params.timespan {
query_params.push(("timespan", timespan));
}
if let Some(ref location) = params.location {
query_params.push(("location", location));
}
if let Some(ref country) = params.country {
query_params.push(("sourcecountry", country));
}
GdeltClient::build_url(endpoints::GEO_API, &query_params)
}
fn build_points_url(&self, params: &GeoPointsParams) -> String {
let resolution = params.resolution.unwrap_or(0).to_string();
let mut query_params = vec![
("query", params.query.as_str()),
("mode", "PointData"),
("format", "json"),
("geores", &resolution),
];
if let Some(ref timespan) = params.timespan {
query_params.push(("timespan", timespan));
}
GdeltClient::build_url(endpoints::GEO_API, &query_params)
}
fn build_heatmap_url(&self, params: &GeoHeatmapParams) -> String {
let mut query_params = vec![
("query", params.query.as_str()),
("mode", "PointHeatmap"),
("format", "json"),
];
if let Some(ref timespan) = params.timespan {
query_params.push(("timespan", timespan));
}
GdeltClient::build_url(endpoints::GEO_API, &query_params)
}
fn build_aggregate_url(&self, params: &GeoAggregateParams) -> String {
let mode = match params.level.as_deref() {
Some("adm1") => "CountryADM1",
_ => "Country",
};
let mut query_params = vec![
("query", params.query.as_str()),
("mode", mode),
("format", "json"),
];
if let Some(ref timespan) = params.timespan {
query_params.push(("timespan", timespan));
}
GdeltClient::build_url(endpoints::GEO_API, &query_params)
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct GeoSearchParams {
pub query: String,
pub timespan: Option<String>,
pub location: Option<String>,
pub country: Option<String>,
pub max_points: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct GeoPointsParams {
pub query: String,
pub timespan: Option<String>,
pub resolution: Option<u8>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct GeoHeatmapParams {
pub query: String,
pub timespan: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct GeoAggregateParams {
pub query: String,
pub level: Option<String>,
pub timespan: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoSearchResponse {
#[serde(default)]
pub features: Vec<GeoFeature>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoFeature {
#[serde(rename = "type", default)]
pub feature_type: String,
pub geometry: GeoGeometry,
#[serde(default)]
pub properties: GeoProperties,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoGeometry {
#[serde(rename = "type", default)]
pub geometry_type: String,
pub coordinates: Vec<f64>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct GeoProperties {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub urltone: Option<f64>,
#[serde(default)]
pub urlcount: Option<u32>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub domain: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoPointsResponse {
#[serde(default)]
pub features: Vec<GeoFeature>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoHeatmapResponse {
#[serde(default)]
pub features: Vec<GeoFeature>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoAggregateResponse {
#[serde(default)]
pub features: Vec<CountryFeature>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CountryFeature {
pub country: String,
#[serde(default)]
pub count: u32,
#[serde(default)]
pub tone: Option<f64>,
}