Skip to content

Commit

Permalink
feat: add search matching
Browse files Browse the repository at this point in the history
  • Loading branch information
gadomski committed Oct 19, 2023
1 parent a129960 commit 9f4342d
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 5 deletions.
3 changes: 2 additions & 1 deletion stac-api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Added

- `Search.validate` ([#206](https://github.com/stac-utils/stac-rs/pull/206))
- `Search::validate` ([#206](https://github.com/stac-utils/stac-rs/pull/206))
- `geo` feature, `Search::matches` and sub-methods, `Search::new`, `Search::ids`, `Default` for `Filter`, `Error::Stac`, and `Error::Unimplemented` ([#209](https://github.com/stac-utils/stac-rs/pull/209))

## [0.3.2] - 2023-10-11

Expand Down
5 changes: 5 additions & 0 deletions stac-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ keywords = ["geospatial", "stac", "metadata", "geo", "raster"]
categories = ["science", "data-structures", "web-programming"]

[features]
geo = ["dep:geo", "stac/geo"]
schemars = ["dep:schemars", "stac/schemars"]

[dependencies]
geo = { version = "0.26", optional = true }
schemars = { version = "0.8", optional = true }
serde = "1"
serde_json = "1"
serde_urlencoded = "0.7"
stac = { version = "0.5", path = "../stac" }
thiserror = "1"
url = "2.3"

[dev-dependencies]
geojson = "0.24"
10 changes: 9 additions & 1 deletion stac-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ To use the library in your project:
stac-api = "0.3"
```

**stac-api** has one optional feature, `schemars`, which can be used to generate [jsonschema](https://json-schema.org/) documents for the API structures.
**stac-api** has two optional features.
`geo` enables `Search::match`:

```toml
[dependencies]
stac-api = { version = "0.3", features = ["geo"] }
```

`schemars`, can be used to generate [jsonschema](https://json-schema.org/) documents for the API structures.
This is useful for auto-generating OpenAPI documentation:

```toml
Expand Down
8 changes: 8 additions & 0 deletions stac-api/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,19 @@ pub enum Error {
#[error(transparent)]
SerdeUrlencodedSer(#[from] serde_urlencoded::ser::Error),

/// [stac::Error]
#[error(transparent)]
Stac(#[from] stac::Error),

/// [std::num::TryFromIntError]
#[error(transparent)]
TryFromInt(#[from] std::num::TryFromIntError),

/// [url::ParseError]
#[error(transparent)]
UrlParse(#[from] url::ParseError),

/// This functionality is not yet implemented.
#[error("this functionality is not yet implemented: {0}")]
Unimplemented(&'static str),
}
6 changes: 6 additions & 0 deletions stac-api/src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ pub enum Filter {
Cql2Json(Map<String, Value>),
}

impl Default for Filter {
fn default() -> Self {
Filter::Cql2Json(Default::default())
}
}

#[cfg(test)]
mod tests {
use super::Filter;
Expand Down
3 changes: 3 additions & 0 deletions stac-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ pub type Result<T> = std::result::Result<T, Error>;
/// servers to explicitly include or exclude certain fields.
pub type Item = serde_json::Map<String, serde_json::Value>;

#[cfg(test)]
use geojson as _;

// From https://github.com/rust-lang/cargo/issues/383#issuecomment-720873790,
// may they be forever blessed.
#[cfg(doctest)]
Expand Down
239 changes: 238 additions & 1 deletion stac-api/src/search.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{Error, Fields, Filter, GetItems, Items, Result, Sortby};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use stac::Geometry;
use stac::{Geometry, Item};
use std::collections::HashMap;

/// The core parameters for STAC search are defined by OAFeat, and STAC adds a few parameters for convenience.
Expand Down Expand Up @@ -124,6 +124,32 @@ pub struct GetSearch {
}

impl Search {
/// Creates a new, empty search.
///
/// # Examples
///
/// ```
/// use stac_api::Search;
///
/// let search = Search::new();
/// ```
pub fn new() -> Search {
Search::default()
}

/// Sets the ids field of this search.
///
/// # Examples
///
/// ```
/// use stac_api::Search;
/// let search = Search::new().ids(vec!["an-id".to_string()]);
/// ```
pub fn ids(mut self, ids: impl Into<Option<Vec<String>>>) -> Search {
self.ids = ids.into();
self
}

/// Validates this search.
///
/// E.g. the search is invalid if both bbox and intersects are specified.
Expand All @@ -144,6 +170,217 @@ impl Search {
Ok(())
}
}

/// Returns true if this item matches this search.
///
/// # Examples
///
/// ```
/// use stac::Item;
/// use stac_api::Search;
///
/// let item = Item::new("an-id");
/// assert!(Search::new().matches(&item).unwrap());
/// assert!(!Search::new().ids(vec!["not-the-id".to_string()]).matches(&item).unwrap());
/// ```
#[cfg(feature = "geo")]
pub fn matches(&self, item: &Item) -> Result<bool> {
Ok(self.collection_matches(item)
& self.id_matches(item)
& self.bbox_matches(item)?
& self.intersects_matches(item)?
& self.datetime_matches(item)?
& self.query_matches(item)?
& self.filter_matches(item)?)
}

/// Returns true if this item's collection matches this search.
///
/// # Examples
///
/// ```
/// use stac_api::Search;
/// use stac::Item;
///
/// let mut search = Search::new();
/// let mut item = Item::new("item-id");
/// assert!(search.collection_matches(&item));
/// search.collections = Some(vec!["collection-id".to_string()]);
/// assert!(!search.collection_matches(&item));
/// item.collection = Some("collection-id".to_string());
/// assert!(search.collection_matches(&item));
/// item.collection = Some("another-collection-id".to_string());
/// assert!(!search.collection_matches(&item));
/// ```
pub fn collection_matches(&self, item: &Item) -> bool {
if let Some(collections) = self.collections.as_ref() {
if let Some(collection) = item.collection.as_ref() {
collections.contains(collection)
} else {
false
}
} else {
true
}
}

/// Returns true if this item's id matches this search.
///
/// # Examples
///
/// ```
/// use stac_api::Search;
/// use stac::Item;
///
/// let mut search = Search::new();
/// let mut item = Item::new("item-id");
/// assert!(search.id_matches(&item));
/// search.ids = Some(vec!["item-id".to_string()]);
/// assert!(search.id_matches(&item));
/// search.ids = Some(vec!["another-id".to_string()]);
/// assert!(!search.id_matches(&item));
/// ```
pub fn id_matches(&self, item: &Item) -> bool {
if let Some(ids) = self.ids.as_ref() {
ids.contains(&item.id)
} else {
true
}
}

/// Returns true if this item's geometry matches this search's bbox.
///
/// # Examples
///
/// ```
/// # #[cfg(feature = "geo")]
/// # {
/// use stac_api::Search;
/// use stac::Item;
/// use geojson::{Geometry, Value};
///
/// let mut search = Search::new();
/// let mut item = Item::new("item-id");
/// assert!(search.bbox_matches(&item).unwrap());
/// search.bbox = Some(vec![-110.0, 40.0, -100.0, 50.0]);
/// assert!(!search.bbox_matches(&item).unwrap());
/// item.set_geometry(Geometry::new(Value::Point(vec![-105.1, 41.1])));
/// assert!(search.bbox_matches(&item).unwrap());
/// # }
/// ```
#[cfg(feature = "geo")]
pub fn bbox_matches(&self, item: &Item) -> Result<bool> {
if let Some(bbox) = self.bbox.as_ref() {
let bbox = stac::geo::bbox(bbox)?;
item.intersects(&bbox).map_err(Error::from)
} else {
Ok(true)
}
}

/// Returns true if this item's geometry matches this search's intersects.
///
/// # Examples
///
/// ```
/// # #[cfg(feature = "geo")]
/// # {
/// use stac_api::Search;
/// use stac::Item;
/// use geojson::{Geometry, Value};
///
/// let mut search = Search::new();
/// let mut item = Item::new("item-id");
/// assert!(search.intersects_matches(&item).unwrap());
/// search.intersects = Some(stac::Geometry::point(-105.1, 41.1));
/// assert!(!search.intersects_matches(&item).unwrap());
/// item.set_geometry(Geometry::new(Value::Point(vec![-105.1, 41.1])));
/// assert!(search.intersects_matches(&item).unwrap());
/// # }
/// ```
#[cfg(feature = "geo")]
pub fn intersects_matches(&self, item: &Item) -> Result<bool> {
if let Some(intersects) = self.intersects.clone() {
let intersects: geo::Geometry = intersects.try_into()?;
item.intersects(&intersects).map_err(Error::from)
} else {
Ok(true)
}
}

/// Returns true if this item's datetime matches this search.
///
/// # Examples
///
/// ```
/// use stac_api::Search;
/// use stac::Item;
///
/// let mut search = Search::new();
/// let mut item = Item::new("item-id"); // default datetime is now
/// assert!(search.datetime_matches(&item).unwrap());
/// search.datetime = Some("../2023-10-09T00:00:00Z".to_string());
/// assert!(!search.datetime_matches(&item).unwrap());
/// item.properties.datetime = Some("2023-10-08T00:00:00Z".to_string());
/// assert!(search.datetime_matches(&item).unwrap());
/// ```
pub fn datetime_matches(&self, item: &Item) -> Result<bool> {
if let Some(datetime) = self.datetime.as_ref() {
item.intersects_datetime_str(datetime).map_err(Error::from)
} else {
Ok(true)
}
}

/// Returns true if this item's matches this search query.
///
/// Currently unsupported, always raises an error if query is set.
///
/// # Examples
///
/// ```
/// use stac_api::Search;
/// use stac::Item;
///
/// let mut search = Search::new();
/// let mut item = Item::new("item-id");
/// assert!(search.query_matches(&item).unwrap());
/// search.query = Some(Default::default());
/// assert!(search.query_matches(&item).is_err());
/// ```
pub fn query_matches(&self, _: &Item) -> Result<bool> {
if let Some(_) = self.query.as_ref() {
// TODO implement
Err(Error::Unimplemented("query"))
} else {
Ok(true)
}
}

/// Returns true if this item matches this search's filter.
///
/// Currently unsupported, always raises an error if filter is set.
///
/// # Examples
///
/// ```
/// use stac_api::Search;
/// use stac::Item;
///
/// let mut search = Search::new();
/// let mut item = Item::new("item-id");
/// assert!(search.filter_matches(&item).unwrap());
/// search.filter = Some(Default::default());
/// assert!(search.filter_matches(&item).is_err());
/// ```
pub fn filter_matches(&self, _: &Item) -> Result<bool> {
if let Some(_) = self.filter.as_ref() {
// TODO implement
Err(Error::Unimplemented("filter"))
} else {
Ok(true)
}
}
}

impl TryFrom<Search> for GetSearch {
Expand Down
5 changes: 3 additions & 2 deletions stac/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- `Geometry::point` ([#206](https://github.com/stac-utils/stac-rs/pull/206))
- `Item::intersects_datetime_str` ([#209](https://github.com/stac-utils/stac-rs/pull/209))

## [0.5.2] - 2023-10-18

### Added

- `Item.intersects` ([#202](https://github.com/stac-utils/stac-rs/pull/202))
- `Item::intersects` ([#202](https://github.com/stac-utils/stac-rs/pull/202))
- Common metadata fields ([#203](https://github.com/stac-utils/stac-rs/pull/203))

### Deprecated

- `Item.intersects_bbox` ([#204](https://github.com/stac-utils/stac-rs/pull/204))
- `Item::intersects_bbox` ([#204](https://github.com/stac-utils/stac-rs/pull/204))

## [0.5.1] - 2023-09-14

Expand Down
16 changes: 16 additions & 0 deletions stac/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,22 @@ impl Item {
}
}

/// Returns true if this item's datetime (or start and end datetime)
/// intersects the provided datetime string.
///
/// # Examples
///
/// ```
/// use stac::Item;
/// let mut item = Item::new("an-id");
/// item.properties.datetime = Some("2023-07-11T12:00:00Z".to_string());
/// assert!(item.intersects_datetime_str("2023-07-11T00:00:00Z/2023-07-12T00:00:00Z").unwrap());
/// ```
pub fn intersects_datetime_str(&self, datetime: &str) -> Result<bool> {
let (start, end) = crate::datetime::parse(datetime)?;
self.intersects_datetimes(start, end)
}

/// Returns true if this item's datetime (or start and end datetimes)
/// intersects the provided datetime.
///
Expand Down

0 comments on commit 9f4342d

Please sign in to comment.