rss_core 0.6.0

Raster Source Service core library for querying, downloading, and processing remote sensing imagery
//! Module for filtering and transforming STAC items and assets.
//!
//! Provides functions for filtering by asset keys, tile identifiers,
//! and numeric property thresholds.
use std::collections::HashMap;

use crate::utils::Cmp;
use anyhow::Result;
use stac::{Asset, ItemCollection};
use std::iter::FromIterator;

/// Filters STAC assets by key name using glob patterns.
///
/// Supports exact match, prefix (`red*`), suffix (`*red`), and substring (`*nir*`) patterns.
/// Unmatched patterns are silently skipped — you get all assets that matched any pattern.
///
/// # Example
///
/// ```ignore
/// let filtered = filter_assets_by_key(assets, &["nbart_red", "nbart_*"]);
/// ```
pub fn filter_assets_by_key(
    assets: HashMap<String, Asset>,
    patterns: &[&str],
) -> HashMap<String, Asset> {
    let mut res: HashMap<String, Asset> = HashMap::new();

    for pattern in patterns {
        let pattern = pattern.trim();

        let starts_with = pattern.ends_with('*');
        let ends_with = pattern.starts_with('*');
        let trimmed_pattern = pattern.trim_matches('*');

        for (k, v) in assets.iter() {
            let is_match = match (starts_with, ends_with) {
                (true, false) => k.starts_with(trimmed_pattern),
                (false, true) => k.ends_with(trimmed_pattern),
                (true, true) => k.contains(trimmed_pattern),
                (false, false) => k == trimmed_pattern,
            };

            if is_match {
                res.insert(k.clone(), v.clone());
            }
        }
    }

    res
}

// pub fn filter_assets_by_key(
//     assets: HashMap<String, Asset>,
//     keys: &[&str],
// ) -> HashMap<String, Asset> {
//     let mut res: HashMap<String, Asset> = HashMap::new();
//     let available_keys = assets.keys().clone();
//     for key in keys.iter() {
//         let (k, v) = assets.get_key_value(*key).unwrap_or_else(|| {
//             panic!(
//                 "{}",
//                 format!("Asset doesn't contain key {key}. Available keys: {available_keys:?}")
//             )
//         });
//         res.insert(k.to_string(), v.to_owned());
//     }
//     res
// }

/// Filters STAC items by a string field, matching against a list of uppercase values.
///
/// Commonly used for DEA scene IDs (e.g., "56JMR").
pub fn filter_tile(
    items: ItemCollection,
    field_name: &str,
    values: &Vec<String>,
) -> Result<ItemCollection> {
    let mut filtered_items = Vec::new();

    for item in items.items {
        // info!("item {item:?}");
        let item_value = item.properties.additional_fields[field_name]
            .as_str()
            .unwrap();

        for value in values {
            if item_value == value.to_uppercase() {
                filtered_items.push(item.clone())
            }
        }
    }

    let result = ItemCollection::from_iter(filtered_items);
    Ok(result)
}

/// Filter Planetary Computer (PC) STAC items by Landsat WRS path/row values.
///
/// This function inspects each item in the given [`ItemCollection`], extracts
/// the `landsat:wrs_path` and `landsat:wrs_row` properties, and builds a string
/// of the form `p{path}r{row}`. It then checks whether this string matches
/// any of the values in the provided `values` vector.
///
/// Only items with matching `p{path}r{row}` codes are retained.
///
/// # Arguments
///
/// * `items` - The input STAC [`ItemCollection`] containing items to filter.
/// * `values` - A vector of `&str` values representing the desired
///   path/row codes (e.g. `"p123r045"`).
///
/// # Returns
///
/// A [`Result`] containing a new [`ItemCollection`] with only the matching items,
/// or an error if collection construction fails.
///
/// # Example
/// ```ignore
/// let values = vec!["p123r045", "p124r046"];
/// let filtered = filter_tile_pc(item_collection, &values)?;
/// ```
pub fn filter_tile_pc(items: ItemCollection, values: &Vec<&str>) -> Result<ItemCollection> {
    let mut filtered_items = Vec::new();

    // Iterate over all STAC items in the collection
    for item in items.items {
        // Extract WRS path and row from Landsat metadata
        let path = item.properties.additional_fields["landsat:wrs_path"]
            .as_str()
            .unwrap();
        let row = item.properties.additional_fields["landsat:wrs_row"]
            .as_str()
            .unwrap();

        // Build combined "p{path}r{row}" string
        let pr = ["p", path, "r", row].concat();

        // Keep item if its path/row code is in the target values
        for value in values {
            if &pr == value {
                filtered_items.push(item.clone());
            }
        }
    }

    // Construct a new ItemCollection from the filtered set
    let result = ItemCollection::from_iter(filtered_items);
    Ok(result)
}

/// Filters STAC items by comparing a numeric field against a threshold.
///
/// # Example
/// ```ignore
/// let clear = filter_items(items, "eo:cloud_cover", Cmp::Less, 10.0)?;
/// ```
pub fn filter_items<T>(
    items: ItemCollection,
    field_name: &str,
    cmp: Cmp,
    value: T,
) -> Result<ItemCollection>
where
    T: std::convert::From<f64> + std::cmp::PartialOrd,
{
    let mut filtered_items = Vec::new();
    for item in items.items {
        let item_value: T = item.properties.additional_fields[field_name]
            .as_f64()
            .unwrap()
            .into();
        match cmp {
            Cmp::Less => {
                if item_value < value {
                    filtered_items.push(item)
                }
            }
            Cmp::Greater => {
                if item_value > value {
                    filtered_items.push(item)
                }
            }
            Cmp::LessEqual => {
                if item_value <= value {
                    filtered_items.push(item)
                }
            }
            Cmp::GreaterEqual => {
                if item_value >= value {
                    filtered_items.push(item)
                }
            }
            Cmp::Equal => {
                if item_value == value {
                    filtered_items.push(item)
                }
            }
        }
    }
    let result = ItemCollection::from_iter(filtered_items);

    Ok(result)
}