use crate::{
Asset, Assets, Bbox, Error, Item, ItemAsset, Link, Links, Migrate, Result, STAC_VERSION,
SelfHref, Version,
};
use chrono::{DateTime, Utc};
use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Map, Value};
use stac_derive::{Fields, Links, SelfHref};
const DEFAULT_LICENSE: &str = "other";
const COLLECTION_TYPE: &str = "Collection";
fn collection_type() -> String {
COLLECTION_TYPE.to_string()
}
fn deserialize_collection_type<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
where
D: Deserializer<'de>,
{
let r#type = String::deserialize(deserializer)?;
if r#type != COLLECTION_TYPE {
Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&r#type),
&COLLECTION_TYPE,
))
} else {
Ok(r#type)
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, SelfHref, Links, Fields)]
pub struct Collection {
#[serde(
default = "collection_type",
deserialize_with = "deserialize_collection_type"
)]
r#type: String,
#[serde(rename = "stac_version", default)]
pub version: Version,
#[serde(rename = "stac_extensions")]
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub extensions: Vec<String>,
#[serde(default)]
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default)]
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub keywords: Option<Vec<String>>,
#[serde(default)]
pub license: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub providers: Option<Vec<Provider>>,
#[serde(default)]
pub extent: Extent,
#[serde(skip_serializing_if = "Option::is_none")]
pub summaries: Option<Map<String, Value>>,
#[serde(default)]
pub links: Vec<Link>,
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
pub assets: IndexMap<String, Asset>,
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
pub item_assets: IndexMap<String, ItemAsset>,
#[serde(flatten)]
pub additional_fields: Map<String, Value>,
#[serde(skip)]
self_href: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Provider {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(flatten)]
pub additional_fields: Map<String, Value>,
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)]
pub struct Extent {
pub spatial: SpatialExtent,
pub temporal: TemporalExtent,
#[serde(flatten)]
pub additional_fields: Map<String, Value>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct SpatialExtent {
pub bbox: Vec<Bbox>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct TemporalExtent {
pub interval: Vec<[Option<DateTime<Utc>>; 2]>,
}
impl Collection {
pub fn new(id: impl ToString, description: impl ToString) -> Collection {
Collection {
r#type: collection_type(),
version: STAC_VERSION,
extensions: Vec::new(),
id: id.to_string(),
title: None,
description: description.to_string(),
keywords: None,
license: DEFAULT_LICENSE.to_string(),
providers: None,
extent: Extent::default(),
summaries: None,
links: Vec::new(),
assets: IndexMap::new(),
item_assets: IndexMap::new(),
additional_fields: Map::new(),
self_href: None,
}
}
pub fn from_id_and_items(id: impl ToString, items: &[Item]) -> Collection {
let description = format!(
"This collection was generated by rustac v{} from {} items",
env!("CARGO_PKG_VERSION"),
items.len()
);
if items.is_empty() {
Collection::new(id, description)
} else {
let mut collection = Collection::new_from_item(id, description, &items[0]);
for item in items.iter().skip(1) {
let _ = collection.add_item(item);
}
collection
}
}
pub fn new_from_item(id: impl ToString, description: impl ToString, item: &Item) -> Collection {
let mut collection = Collection::new(id, description);
if let Some(bbox) = item.bbox {
collection.extent.spatial.bbox[0] = bbox;
}
let (start, end) = item.datetimes();
collection.extent.temporal.update(start, end);
let _ = collection.maybe_add_item_link(item);
collection
}
fn update_extents(&mut self, item: &Item) {
if let Some(bbox) = item.bbox {
self.extent.spatial.update(bbox);
}
let (start, end) = item.datetimes();
self.extent.temporal.update(start, end);
}
fn maybe_add_item_link(&mut self, item: &Item) -> Option<&Link> {
if let Some(href) = item
.self_href()
.or(item.self_link().map(|link| link.href.as_str()))
{
self.links.push(Link::item(href));
self.links.last()
} else {
None
}
}
pub fn add_item(&mut self, item: &Item) -> Option<&Link> {
self.update_extents(item);
self.maybe_add_item_link(item)
}
}
impl Provider {
pub fn new(name: impl ToString) -> Provider {
Provider {
name: name.to_string(),
description: None,
roles: None,
url: None,
additional_fields: Map::new(),
}
}
}
impl Default for SpatialExtent {
fn default() -> SpatialExtent {
SpatialExtent {
bbox: vec![Default::default()],
}
}
}
impl SpatialExtent {
fn update(&mut self, other: Bbox) {
if self.bbox.is_empty() {
self.bbox.push(other);
} else {
self.bbox[0].update(other);
}
}
}
impl TemporalExtent {
fn update(&mut self, start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>) {
if self.interval.is_empty() {
self.interval.push([start, end]);
} else {
if let Some(start) = start
&& self.interval[0][0].map(|dt| dt > start).unwrap_or(true)
{
self.interval[0][0] = Some(start);
}
if let Some(end) = end
&& self.interval[0][1].map(|dt| dt < end).unwrap_or(true)
{
self.interval[0][1] = Some(end);
}
}
}
}
impl Default for TemporalExtent {
fn default() -> TemporalExtent {
TemporalExtent {
interval: vec![[None, None]],
}
}
}
impl Assets for Collection {
fn assets(&self) -> &IndexMap<String, Asset> {
&self.assets
}
fn assets_mut(&mut self) -> &mut IndexMap<String, Asset> {
&mut self.assets
}
}
impl TryFrom<Collection> for Map<String, Value> {
type Error = Error;
fn try_from(collection: Collection) -> Result<Self> {
match serde_json::to_value(collection)? {
Value::Object(object) => Ok(object),
_ => {
panic!("all STAC collections should serialize to a serde_json::Value::Object")
}
}
}
}
impl TryFrom<Map<String, Value>> for Collection {
type Error = serde_json::Error;
fn try_from(map: Map<String, Value>) -> std::result::Result<Self, Self::Error> {
serde_json::from_value(Value::Object(map))
}
}
impl Migrate for Collection {}
#[cfg(test)]
mod tests {
use super::{Collection, Extent, Provider};
use serde_json::json;
mod collection {
use super::Collection;
use crate::{Bbox, Extent, Links, STAC_VERSION};
use chrono::{DateTime, Utc};
#[test]
fn new() {
let collection = Collection::new("an-id", "a description");
assert!(collection.title.is_none());
assert_eq!(collection.description, "a description");
assert_eq!(collection.license, "other");
assert!(collection.providers.is_none());
assert_eq!(collection.extent, Extent::default());
assert!(collection.summaries.is_none());
assert!(collection.assets.is_empty());
assert_eq!(collection.version, STAC_VERSION);
assert!(collection.extensions.is_empty());
assert_eq!(collection.id, "an-id");
assert!(collection.links.is_empty());
}
#[test]
fn skip_serializing() {
let collection = Collection::new("an-id", "a description");
let value = serde_json::to_value(collection).unwrap();
assert!(value.get("stac_extensions").is_none());
assert!(value.get("title").is_none());
assert!(value.get("keywords").is_none());
assert!(value.get("providers").is_none());
assert!(value.get("summaries").is_none());
assert!(value.get("assets").is_none());
}
#[test]
fn new_from_item() {
let item = crate::read("examples/simple-item.json").unwrap();
let collection = Collection::new_from_item("an-id", "a description", &item);
assert_eq!(
collection.extent.spatial.bbox[0],
Bbox::TwoDimensional([
172.91173669923782,
1.3438851951615003,
172.95469614953714,
1.3690476620161975
])
);
assert_eq!(
collection.extent.temporal.interval[0][0].unwrap(),
"2020-12-11T22:38:32.125000Z"
.parse::<DateTime<Utc>>()
.unwrap()
);
assert_eq!(
collection.extent.temporal.interval[0][1].unwrap(),
"2020-12-11T22:38:32.125000Z"
.parse::<DateTime<Utc>>()
.unwrap()
);
let link = collection.link("item").unwrap();
assert!(link.href.to_string().ends_with("simple-item.json"));
}
}
mod provider {
use super::Provider;
#[test]
fn new() {
let provider = Provider::new("a-name");
assert_eq!(provider.name, "a-name");
assert!(provider.description.is_none());
assert!(provider.roles.is_none());
assert!(provider.url.is_none());
assert!(provider.additional_fields.is_empty());
}
#[test]
fn skip_serializing() {
let provider = Provider::new("an-id");
let value = serde_json::to_value(provider).unwrap();
assert!(value.get("description").is_none());
assert!(value.get("roles").is_none());
assert!(value.get("url").is_none());
}
}
mod extent {
use super::Extent;
use crate::Bbox;
#[test]
fn default() {
let extent = Extent::default();
assert_eq!(
extent.spatial.bbox[0],
Bbox::TwoDimensional([-180.0, -90.0, 180.0, 90.0])
);
assert_eq!(extent.temporal.interval, [[None, None]]);
assert!(extent.additional_fields.is_empty());
}
}
mod roundtrip {
use super::Collection;
use crate::tests::roundtrip;
roundtrip!(collection, "examples/collection.json", Collection);
roundtrip!(
collection_with_schemas,
"examples/collection-only/collection-with-schemas.json",
Collection
);
roundtrip!(
collection_only,
"examples/collection-only/collection.json",
Collection
);
roundtrip!(
extensions_collection,
"examples/extensions-collection/collection.json",
Collection
);
}
#[test]
fn permissive_deserialization() {
let _: Collection = serde_json::from_value(json!({})).unwrap();
}
#[test]
fn has_type() {
let value: serde_json::Value =
serde_json::to_value(Collection::new("an-id", "a description")).unwrap();
assert_eq!(value.as_object().unwrap()["type"], "Collection");
}
}