diff --git a/CHANGELOG.md b/CHANGELOG.md index c5cc52a..0ab6df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Dynamic OpenAPI definition extraction. +- Types for `OGCAPI - Moving Features` - Async OGC API - Processes execution (jobs). - Multipart/related response support for raw OGC API - Processes results with multiple outputs. - Echo process for testing. diff --git a/ogcapi-types/Cargo.toml b/ogcapi-types/Cargo.toml index a0d385a..8fa5023 100644 --- a/ogcapi-types/Cargo.toml +++ b/ogcapi-types/Cargo.toml @@ -11,7 +11,7 @@ keywords.workspace = true include = ["/src", "/assets"] [features] -default = ["common", "edr", "features", "processes", "tiles"] +default = ["common", "edr", "features", "movingfeatures", "processes", "tiles"] # standards common = [] @@ -22,6 +22,7 @@ stac = ["features"] styles = [] tiles = ["common"] coverages = [] +movingfeatures = ["common", "features"] [dependencies] chrono = { version = "0.4.42", features = ["serde"] } diff --git a/ogcapi-types/src/common/collection.rs b/ogcapi-types/src/common/collection.rs index d05893e..22fb168 100644 --- a/ogcapi-types/src/common/collection.rs +++ b/ogcapi-types/src/common/collection.rs @@ -87,6 +87,14 @@ pub struct Collection { #[cfg(feature = "stac")] #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")] pub assets: std::collections::HashMap, + #[cfg(feature = "movingfeatures")] + #[serde( + default, + rename = "updateFrequency", + skip_serializing_if = "Option::is_none" + )] + /// A time interval of sampling location. The time unit of this property is millisecond. + pub update_frequency: Option, #[serde(flatten, default, skip_serializing_if = "Map::is_empty")] pub additional_properties: Map, } @@ -131,6 +139,8 @@ impl Default for Collection { summaries: Default::default(), #[cfg(feature = "stac")] assets: Default::default(), + #[cfg(feature = "movingfeatures")] + update_frequency: Default::default(), additional_properties: Default::default(), } } diff --git a/ogcapi-types/src/common/extent.rs b/ogcapi-types/src/common/extent.rs index cb06d54..cc46f80 100644 --- a/ogcapi-types/src/common/extent.rs +++ b/ogcapi-types/src/common/extent.rs @@ -73,7 +73,7 @@ impl Default for TemporalExtent { } } -fn serialize_interval( +pub(crate) fn serialize_interval( interval: &Vec<[Option>; 2]>, serializer: S, ) -> Result diff --git a/ogcapi-types/src/features/feature.rs b/ogcapi-types/src/features/feature.rs index 91d56ea..c8f42ae 100644 --- a/ogcapi-types/src/features/feature.rs +++ b/ogcapi-types/src/features/feature.rs @@ -4,6 +4,16 @@ use std::fmt::Display; #[cfg(feature = "stac")] use crate::common::Bbox; + +#[cfg(feature = "movingfeatures")] +use crate::movingfeatures::{ + crs::Crs, temporal_geometry::TemporalGeometry, temporal_properties::TemporalProperties, + trs::Trs, +}; + +#[cfg(feature = "movingfeatures")] +use chrono::{DateTime, Utc}; + use geojson::Geometry; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -69,6 +79,27 @@ pub struct Feature { #[cfg(feature = "stac")] #[serde(default)] pub assets: HashMap, + #[cfg(feature = "movingfeatures")] + #[serde( + default, + serialize_with = "crate::common::serialize_interval", + skip_serializing_if = "Vec::is_empty" + )] + /// Life span information for the moving feature. + /// See [MF-Json 7.2.3 LifeSpan](https://docs.ogc.org/is/19-045r3/19-045r3.html#time) + pub time: Vec<[Option>; 2]>, + #[cfg(feature = "movingfeatures")] + #[serde(default)] + pub crs: Crs, + #[cfg(feature = "movingfeatures")] + #[serde(default)] + pub trs: Trs, + #[cfg(feature = "movingfeatures")] + #[serde(default, rename = "temporalGeometry")] + pub temporal_geometry: Option, + #[cfg(feature = "movingfeatures")] + #[serde(default, rename = "temporalProperties")] + pub temporal_properties: Option, } impl Feature { diff --git a/ogcapi-types/src/features/feature_collection.rs b/ogcapi-types/src/features/feature_collection.rs index 7bd4118..4291ce6 100644 --- a/ogcapi-types/src/features/feature_collection.rs +++ b/ogcapi-types/src/features/feature_collection.rs @@ -4,6 +4,9 @@ use utoipa::ToSchema; use crate::common::Link; +#[cfg(feature = "movingfeatures")] +use crate::common::Bbox; + use super::Feature; #[derive(Serialize, Deserialize, ToSchema, Default, Debug, Clone, Copy, PartialEq, Eq)] @@ -26,6 +29,15 @@ pub struct FeatureCollection { pub time_stamp: Option, pub number_matched: Option, pub number_returned: Option, + #[cfg(feature = "movingfeatures")] + #[serde(default)] + pub crs: crate::movingfeatures::crs::Crs, + #[cfg(feature = "movingfeatures")] + #[serde(default)] + pub trs: crate::movingfeatures::trs::Trs, + #[cfg(feature = "movingfeatures")] + #[serde(default)] + pub bbox: Option, } impl FeatureCollection { diff --git a/ogcapi-types/src/lib.rs b/ogcapi-types/src/lib.rs index 7d7b58a..378f87c 100644 --- a/ogcapi-types/src/lib.rs +++ b/ogcapi-types/src/lib.rs @@ -9,6 +9,9 @@ pub mod edr; /// Types specified in the `OGC API - Features` standard. #[cfg(feature = "features")] pub mod features; +/// Types specified in the `OGC API - Moving Features` standard. +#[cfg(feature = "movingfeatures")] +pub mod movingfeatures; /// Types specified in the `OGC API - Processes` standard. #[cfg(feature = "processes")] pub mod processes; diff --git a/ogcapi-types/src/movingfeatures/crs.rs b/ogcapi-types/src/movingfeatures/crs.rs new file mode 100644 index 0000000..bec0e35 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/crs.rs @@ -0,0 +1,93 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::common; + +/// MF-JSON uses a CRS as described in in (GeoJSON:2008)[https://geojson.org/geojson-spec#coordinate-reference-system-objects] +/// See (7.2.3 CoordinateReferenceSystem Object)[https://docs.ogc.org/is/19-045r3/19-045r3.html#crs] +/// See (6. Overview of Moving features JSON Encodings)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_overview_of_moving_features_json_encodings_informative] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] +#[serde(tag = "type", content = "properties")] +pub enum Crs { + /// A Named CRS object indicates a coordinate reference system by name. In this case, the value of its "type" member + /// is the string "Name". The value of its "properties" member is a JSON object containing a "name" member whose + /// value is a string identifying a coordinate reference system (not JSON null value). The value of "href" and "type" + /// is a JSON null value. This standard recommends an EPSG[3] code as the value of "name", such as "EPSG::4326." + /// + /// See (7.2.3.1 Named CRS)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_1_named_crs] + Name { name: String }, + /// A linked CRS object has one required member "href" and one optional member "type". The value of the required "href" + /// member is a dereferenceable URI. The value of the optional "type" member is a string that hints at the format used + /// to represent CRS parameters at the provided URI. Suggested values are: "Proj4", "OGCWKT", "ESRIWKT", but others can + /// be used. + /// + /// See (7.2.3.2. Linked CRS)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_2_linked_crs] + Link { + r#type: Option, + href: String, + }, +} + +impl Default for Crs { + fn default() -> Self { + Self::Name { + name: common::Crs::default().to_urn(), + } + } +} + +impl TryFrom for common::Crs { + type Error = String; + + fn try_from(value: Crs) -> Result { + match value { + // TODO this might not work for names like "EPSG:4326" + Crs::Name { name } => Self::from_str(name.as_str()), + Crs::Link { href, .. } => Self::from_str(href.as_str()), + } + } +} + +impl From for Crs { + fn from(value: common::Crs) -> Self { + Self::Name { + name: value.to_urn(), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn serde_json() { + // TODO this contradicts example from https://developer.ogc.org/api/movingfeatures/index.html#tag/MovingFeatures/operation/retrieveMovingFeatures + // Example from https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_1_named_crs + let trs: Crs = serde_json::from_str( + r#" + { + "type": "Name", + "properties": { + "name": "urn:ogc:def:crs:OGC:1.3:CRS84" + } + } + "#, + ) + .expect("Failed to parse Crs"); + let expected_trs = Crs::default(); + assert_eq!(trs, expected_trs); + } + + #[test] + fn into_common_crs() { + // assert_eq!(common::Crs::try_from(Crs::default()).unwrap(), common::Crs::default()); + assert_eq!(common::Crs::default(), Crs::default().try_into().unwrap()); + + // assert_eq!(Crs::from(common::Crs::default()), Crs::default()); + assert_eq!(Crs::default(), common::Crs::default().into()); + } +} diff --git a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs new file mode 100644 index 0000000..21f01a7 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs @@ -0,0 +1,181 @@ +use std::collections::HashMap; + +use chrono::{DateTime, FixedOffset}; +use serde::{Deserialize, Serialize, Serializer, ser::Error}; +use serde_json::json; +use utoipa::ToSchema; + +use super::temporal_property::Interpolation; + +/// MF-JSON TemporalProperties +/// +/// A TemporalProperties object is a JSON array of ParametricValues objects that groups a collection of dynamic +/// non-spatial attributes and its parametric values with time. +/// +/// See [7.2.2 MF-JSON TemporalProperties](https://docs.ogc.org/is/19-045r3/19-045r3.html#tproperties) +/// +/// Opposed to [TemporalProperty](super::temporal_property::TemporalProperty) values for all +/// represented properties are all measured at the same points in time. +#[derive(Deserialize, Debug, Clone, PartialEq, ToSchema)] +pub struct MFJsonTemporalProperties { + datetimes: Vec>, + #[serde(flatten)] + values: HashMap, +} + +impl MFJsonTemporalProperties { + pub fn new( + datetimes: Vec>, + values: HashMap, + ) -> Result { + let dt_len = datetimes.len(); + if values.values().any(|property| property.len() != dt_len) { + Err("all values and datetimes must be of same length") + } else { + Ok(Self { datetimes, values }) + } + } + + pub fn datetimes(&self) -> &[DateTime] { + &self.datetimes + } + + pub fn values(&self) -> &HashMap { + &self.values + } +} + +impl Serialize for MFJsonTemporalProperties { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let dt_len = self.datetimes.len(); + if self + .values + .values() + .any(|property| property.len() != dt_len) + { + Err(S::Error::custom( + "all values and datetimes must be of same length", + )) + } else { + let value = json!(self); + value.serialize(serializer) + } + } +} + +/// A ParametricValues object is a JSON object that represents a collection of parametric values of dynamic non-spatial +/// attributes that are ascertained at the same times. A parametric value may be a time-varying measure, a sequence of +/// texts, or a sequence of images. Even though the parametric value may depend on the spatiotemporal location, +/// MF-JSON Prism only considers the temporal dependencies of their changes of value. +/// +/// See [7.2.2.1 MF-JSON ParametricValues](https://docs.ogc.org/is/19-045r3/19-045r3.html#pvalues) +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)] +#[serde(tag = "type")] +pub enum ParametricValues { + /// The "values" member contains any numeric values. + Measure { + values: Vec, + /// Allowed Interpolations: Discrete, Step, Linear, Regression + interpolation: Option, + description: Option, + /// The "form" member is optional and its value is a JSON string as a common code (3 characters) described in + /// the [Code List Rec 20 by the UN Centre for Trade Facilitation and Electronic Business (UN/CEFACT)](https://www.unece.org/uncefact/codelistrecs.html) or a + /// URL specifying the unit of measurement. This member is applied only for a temporal property whose value + /// type is Measure. + form: Option, + }, + /// The "values" member contains any strings. + Text { + values: Vec, + /// Allowed Interpolations: Discrete, Step + // TODO enforce? + interpolation: Option, + description: Option, + }, + /// The "values" member contains Base64 strings converted from images or URLs to address images. + Image { + values: Vec, + /// Allowed Interpolations: Discrete, Step + // TODO enforce? + interpolation: Option, + description: Option, + }, +} + +impl ParametricValues { + fn len(&self) -> usize { + match self { + Self::Measure { values, .. } => values.len(), + Self::Text { values, .. } => values.len(), + Self::Image { values, .. } => values.len(), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn serde_mfjson_temporal_properties() { + // https://developer.ogc.org/api/movingfeatures/index.html#tag/TemporalProperty/operation/insertTemporalProperty + let tp_json = r#"[ + { + "datetimes": [ + "2011-07-14T22:01:01.450Z", + "2011-07-14T23:01:01.450Z", + "2011-07-15T00:01:01.450Z" + ], + "length": { + "type": "Measure", + "form": "http://qudt.org/vocab/quantitykind/Length", + "values": [ + 1, + 2.4, + 1 + ], + "interpolation": "Linear" + }, + "discharge": { + "type": "Measure", + "form": "MQS", + "values": [ + 3, + 4, + 5 + ], + "interpolation": "Step" + } + }, + { + "datetimes": [ + "2011-07-14T22:01:01.450Z", + "2011-07-14T23:01:01.450Z" + ], + "camera": { + "type": "Image", + "values": [ + "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/image1", + "iVBORw0KGgoAAAANSUhEU......" + ], + "interpolation": "Discrete" + }, + "labels": { + "type": "Text", + "values": [ + "car", + "human" + ], + "interpolation": "Discrete" + } + } + ]"#; + + let _: Vec = + serde_json::from_str(tp_json).expect("Failed to parse MF-JSON Temporal Properties"); + } +} diff --git a/ogcapi-types/src/movingfeatures/mod.rs b/ogcapi-types/src/movingfeatures/mod.rs new file mode 100644 index 0000000..a2913b0 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/mod.rs @@ -0,0 +1,10 @@ +pub mod temporal_complex_geometry; +pub mod temporal_geometry; +pub mod temporal_primitive_geometry; + +pub mod mfjson_temporal_properties; +pub mod temporal_properties; +pub mod temporal_property; + +pub mod crs; +pub mod trs; diff --git a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs new file mode 100644 index 0000000..974b92c --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::{crs::Crs, temporal_primitive_geometry::TemporalPrimitiveGeometry, trs::Trs}; + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, ToSchema)] +pub enum Type { + #[default] + MovingGeometryCollection, +} + +/// A TemporalComplexGeometry object represents a set of TemporalPrimitiveGeometry objects. When a TemporalGeometry +/// object has a "type" member is "MovingGeometryCollection", the object is specialized as a TemporalComplexGeometry +/// object with one additional mandatory member named "prisms". The value of the "prisms" member is represented by a +/// JSON array of a set of TemporalPrimitiveGeometry instances having at least one element in the array. +/// +/// See [7.2.1.2. TemporalComplexGeometry Object](https://docs.ogc.org/is/19-045r3/19-045r3.html#tcomplex) +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] +pub struct TemporalComplexGeometry { + pub r#type: Type, + pub prisms: Vec, + pub crs: Option, + pub trs: Option, +} + +impl From> for TemporalComplexGeometry { + fn from(value: Vec) -> Self { + debug_assert!(!value.is_empty()); + Self { + r#type: Default::default(), + prisms: value, + crs: Default::default(), + trs: Default::default(), + } + } +} diff --git a/ogcapi-types/src/movingfeatures/temporal_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_geometry.rs new file mode 100644 index 0000000..4af7984 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_geometry.rs @@ -0,0 +1,79 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::{ + temporal_complex_geometry::TemporalComplexGeometry, + temporal_primitive_geometry::TemporalPrimitiveGeometry, +}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] +#[serde(untagged)] +pub enum TemporalGeometry { + Primitive(TemporalPrimitiveGeometry), + Complex(TemporalComplexGeometry), +} + +#[cfg(test)] +mod tests { + + use chrono::DateTime; + + use super::*; + + #[test] + fn moving_complex_geometry_from_json_value() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let primitive_geometry = + TemporalPrimitiveGeometry::new((datetimes, coordinates).try_into().unwrap()); + let geometry = TemporalGeometry::Complex(TemporalComplexGeometry::from(vec![ + primitive_geometry.clone(), + primitive_geometry, + ])); + let deserialized_geometry: TemporalGeometry = serde_json::from_str( + r#"{ + "type": "MovingGeometryCollection", + "prisms": [ + { + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }, + { + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + } + ] + }"#, + ) + .unwrap(); + assert_eq!(geometry, deserialized_geometry); + } + + #[test] + fn moving_primitive_geometry_from_json_value() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let geometry: TemporalGeometry = TemporalGeometry::Primitive( + TemporalPrimitiveGeometry::new((datetimes, coordinates).try_into().unwrap()), + ); + let deserialized_geometry: TemporalGeometry = serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }"#, + ) + .unwrap(); + assert_eq!(geometry, deserialized_geometry); + } +} diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs new file mode 100644 index 0000000..0bf175c --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -0,0 +1,361 @@ +use chrono::{DateTime, Utc}; +use geojson::{LineStringType, PointType, PolygonType}; +use serde::{Deserialize, Serialize, Serializer, ser}; +use serde_json::json; +use utoipa::ToSchema; + +use super::{crs::Crs, trs::Trs}; + +/// TemporalPrimitiveGeometry Object +/// +/// A [TemporalPrimitiveGeometry](https://docs.ogc.org/is/19-045r3/19-045r3.html#tprimitive) object describes the +/// movement of a geographic feature whose leaf geometry at a time instant is drawn by a primitive geometry such as a +/// point, linestring, and polygon in the two- or three-dimensional spatial coordinate system, or a point cloud in the +/// three-dimensional spatial coordinate system. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] +pub struct TemporalPrimitiveGeometry { + pub id: Option, + #[serde(flatten)] + pub value: Value, + #[serde(default)] + pub interpolation: Interpolation, + pub crs: Option, + pub trs: Option, +} + +impl TemporalPrimitiveGeometry { + pub fn new(value: Value) -> Self { + Self { + id: None, + value, + interpolation: Interpolation::default(), + crs: Default::default(), + trs: Default::default(), + } + } +} + +impl From for TemporalPrimitiveGeometry +where + V: Into, +{ + fn from(v: V) -> TemporalPrimitiveGeometry { + TemporalPrimitiveGeometry::new(v.into()) + } +} + +///The value specifies the variants of a TemporalPrimitiveGeometry object with constraints on the interpretation of the +///array value of the "coordinates" member, the same-length "datetimes" array member and the optional members "base" and +///"orientations". +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Value { + ///The type represents a trajectory of a time-parametered 0-dimensional (0D) geometric primitive (Point), + ///representing a single position at a time position (instant) within its temporal domain. Intuitively a temporal + ///geometry of a continuous movement of point depicts a set of curves in a spatiotemporal domain. + ///It supports more complex movements of moving features, as well as linear movement like MF-JSON Trajectory. + ///For example, the non-linear movement information of people, vehicles, or hurricanes can be shared as a + ///TemporalPrimitiveGeometry object with the "MovingPoint" type. + MovingPoint { + #[serde(flatten)] + dt_coords: DateTimeCoords, PointType>, + #[serde(flatten)] + base_representation: Option, + }, + ///The type represents the prism of a time-parametered 1-dimensional (1D) geometric primitive (LineString), whose + ///leaf geometry at a time position is a 1D linear object in a particular period. Intuitively a temporal geometry + ///of a continuous movement of curve depicts a set of surfaces in a spatiotemporal domain. For example, the + ///movement information of weather fronts or traffic congestion on roads can be shared as a + ///TemporalPrimitiveGeometry object with the "MovingLineString" type. + MovingLineString { + #[serde(flatten)] + dt_coords: DateTimeCoords, LineStringType>, + }, + ///The type represents the prism of a time-parameterized 2-dimensional (2D) geometric primitive (Polygon), whose + ///leaf geometry at a time position is a 2D polygonal object in a particular period. The list of points are in + ///counterclockwise order. Intuitively a temporal geometry of a continuous movement of polygon depicts a set of + ///volumes in a spatiotemporal domain. For example, the changes of flooding areas or the movement information of + ///air pollution can be shared as a TemporalPrimitiveGeometry object with the "MovingPolygon" type. + MovingPolygon { + #[serde(flatten)] + dt_coords: DateTimeCoords, PolygonType>, + }, + ///The type represents the prism of a time-parameterized point cloud whose leaf geometry at a time position is a + ///set of points in a particular period. Intuitively a temporal geometry of a continuous movement of point set + ///depicts a set of curves in a spatiotemporal domain. For example, the tacking information by using Light + ///Detection and Ranging (LiDAR) can be shared as a TemporalPrimitiveGeometry object with the "MovingPointCloud" + ///type. + MovingPointCloud { + #[serde(flatten)] + dt_coords: DateTimeCoords, Vec>, + }, +} + +impl TryFrom<(Vec>, Vec)> for Value { + type Error = String; + fn try_from(value: (Vec>, Vec)) -> Result { + let dt_coords = DateTimeCoords::new(value.0, value.1)?; + Ok(Self::MovingPoint { + dt_coords, + base_representation: None, + }) + } +} + +#[derive(Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[serde(try_from = "DateTimeCoordsUnchecked")] +pub struct DateTimeCoords { + datetimes: Vec, + coordinates: Vec, +} + +impl DateTimeCoords { + pub fn new(datetimes: Vec, coordinates: Vec) -> Result { + if coordinates.len() != datetimes.len() { + Err("coordinates and datetimes must be of same length") + } else { + Ok(Self { + datetimes, + coordinates, + }) + } + } + + pub fn append(&mut self, other: &mut Self) { + self.datetimes.append(&mut other.datetimes); + self.coordinates.append(&mut other.coordinates); + } + + pub fn datetimes(&self) -> &[A] { + self.datetimes.as_slice() + } + + pub fn coordinates(&self) -> &[B] { + self.coordinates.as_slice() + } +} + +#[derive(Deserialize)] +struct DateTimeCoordsUnchecked { + datetimes: Vec, + coordinates: Vec, +} + +impl TryFrom> for DateTimeCoords { + type Error = &'static str; + + fn try_from(value: DateTimeCoordsUnchecked) -> Result { + DateTimeCoords::new(value.datetimes, value.coordinates) + } +} + +impl Serialize for DateTimeCoords { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.coordinates.len() != self.datetimes.len() { + Err(ser::Error::custom( + "coordinates and datetimes must be of same length", + )) + } else { + let value = json!(self); + value.serialize(serializer) + } + } +} + +///MF-JSON Prism separates out translational motion and rotational motion. The "interpolation" member is default and +///represents the translational motion of the geometry described by the "coordinates" value. Its value is a MotionCurve +///object described by one of predefined five motion curves (i.e., "Discrete", "Step", "Linear", "Quadratic", and +///"Cubic") or a URL (e.g., "") +/// +///See [7.2.10 MotionCurve Objects](https://docs.ogc.org/is/19-045r3/19-045r3.html#interpolation) +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, ToSchema)] +pub enum Interpolation { + ///The positions are NOT connected. The position is valid only at the time instant in datetimes + Discrete, + ///This method is realized as a jump from one position to the next at the end of a subinterval. The curve is not + ///continuous but would be useful for representing an accident or event. This interpolation requires at least two + ///positions. + Step, + ///This method is the default value of the "interpolation" member. It connects straight lines between positions. + ///The position with respect to time is constructed from linear splines that are two–positions interpolating + ///polynomials. Therefore, this interpolation also requires at least two positions. + #[default] + Linear, + ///This method interpolates the position at time t by using a piecewise quadratic spline on each interval [t_{-1},t] + ///with first-order parametric continuity. Between consecutive positions, piecewise quadratic splines are constructed + ///from the following parametric equations in terms of the time variable. This method results in a curve of a + ///temporal trajectory that is continuous and has a continuous first derivative at the positions in coordinates + ///except the two end positions. For this interpolation, at least three leaves at particular times are required. + Quadratic, + ///This method interpolates the position at time t by using a Catmull–Rom (cubic) spline on each interval [t_{-1},t]. + /// + ///See [7.2.10 MotionCurve Objects](https://docs.ogc.org/is/19-045r3/19-045r3.html#interpolation) + Cubic, + ///If applications need to define their own interpolation methods, the "interpolation" member in the + ///TemporalPrimitiveGeometry object has a URL to a JSON array of parametric equations defined on a set of intervals of parameter t-value. + /// + ///See [7.2.10.2 URLs for user-defined parametric curve](https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_10_2_urls_for_user_defined_parametric_curve) + Url(String), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct BaseRepresentation { + base: Base, + orientations: Vec, +} + +///The 3D model represents a base geometry of a 3D shape, and the combination of the "base" and "orientations" members +///represents a 3D temporal geometry of the MF_RigidTemporalGeometry type in ISO 19141. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Base { + ///The "type" member has a JSON string to represent a 3D File format such as STL, OBJ, PLY, and glTF. + r#type: String, + ///The "href" member has a URL to address 3D model data. + href: String, +} + +///Orientations represents rotational motion of the base representation of a member named "base" +///as a transform matrix of the base representation at each time of the elements in "datetimes". +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Orientation { + ///The "scales" member has a array value of numbers along the x, y and z axis in order as three scale factors. + scales: [f64; 3], + ///the "angles" member has a JSON array value of numbers along the x, y and z axis in order as Euler angles in degree. + ///Angles are defined according to the right-hand rule; a positive value represents a rotation that appears clockwise + ///when looking in the positive direction of the axis and a negative value represents a counter-clockwise rotation. + angles: [f64; 3], +} +#[cfg(test)] +mod tests { + + use geojson::JsonObject; + + use super::*; + + #[test] + fn from_json_object() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let moving_point = Value::MovingPoint { + dt_coords: DateTimeCoords::new(datetimes, coordinates).unwrap(), + base_representation: None, + }; + let jo: JsonObject = serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }"#, + ) + .unwrap(); + assert_eq!(moving_point, serde_json::from_value(jo.into()).unwrap()); + } + + #[test] + fn moving_geometry_from_json_value() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let geometry: TemporalPrimitiveGeometry = + TemporalPrimitiveGeometry::new(Value::MovingPoint { + dt_coords: DateTimeCoords::new(datetimes, coordinates).unwrap(), + base_representation: None, + }); + let deserialized_geometry: TemporalPrimitiveGeometry = serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }"#, + ) + .unwrap(); + assert_eq!(geometry, deserialized_geometry); + } + + #[test] + fn invalid_moving_geometry_from_json_value() { + let geometry_too_few_datetimes: Result = + serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z"] + }"#, + ); + assert!(geometry_too_few_datetimes.is_err()); + + let geometry_too_few_coordinates: Result = + serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }"#, + ); + assert!(geometry_too_few_coordinates.is_err()) + } + + #[test] + fn moving_base_rep_geometry_from_json_value() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + let orientations = vec![ + Orientation { + scales: [1.0, 1.0, 1.0], + angles: [0.0, 0.0, 0.0], + }, + Orientation { + scales: [1.0, 1.0, 1.0], + angles: [0.0, 355.0, 0.0], + }, + Orientation { + scales: [1.0, 1.0, 1.0], + angles: [0.0, 0.0, 330.0], + }, + ]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let geometry: TemporalPrimitiveGeometry = TemporalPrimitiveGeometry::new( + Value::MovingPoint{ + dt_coords: DateTimeCoords::new(datetimes, coordinates).unwrap(), + base_representation: Some(BaseRepresentation{ + base: Base{ + r#type: "glTF".to_string(), + href: "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/car3dmodel.gltf".to_string() + }, + orientations + }), + }); + let deserialized_geometry: TemporalPrimitiveGeometry = serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"], + "interpolation": "Linear", + "base": { + "type": "glTF", + "href": "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/car3dmodel.gltf" + }, + "orientations": [ + {"scales":[1,1,1], "angles":[0,0,0]}, + {"scales":[1,1,1], "angles":[0,355,0]}, + {"scales":[1,1,1], "angles":[0,0,330]} + ] + }"# + ) + .unwrap(); + assert_eq!(geometry, deserialized_geometry); + } +} diff --git a/ogcapi-types/src/movingfeatures/temporal_properties.rs b/ogcapi-types/src/movingfeatures/temporal_properties.rs new file mode 100644 index 0000000..197bf9a --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_properties.rs @@ -0,0 +1,140 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::common::Link; + +use super::{ + mfjson_temporal_properties::MFJsonTemporalProperties, temporal_property::TemporalProperty, +}; + +/// A TemporalProperties object consists of the set of [TemporalProperty] or a set of [MFJsonTemporalProperties]. +/// +/// See [8.8 TemporalProperties](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalProperties-section) +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TemporalProperties { + pub temporal_properties: TemporalPropertiesValue, + pub links: Option>, + pub time_stamp: Option, + pub number_matched: Option, + pub number_returned: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] +#[serde(untagged)] +pub enum TemporalPropertiesValue { + /// [MFJsonTemporalProperties] allows to represent multiple property values all measured at the same points in time. + MFJsonTemporalProperties(Vec), + /// [TemporalProperty] allows to represent a property value at independent points in time. + TemporalProperty(Vec), +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use chrono::DateTime; + + use crate::{ + common::Link, + movingfeatures::{ + mfjson_temporal_properties::ParametricValues, temporal_property::Interpolation, + }, + }; + + use super::*; + + #[test] + fn serde_temporal_properties() { + let links: Vec = vec![ + Link::new("https://data.example.org/collections/mfc-1/items/mf-1/tproperties","self").mediatype("application/json"), + Link::new("https://data.example.org/collections/mfc-1/items/mf-1/tproperties&offset=2&limit=2","next").mediatype("application/json"), + ]; + + let datetimes = vec![ + DateTime::parse_from_rfc3339("2011-07-14T22:01:06.000Z").unwrap(), + DateTime::parse_from_rfc3339("2011-07-14T22:01:07.000Z").unwrap(), + DateTime::parse_from_rfc3339("2011-07-14T22:01:08.000Z").unwrap(), + ]; + + let values = HashMap::from([ + ( + "length".to_string(), + ParametricValues::Measure { + values: vec![1.0, 2.4, 1.0], + interpolation: Some(Interpolation::Linear), + description: None, + form: Some("http://qudt.org/vocab/quantitykind/Length".to_string()), + }, + ), + ( + "speed".to_string(), + ParametricValues::Measure { + values: vec![65.0, 70.0, 80.0], + interpolation: Some(Interpolation::Linear), + form: Some("KMH".to_string()), + description: None, + }, + ), + ]); + + let temporal_properties = TemporalProperties { + temporal_properties: TemporalPropertiesValue::MFJsonTemporalProperties(vec![ + MFJsonTemporalProperties::new(datetimes, values).unwrap(), + ]), + links: Some(links), + time_stamp: Some("2021-09-01T12:00:00Z".into()), + number_matched: Some(10), + number_returned: Some(2), + }; + + // https://developer.ogc.org/api/movingfeatures/index.html#tag/TemporalProperty/operation/retrieveTemporalProperties + let tp_json = r#"{ + "temporalProperties": [ + { + "datetimes": [ + "2011-07-14T22:01:06.000Z", + "2011-07-14T22:01:07.000Z", + "2011-07-14T22:01:08.000Z" + ], + "length": { + "type": "Measure", + "form": "http://qudt.org/vocab/quantitykind/Length", + "values": [ + 1, + 2.4, + 1 + ], + "interpolation": "Linear" + }, + "speed": { + "type": "Measure", + "form": "KMH", + "values": [ + 65, + 70, + 80 + ], + "interpolation": "Linear" + } + } + ], + "links": [ + { + "href": "https://data.example.org/collections/mfc-1/items/mf-1/tproperties", + "rel": "self", + "type": "application/json" + }, + { + "href": "https://data.example.org/collections/mfc-1/items/mf-1/tproperties&offset=2&limit=2", + "rel": "next", + "type": "application/json" + } + ], + "timeStamp": "2021-09-01T12:00:00Z", + "numberMatched": 10, + "numberReturned": 2 + }"#; + assert_eq!(temporal_properties, serde_json::from_str(tp_json).unwrap()); + } +} diff --git a/ogcapi-types/src/movingfeatures/temporal_property.rs b/ogcapi-types/src/movingfeatures/temporal_property.rs new file mode 100644 index 0000000..58fb9d6 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_property.rs @@ -0,0 +1,154 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize, Serializer, ser::Error}; +use serde_json::json; +use utoipa::ToSchema; + +use crate::common::Link; + +/// A temporal property object is a collection of dynamic non-spatial attributes and their temporal values with time. +/// An abbreviated copy of this information is returned for each TemporalProperty in the +/// [{root}/collections/{collectionId}/items/{mFeatureId}/tproperties](super::temporal_properties::TemporalProperties) response. +/// The schema for the temporal property object presented in this clause is an extension of the [ParametricValues Object](https://docs.opengeospatial.org/is/19-045r3/19-045r3.html#tproperties) defined in [MF-JSON](https://docs.ogc.org/is/22-003r3/22-003r3.html#OGC_19-045r3). +/// +/// See [8.9. TemporalProperty](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalProperty-section) +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TemporalProperty { + /// An identifier for the resource assigned by an external entity. + pub name: String, + #[serde(flatten)] + pub value_sequence: TemporalPropertyValue, + /// A unit of measure + pub form: Option, + /// A short description + pub description: Option, + pub links: Option>, +} + +/// A predefined temporal property type. +/// +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)] +#[serde(tag = "type", content = "valueSequence")] +pub enum TemporalPropertyValue { + TBoolean(Vec>), + TText(Vec>), + TInteger(Vec>), + TReal(Vec>), + TImage(Vec>), +} + +/// The TemporalPrimitiveValue resource represents the dynamic change of a non-spatial attribute’s value with time. An +/// abbreviated copy of this information is returned for each TemporalPrimitiveValue in the +/// {root}/collections/{collectionId}/items/{mFeatureId}/tproperties/{tPropertyName} response. +/// +/// See [8.10. TemporalPrimitiveValue](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalPrimitiveValue-section) +#[derive(Deserialize, Debug, Default, Clone, PartialEq, ToSchema)] +#[serde(try_from = "TemporalPrimitiveValueUnchecked")] +pub struct TemporalPrimitiveValue { + /// A unique identifier to the temporal primitive value. + // TODO mandatory according to https://docs.ogc.org/is/22-003r3/22-003r3.html#_overview_13 + // but missing in response sample at https://developer.ogc.org/api/movingfeatures/index.html#tag/TemporalProperty/operation/retrieveTemporalProperty + pub id: Option, + /// A sequence of monotonic increasing instants. + pub datetimes: Vec>, + /// A sequence of dynamic values having the same number of elements as “datetimes”. + pub values: Vec, + /// A predefined type for a dynamic value (i.e., one of ‘Discrete’, ‘Step’, ‘Linear’, or ‘Regression’). + pub interpolation: Interpolation, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +struct TemporalPrimitiveValueUnchecked { + id: Option, + datetimes: Vec>, + values: Vec, + interpolation: Interpolation, +} + +impl TryFrom> for TemporalPrimitiveValue { + type Error = &'static str; + + fn try_from(value: TemporalPrimitiveValueUnchecked) -> Result { + if value.values.len() != value.datetimes.len() { + Err("values and datetimes must be of same length") + } else { + Ok(Self { + id: value.id, + interpolation: value.interpolation, + datetimes: value.datetimes, + values: value.values, + }) + } + } +} + +impl Serialize for TemporalPrimitiveValue { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.values.len() != self.datetimes.len() { + Err(S::Error::custom( + "values and datetimes must be of same length", + )) + } else { + let value = json!(self); + value.serialize(serializer) + } + } +} + +/// See [ParametricValues Object -> "interpolation"](https://docs.opengeospatial.org/is/19-045r3/19-045r3.html#tproperties) +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, ToSchema)] +pub enum Interpolation { + /// The sampling of the attribute occurs such that it is not possible to regard the series as continuous; thus, + /// there is no interpolated value if t is not an element in "datetimes". + #[default] + Discrete, + /// The values are not connected at the end of a subinterval with two successive instants. The value just jumps from + /// one value to the other at the end of a subinterval. + Step, + /// The values are essentially connected and a linear interpolation estimates the value of the property at the + /// indicated instant during a subinterval. + Linear, + /// The value of the attribute at the indicated instant is extrapolated from a simple linear regression model with + /// the whole values corresponding to the all elements in "datetimes". + Regression, + /// For a URL, this standard refers to the [InterpolationCode Codelist](http://docs.opengeospatial.org/is/15-042r3/15-042r3.html#75) defined in [OGC TimeseriesML 1.0](http://docs.opengeospatial.org/is/15-042r3/15-042r3.html)[OGC 15-042r3] + /// between neighboring points in a timeseries, e.g., "", + /// "", and etc. + Url(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde_temporal_property() { + // https://developer.ogc.org/api/movingfeatures/index.html#tag/TemporalProperty/operation/retrieveTemporalProperty + let tp_json = r#"{ + "name": "speed", + "type": "TReal", + "form": "KMH", + "valueSequence": [ + { + "datetimes": [ + "2011-07-15T08:00:00Z", + "2011-07-15T08:00:01Z", + "2011-07-15T08:00:02Z" + ], + "values": [ + 0, + 20, + 50 + ], + "interpolation": "Linear" + } + ] + }"#; + + let _: TemporalProperty = + serde_json::from_str(tp_json).expect("Failed to deserialize Temporal Property"); + } +} diff --git a/ogcapi-types/src/movingfeatures/trs.rs b/ogcapi-types/src/movingfeatures/trs.rs new file mode 100644 index 0000000..9794b20 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/trs.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] +#[serde(tag = "type", content = "properties")] +pub enum Trs { + Name { + name: String, + }, // r#type: String, + Link { + r#type: Option, + href: String, + }, // r#type: String, + // properties: TrsProperties, +} + +impl Default for Trs { + fn default() -> Self { + Self::Name { + name: "urn:ogc:data:time:iso8601".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn serde_json() { + // TODO this contradicts example from https://developer.ogc.org/api/movingfeatures/index.html#tag/MovingFeatures/operation/retrieveMovingFeatures + // Example from https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_1_named_crs + let trs: Trs = serde_json::from_str( + r#" + { + "type": "Name", + "properties": {"name": "urn:ogc:data:time:iso8601"} + } + "#, + ) + .expect("Failed to parse Trs"); + let expected_trs = Trs::default(); + assert_eq!(trs, expected_trs); + } +}