gdelt 0.1.0

CLI for GDELT Project - optimized for agentic usage with local data caching
//! GEO 2.0 API client for geographic searches.
//!
//! The GEO API provides geographic context for news articles,
//! including point data, heatmaps, and country/region aggregations.

use crate::api::client::{endpoints, GdeltClient};
use crate::error::Result;
use serde::{Deserialize, Serialize};
use tracing::instrument;

/// GEO 2.0 API client
#[derive(Debug, Clone)]
pub struct GeoApi {
    client: GdeltClient,
}

impl GeoApi {
    /// Create a new GEO API client
    pub fn new(client: GdeltClient) -> Self {
        Self { client }
    }

    /// Search with geographic context
    ///
    /// Note: The GEO 2.0 API may be temporarily unavailable or have limited availability.
    /// If you receive 404 errors, the API endpoint may have been deprecated.
    /// Consider using the DOC API with location filters as an alternative.
    #[instrument(skip(self))]
    pub async fn search(&self, params: GeoSearchParams) -> Result<GeoSearchResponse> {
        let url = self.build_search_url(&params);
        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),
        }
    }

    /// Get point data for mapping
    #[instrument(skip(self))]
    pub async fn points(&self, params: GeoPointsParams) -> Result<GeoPointsResponse> {
        let url = self.build_points_url(&params);
        self.client.get_json(&url).await
    }

    /// Get heatmap data
    #[instrument(skip(self))]
    pub async fn heatmap(&self, params: GeoHeatmapParams) -> Result<GeoHeatmapResponse> {
        let url = self.build_heatmap_url(&params);
        self.client.get_json(&url).await
    }

    /// Get country/region aggregations
    #[instrument(skip(self))]
    pub async fn aggregate(&self, params: GeoAggregateParams) -> Result<GeoAggregateResponse> {
        let url = self.build_aggregate_url(&params);
        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)
    }
}

/// Parameters for GEO search
#[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>,
}

/// Parameters for point queries
#[derive(Debug, Clone, Default, Serialize)]
pub struct GeoPointsParams {
    pub query: String,
    pub timespan: Option<String>,
    pub resolution: Option<u8>,
}

/// Parameters for heatmap queries
#[derive(Debug, Clone, Default, Serialize)]
pub struct GeoHeatmapParams {
    pub query: String,
    pub timespan: Option<String>,
}

/// Parameters for aggregate queries
#[derive(Debug, Clone, Default, Serialize)]
pub struct GeoAggregateParams {
    pub query: String,
    pub level: Option<String>,
    pub timespan: Option<String>,
}

/// Response from GEO search/points
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoSearchResponse {
    #[serde(default)]
    pub features: Vec<GeoFeature>,
}

/// A geographic feature
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoFeature {
    #[serde(rename = "type", default)]
    pub feature_type: String,
    pub geometry: GeoGeometry,
    #[serde(default)]
    pub properties: GeoProperties,
}

/// Geographic geometry
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoGeometry {
    #[serde(rename = "type", default)]
    pub geometry_type: String,
    pub coordinates: Vec<f64>,
}

/// Geographic properties
#[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>,
}

/// Response from GEO points
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoPointsResponse {
    #[serde(default)]
    pub features: Vec<GeoFeature>,
}

/// Response from GEO heatmap
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoHeatmapResponse {
    #[serde(default)]
    pub features: Vec<GeoFeature>,
}

/// Response from GEO aggregate
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeoAggregateResponse {
    #[serde(default)]
    pub features: Vec<CountryFeature>,
}

/// A country aggregation feature
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CountryFeature {
    pub country: String,
    #[serde(default)]
    pub count: u32,
    #[serde(default)]
    pub tone: Option<f64>,
}