use serde::{Deserialize, Serialize};
use super::error::FeaturesError;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Link {
pub href: String,
pub rel: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub type_: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hreflang: Option<String>,
}
impl Link {
pub fn new(href: impl Into<String>, rel: impl Into<String>) -> Self {
Self {
href: href.into(),
rel: rel.into(),
type_: None,
title: None,
hreflang: None,
}
}
pub fn with_type(mut self, t: impl Into<String>) -> Self {
self.type_ = Some(t.into());
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LandingPage {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub links: Vec<Link>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConformanceClasses {
pub conforms_to: Vec<String>,
}
impl ConformanceClasses {
pub fn ogc_features_core() -> Self {
Self {
conforms_to: vec![
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core".to_string(),
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30".to_string(),
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html".to_string(),
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson".to_string(),
],
}
}
pub fn with_crs() -> Self {
let mut base = Self::ogc_features_core();
base.conforms_to
.push("http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs".to_string());
base
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpatialExtent {
pub bbox: Vec<[f64; 4]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub crs: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalExtent {
pub interval: Vec<[Option<String>; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trs: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Extent {
#[serde(skip_serializing_if = "Option::is_none")]
pub spatial: Option<SpatialExtent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temporal: Option<TemporalExtent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Collection {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub links: Vec<Link>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extent: Option<Extent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub item_type: Option<String>,
#[serde(default)]
pub crs: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_crs: Option<String>,
}
impl Collection {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
title: None,
description: None,
links: vec![],
extent: None,
item_type: Some("feature".to_string()),
crs: vec![],
storage_crs: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Collections {
pub links: Vec<Link>,
pub collections: Vec<Collection>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum FeatureId {
String(String),
Integer(i64),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Feature {
#[serde(rename = "type")]
pub type_: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<FeatureId>,
pub geometry: Option<serde_json::Value>,
pub properties: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<Vec<Link>>,
}
impl Feature {
pub fn new() -> Self {
Self {
type_: "Feature".to_string(),
id: None,
geometry: None,
properties: None,
links: None,
}
}
}
impl Default for Feature {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FeatureCollection {
#[serde(rename = "type")]
pub type_: String,
pub features: Vec<Feature>,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<Vec<Link>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_stamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub number_matched: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub number_returned: Option<u64>,
}
impl FeatureCollection {
pub fn new() -> Self {
Self {
type_: "FeatureCollection".to_string(),
features: vec![],
links: None,
time_stamp: None,
number_matched: None,
number_returned: None,
}
}
}
impl Default for FeatureCollection {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DateTimeFilter {
Instant(String),
Interval(Option<String>, Option<String>),
}
impl DateTimeFilter {
pub fn parse(s: &str) -> Result<Self, FeaturesError> {
if s.is_empty() {
return Err(FeaturesError::InvalidDatetime(
"datetime value is empty".to_string(),
));
}
if let Some(slash_pos) = s.find('/') {
let start_str = &s[..slash_pos];
let end_str = &s[slash_pos + 1..];
let start = if start_str == ".." || start_str.is_empty() {
None
} else {
Some(start_str.to_string())
};
let end = if end_str == ".." || end_str.is_empty() {
None
} else {
Some(end_str.to_string())
};
Ok(DateTimeFilter::Interval(start, end))
} else {
Ok(DateTimeFilter::Instant(s.to_string()))
}
}
}