omf/
project.rs

1use chrono::{DateTime, Utc};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{
7    Element, Vector3,
8    date_time::utc_now,
9    geometry::zero_origin,
10    validate::{Validate, Validator},
11};
12
13/// Root object of an OMF file.
14///
15/// This is the root element of an OMF file, holding global metadata and a list of
16/// [Elements](crate::Element) that describe the objects or shapes within the file.
17#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
18pub struct Project {
19    /// Project name.
20    #[serde(default, skip_serializing_if = "String::is_empty")]
21    pub name: String,
22    /// Optional project description.
23    #[serde(default, skip_serializing_if = "String::is_empty")]
24    pub description: String,
25    /// Optional [EPSG](https://epsg.io/) or [PROJ](https://proj.org/) local transformation
26    /// string, default empty.
27    ///
28    /// Exactly what is supported depends on the application reading the file.
29    #[serde(default, skip_serializing_if = "String::is_empty")]
30    pub coordinate_reference_system: String,
31    /// Optional unit for distances and locations within the file.
32    ///
33    /// Typically "meters", "metres", "feet", or empty because the coordinate reference system
34    /// defines it. If both are empty then applications may assume meters.
35    #[serde(default, skip_serializing_if = "String::is_empty")]
36    pub units: String,
37    /// Optional project origin, default [0, 0, 0].
38    ///
39    /// Most geometries also have their own origin field. To get the real location add this
40    /// origin and the geometry origin to all locations within each element.
41    #[serde(default, skip_serializing_if = "zero_origin")]
42    pub origin: Vector3,
43    /// Optional name or email address of the person that created the file, default empty.
44    #[serde(default, skip_serializing_if = "String::is_empty")]
45    pub author: String,
46    /// Optional name and version of the application that created the file, default empty.
47    #[serde(default, skip_serializing_if = "String::is_empty")]
48    pub application: String,
49    /// File or data creation date. Defaults to the current date and time on creation.
50    pub date: DateTime<Utc>,
51    /// Arbitrary metadata.
52    ///
53    /// This is the place to put anything that doesn't fit in the other fields.
54    /// Application-specific data should use a prefix that identifies the application, like
55    /// `"lf-something"` for Leapfrog.
56    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
57    pub metadata: serde_json::Map<String, Value>,
58    /// List of elements.
59    #[serde(default)]
60    pub elements: Vec<Element>,
61}
62
63impl Project {
64    /// Create a new project with just the name set.
65    pub fn new(name: impl Into<String>) -> Self {
66        Self {
67            name: name.into(),
68            ..Default::default()
69        }
70    }
71}
72
73impl Default for Project {
74    fn default() -> Self {
75        Self {
76            name: Default::default(),
77            description: Default::default(),
78            coordinate_reference_system: Default::default(),
79            units: Default::default(),
80            origin: Default::default(),
81            author: Default::default(),
82            application: Default::default(),
83            date: utc_now(),
84            metadata: Default::default(),
85            elements: Default::default(),
86        }
87    }
88}
89
90impl Validate for Project {
91    fn validate_inner(&mut self, val: &mut Validator) {
92        val.enter("Project")
93            .name(&self.name)
94            .finite_seq(self.origin, "origin")
95            .objs(&mut self.elements)
96            .unique(
97                self.elements.iter().map(|e| &e.name),
98                "elements[..]::name",
99                false,
100            );
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use std::str::FromStr;
107
108    use serde_json::Value;
109
110    use super::*;
111
112    #[test]
113    fn serde_empty_project() {
114        let mut p = Project::new("Test");
115        p.name = "Foo".to_owned();
116        p.units = "meters".to_owned();
117        p.origin = [1e6, 0.0, 0.0];
118        p.date = chrono::DateTime::from_str("2022-10-31T09:00:00.594Z").unwrap();
119        p.metadata.insert("other".to_owned(), Value::Bool(true));
120        let s = serde_json::to_string(&p).unwrap();
121        let q = serde_json::from_str(&s).unwrap();
122        assert_eq!(p, q);
123        assert_eq!(
124            s,
125            concat!(
126                r#"{"name":"Foo","units":"meters","origin":[1000000.0,0.0,0.0],"#,
127                r#""date":"2022-10-31T09:00:00.594Z","metadata":{"other":true},"#,
128                r#""elements":[]}"#
129            )
130        );
131    }
132}