rss_core 0.5.0

Raster Source Service core library for querying, downloading, and processing remote sensing imagery
//! The Remote Sensing Sciences (RSS) crate is a [Rust](https://www.rust-lang.org/) library and
//! command line application for remote sensing imagery processing, built for the JRSRP workflow.
//!
//! # Overview
//!
//! RSS provides tools for querying, downloading, and processing satellite imagery from multiple
//! sources including Digital Earth Australia (DEA), Element84, Microsoft Planetary Computer, and
//! local Apollo filestores. It supports both STAC-based cloud catalogs and PostgreSQL metadata
//! databases.
//!
//! # Modules
//!
//! - [`query`]: Build and execute imagery queries via STAC API or PostgreSQL
//! - [`io`]: Download and recall imagery with async/sync support
//! - [`qvf`]: Parse and manipulate QVF (Queensland RSC) filename conventions
//! - [`stac`]: Filter and transform STAC items and assets
//! - [`utils`]: Core types like [`utils::Bbox`], and [`utils::Intersects`]
//! - [`masks`]: Cloud mask utilities with Python bindings
//!
//! # Examples
//!
//! ## Query and Download Imagery
//!
//! ```ignore
//! use std::path::PathBuf;
//! use chrono::NaiveDate;
//! use rss_core::{DEA, query::ImageQueryBuilder, qvf::Collection, utils::{Cmp, Intersects}};
//!
//! let source = DEA.clone();
//!
//! let query = ImageQueryBuilder::new(
//!     source,
//!     Collection::Sentinel2,
//!     Intersects::Scene(vec!["56jmr"]),
//! )
//! .canonical_bands(["red", "nir"])
//! .start_date(NaiveDate::parse_from_str("2022-01-01", "%Y-%m-%d")?)
//! .end_date(NaiveDate::parse_from_str("2022-01-15", "%Y-%m-%d")?)
//! .cloudcover((Cmp::Less, 10))
//! .build();
//!
//! let fc = query.get(&PathBuf::from("./output"), None, None)?;
//! ```
//!
//! ## Parse QVF Filenames
//!
//! ```ignore
//! use std::str::FromStr;
//! use rss_core::qvf::QvfFilename;
//!
//! let qvf = QvfFilename::from_str("cfmsre_t56jmr_20220104_abam5.img")?;
//! println!("Scene: {}, Date: {}", qvf.scene, qvf.date);
//! println!("Is Sentinel: {}", qvf.is_sentinel());
//! ```
//!
//! # Python Bindings
//!
//! Selected functions are exposed via PyO3 for integration with Python workflows:
//!
//! ```python
//! from rss_core import get_s2_cloudless_dea
//!
//! get_s2_cloudless_dea("cfmsre_t56jmr_20220104_abam5.img", "output.tif")
//! ```

#![warn(missing_docs)]

pub mod cache;
pub mod cloud_mask;
pub mod io;
pub mod masks;
pub mod products;
pub mod qvf;
pub mod query;
pub mod stac;
pub mod utils;

use lazy_static::lazy_static;
use utils::{ImageryProvider, ImageryProviderType, ImagerySource};

// Pre-configured imagery sources for common providers.
// Clone these values when passing to ImageQueryBuilder.
lazy_static! {
    /// Digital Earth Australia (DEA) STAC endpoint.
    ///
    /// Provides access to Landsat 5/7/8/9 and Sentinel-2 ARD collections.
    /// Uses `/vsis3/` protocol for direct S3 access.
    ///
    /// # Example
    /// ```ignore
    /// use rss_core::DEA;
    ///
    /// let source = DEA.clone();
    /// // Pass to ImageQueryBuilder::new(...)
    /// ```
    pub static ref DEA: ImagerySource = ImagerySource::Dea(ImageryProvider {
        provider_type: ImageryProviderType::Stac,
        url: "https://explorer.sandbox.dea.ga.gov.au/stac/search".to_owned()
    });

    /// Apollo local filestore metadata database.
    ///
    /// Queries a PostgreSQL database for imagery metadata and recalls files
    /// from the local DM-based filestore at `/apollo`.
    ///
    /// # Example
    /// ```ignore
    /// use rss_core::APOLLO;
    ///
    /// let source = APOLLO.clone();
    /// // Pass to ImageQueryBuilder::new(...)
    /// ```
    pub static ref APOLLO: ImagerySource = ImagerySource::Apollo(ImageryProvider {
        provider_type: ImageryProviderType::Local,
        url: "/apollo".to_owned()
    });

    /// Element84 AWS-hosted STAC catalog.
    ///
    /// Provides access to USGS Landsat Collection 2 and Sentinel-2 Level-2A.
    /// Uses `/vsis3/` for Landsat and `/vsicurl/` for Sentinel-2.
    ///
    /// # Example
    /// ```ignore
    /// use rss_core::ELEMENT84;
    ///
    /// let source = ELEMENT84.clone();
    /// // Pass to ImageQueryBuilder::new(...)
    /// ```
    pub static ref ELEMENT84: ImagerySource = ImagerySource::Element(ImageryProvider {
        provider_type: ImageryProviderType::Stac,
        url: "https://earth-search.aws.element84.com/v1/search".to_owned()
    });

    /// Microsoft Planetary Computer STAC catalog.
    ///
    /// Provides access to Landsat Collection 2 with SAS token signing
    /// for authenticated access. Uses `/vsicurl/` protocol.
    ///
    /// # Example
    /// ```ignore
    /// use rss_core::PLANETARYCOMPUTER;
    ///
    /// let source = PLANETARYCOMPUTER.clone();
    /// // Pass to ImageQueryBuilder::new(...)
    /// ```
    pub static ref PLANETARYCOMPUTER: ImagerySource = ImagerySource::PlanetaryComputer(ImageryProvider {
        provider_type: ImageryProviderType::Stac,
        url: "https://planetarycomputer.microsoft.com/api/stac/v1/search".to_owned()
    });
}

// Product registry loaded from YAML manifests in `data/products/`.
// Provides declarative product definitions replacing the old hardcoded
// `COLLECTION_MAPPINGS`. Each product defines measurements, cloud mask
// configuration, STAC collection IDs, and output format preferences.
#[allow(missing_docs)]
mod registry_init {
    use super::products::ProductRegistry;
    use lazy_static::lazy_static;

    lazy_static! {
        pub static ref PRODUCT_REGISTRY: ProductRegistry = {
            let manifest_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/data/products");
            ProductRegistry::load_from_dir(manifest_dir).expect("Failed to load product manifests")
        };
    }
}
pub use registry_init::PRODUCT_REGISTRY;

#[cfg(test)]
mod tests {
    use std::{path::PathBuf, str::FromStr};

    use chrono::NaiveDate;

    use crate::qvf::{
        Collection, Extension, ImageType, Instrument, Product, QvfDate, QvfFields, QvfFilename,
        QvfFilenames, Satellite,
    };

    // #[test]
    // fn test_query_dea_sentinel() {
    //     let query = ImageQueryBuilder::new(
    //         ImagerySource::Dea,
    //         Collection::Sentinel2,
    //         Intersects::Scene((&["56jmr"]).to_vec()),
    //         &["nbart_red"],
    //     )
    //     .start_date(NaiveDate::parse_from_str("20220101", "%Y%m%d").unwrap())
    //     .end_date(NaiveDate::parse_from_str("20220501", "%Y%m%d").unwrap())
    //     .landcover((Cmp::Greater, 10))
    //     .cloudcover((Cmp::Less, 50))
    //     .build();
    //     println!("{query:?}");
    // }

    // #[test]
    // fn test_from_query() {
    //     let query = ImageQueryBuilder::new(
    //         ImagerySource::Apollo,
    //         Collection::Sentinel2,
    //         Intersects::Scene((["t56jmr"]).to_vec()),
    //         &["aba"],
    //     )
    //     .start_date(NaiveDate::parse_from_str("20220101", "%Y%m%d").unwrap())
    //     .end_date(NaiveDate::parse_from_str("20220501", "%Y%m%d").unwrap())
    //     .landcover((Cmp::Greater, 10))
    //     .cloudcover((Cmp::Less, 50))
    //     .build();
    // }
    // #[test]
    // fn test_query_landsat_dea() {
    //     let query = ImageQueryBuilder::new(
    //         ImagerySource::Dea,
    //         Collection::Landsat8,
    //         Intersects::Scene(["p091r077"].to_vec()),
    //         &["nbart_red"],
    //     )
    //     .start_date(NaiveDate::parse_from_str("20220101", "%Y%m%d").unwrap())
    //     .end_date(NaiveDate::parse_from_str("20220501", "%Y%m%d").unwrap())
    //     .landcover((Cmp::Greater, 10))
    //     .cloudcover((Cmp::Less, 50))
    //     .build();
    //     query.get(&PathBuf::from("/tmp")).unwrap();
    // }

    #[test]
    fn test_qvf_from_str() {
        let name = "/scratch/rsc8/hardtkel/tmp/cfmsre_t56jmr_20220104_abam6.img";
        let qvf = QvfFilename::from_str(name).unwrap();
        println!("{qvf}");
    }

    // #[test]
    // fn test_recall_multiple() {
    //     let query = ImageQueryBuilder::new(
    //         ImagerySource::Apollo,
    //         Collection::Sentinel2,
    //         Intersects::Scene(&["t56jmr"]),
    //         &["aba"],
    //     )
    //     .start_date(NaiveDate::parse_from_str("20220101", "%Y%m%d").unwrap())
    //     .end_date(NaiveDate::parse_from_str("20220501", "%Y%m%d").unwrap())
    //     .landcover((Cmp::Greater, 10))
    //     .cloudcover((Cmp::Less, 50))
    //     .build().expect("Invalid query");
    //     qvfs.recall(&PathBuf::from("/tmp"))
    //         .expect("Unable to recall files");
    // }

    // #[test]
    // fn test_dirname() {
    //     let qvfn = QvfFilename::from_str("cfmsre_t55kgs_20180531_abam5.img")
    //         .expect("Could not parse the file name.");
    //     let expected = PathBuf::from("/apollo/imagery/rsc/sentinel2/t55k/t55kgs/2018/201805/");
    //     let dir = qvfn.qv_dir().unwrap();
    //     assert_eq!(expected, dir);

    //     let qvfn = QvfFilename::from_str("l8olre_p092r078_20220104_da1m5.img")
    //         .expect("Could not parse the file name.");
    //     let expected =
    //         PathBuf::from("/apollo/imagery/rsc/landsat/landsat57tm/wrs2/092_078/2022/202201/");
    //     let dir = qvfn.qv_dir().unwrap();
    //     assert_eq!(expected, dir);
    // }

    // #[test]
    // fn test_recall() {
    //     let qvfn = QvfFilename::from_str("cfmsre_t55kgs_20180531_abam5.img")
    //         .expect("Could not parse the file name.");
    //     let qvfn = QvfFilename::from_str("l8olre_p092r078_20220104_da1m5.img")
    //         .expect("Could not parse the file name.");
    //     qvfn.recall(PathBuf::from("/tmp"));
    //     // let qvfn = QvfFilename::from_str("lztmre_p094r075_m201903201905_djam5.img")
    //     //     .expect("Could not parse the file name.");
    // }

    // #[test]
    // fn test_date_format() {
    //     let file = "cfmsre_t55kgs_20180531_abam5.img";
    //     let qvfn = QvfFilename::from_str(file).expect("Could not parse the file name.");
    //     println!("{}", qvfn.date.format("%Y"));
    // }

    #[test]
    pub(crate) fn test_parse() {
        let file = "cfmsre_t55kgs_20180531_abam5.img";
        let qvfn = QvfFilename::from_str(file).expect("Could not parse the file name.");
        let expected = QvfFilename {
            satellite: Satellite::cf,
            instrument: Instrument::ms,
            product: Product::re,
            scene: "t55kgs".to_string(),
            date: QvfDate::Date(NaiveDate::parse_from_str("20180531", "%Y%m%d").unwrap()),
            stage_code: "aba".to_string(),
            zone: "m5".to_string(),
            extension: Extension::img,
            image_type: ImageType::Scene,
            collection: Collection::Sentinel2,
            location: Some(PathBuf::from("")),
            extra_fields: None,
        };
        assert_eq!(qvfn, expected);

        let file = "lztmre_p093r088_m202203202205_dimm4.img";
        let _qvfn = QvfFilename::from_str(file).expect("Could not parse the file name.");
    }
    #[test]
    fn test_change_sat() {
        let file = "cfmsre_t55kgs_20180531_abam5.img";
        let mut qvfn = QvfFilename::from_str(file).expect("Could not parse the file name.");
        qvfn = qvfn.change_satellite(Satellite::l7);
        let expected = QvfFilename {
            satellite: Satellite::l7,
            instrument: Instrument::ms,
            product: Product::re,
            scene: "t55kgs".to_string(),
            date: QvfDate::Date(NaiveDate::parse_from_str("20180531", "%Y%m%d").unwrap()),
            stage_code: "aba".to_string(),
            zone: "m5".to_string(),
            extension: Extension::img,
            image_type: ImageType::Scene,
            collection: Collection::Sentinel2,
            location: Some(PathBuf::from("")),
            extra_fields: None,
        };
        assert_eq!(qvfn, expected);
    }
    #[test]
    fn test_change_inst() {
        let file = "cfmsre_t55kgs_20180531_abam5.img";
        let mut qvfn = QvfFilename::from_str(file).expect("Could not parse the file name.");
        qvfn = qvfn.change_instrument(Instrument::tm);
        let expected = QvfFilename {
            satellite: Satellite::cf,
            instrument: Instrument::tm,
            product: Product::re,
            scene: "t55kgs".to_string(),
            date: QvfDate::Date(NaiveDate::parse_from_str("20180531", "%Y%m%d").unwrap()),
            stage_code: "aba".to_string(),
            zone: "m5".to_string(),
            extension: Extension::img,
            image_type: ImageType::Scene,
            collection: Collection::Sentinel2,
            location: Some(PathBuf::from("")),
            extra_fields: None,
        };
        assert_eq!(qvfn, expected);
    }

    #[test]
    fn test_sort_by_dates() {
        let file_1 = QvfFilename::from_str("cfmsre_t55kgs_20180531_abam5.img").unwrap();
        let file_2 = QvfFilename::from_str("cfmsre_t55kgs_20180528_abam5.img").unwrap();
        let file_3 = QvfFilename::from_str("cfmsre_t55kgs_20170430_abam5.img").unwrap();
        let mut qvfs = QvfFilenames {
            qvf_filenames: vec![file_1, file_2, file_3],
        };
        qvfs.sort_by(QvfFields::Date);
        let file_1 = QvfFilename::from_str("cfmsre_t55kgs_20180531_abam5.img").unwrap();
        let file_2 = QvfFilename::from_str("cfmsre_t55kgs_20180528_abam5.img").unwrap();
        let file_3 = QvfFilename::from_str("cfmsre_t55kgs_20170430_abam5.img").unwrap();
        let expected = QvfFilenames {
            qvf_filenames: vec![file_3, file_2, file_1],
        };
        assert_eq!(qvfs, expected);
    }

    #[test]
    fn test_change_prod() {
        let file = "cfmsre_t55kgs_20180531_abam5.img";
        let mut qvfn = QvfFilename::from_str(file).expect("Could not parse the file name.");
        qvfn = qvfn.change_product(Product::pa);
        let expected = QvfFilename {
            satellite: Satellite::cf,
            instrument: Instrument::ms,
            product: Product::pa,
            scene: "t55kgs".to_string(),
            date: QvfDate::Date(NaiveDate::parse_from_str("20180531", "%Y%m%d").unwrap()),
            stage_code: "aba".to_string(),
            zone: "m5".to_string(),
            extension: Extension::img,
            image_type: ImageType::Scene,
            collection: Collection::Sentinel2,
            location: Some(PathBuf::from("")),
            extra_fields: None,
        };
        assert_eq!(qvfn, expected);
    }
}