omf/
attribute.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5use crate::{
6    Array, NumberColormap, Orient2,
7    array::Constraint,
8    array_type,
9    colormap::NumberRange,
10    validate::{Validate, Validator},
11};
12
13/// The various types of data that can be attached to an [`Attribute`](crate::Attribute).
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
15#[serde(tag = "type")]
16pub enum AttributeData {
17    /// Number data with flexible type.
18    ///
19    /// Values can be stored as 32 or 64-bit signed integers, 32 or 64-bit floating point,
20    /// date, or date-time. Valid dates are approximately ±262,000 years
21    /// from the common era. Date-time values are written with microsecond accuracy,
22    /// and times are always in UTC.
23    Number {
24        /// Array with `Number` type storing the attribute values.
25        values: Array<array_type::Number>,
26        /// Optional colormap. If absent then the importing application should invent one.
27        ///
28        /// Make sure the colormap uses the same number type as `values`.
29        #[serde(default, skip_serializing_if = "Option::is_none")]
30        colormap: Option<NumberColormap>,
31    },
32    /// 2D or 3D vector data.
33    Vector {
34        /// Array with `Vector` type storing the attribute values.
35        values: Array<array_type::Vector>,
36    },
37    /// Text data.
38    Text {
39        /// Array with `Text` type storing the attribute values.
40        values: Array<array_type::Text>,
41    },
42    /// Category data.
43    ///
44    /// A name is required for each category, a color is optional, and other values can be attached
45    /// as sub-attributes.
46    Category {
47        /// Array with `Index` type storing the category indices.
48        ///
49        /// Values are indices into the `names` array, `colors` array, and any sub-attributes,
50        /// and must be within range for them.
51        values: Array<array_type::Index>,
52        /// Array with `Name` type storing category names.
53        names: Array<array_type::Name>,
54        /// Optional array with `Gradient` type storing category colors.
55        ///
56        /// If present, must be the same length as `names`. If absent then the importing
57        /// application should invent colors.
58        #[serde(default, skip_serializing_if = "Option::is_none")]
59        gradient: Option<Array<array_type::Gradient>>,
60        /// Additional attributes that use the same indices.
61        ///
62        /// This could be used to store the density of rock types in a lithology attribute for
63        /// example. The location field of these attributes must be
64        /// `Categories`[crate::Location::Categories]. They must have the same length as `names`.
65        #[serde(default, skip_serializing_if = "Vec::is_empty")]
66        attributes: Vec<Attribute>,
67    },
68    /// Boolean or filter data.
69    Boolean {
70        /// Array with `Boolean` type storing the attribute values.
71        ///
72        /// These values may be true, false, or null. Applications that don't support
73        /// [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic) may treat
74        /// null as false.
75        values: Array<array_type::Boolean>,
76    },
77    /// Color data.
78    Color {
79        /// Array with `Color` type storing the attribute values.
80        ///
81        /// Null values may be replaced by the element color, or a default color as the
82        /// application prefers.
83        values: Array<array_type::Color>,
84    },
85    /// A texture applied with [UV mapping](https://en.wikipedia.org/wiki/UV_mapping).
86    ///
87    /// Typically applied to surface vertices. Applications may ignore other locations.
88    MappedTexture {
89        /// Array with `Image` type storing the texture image.
90        image: Array<array_type::Image>,
91        /// Array with `Texcoord` type storing the UV texture coordinates.
92        ///
93        /// Each item is a normalized (U, V) pair. For values outside [0, 1] the texture wraps.
94        texcoords: Array<array_type::Texcoord>,
95    },
96    /// A texture defined as a rectangle in space projected along its normal.
97    ///
98    /// Behavior of the texture outside the projected rectangle is not defined. The texture
99    /// might repeat, clip the element, or itself be clipped to reveal the flat color of the
100    /// element.
101    ///
102    /// The attribute location must be [`Projected`](crate::Location::Projected).
103    ProjectedTexture {
104        /// Array with `Image` type storing the texture image.
105        image: Array<array_type::Image>,
106        /// Orientation of the image.
107        orient: Orient2,
108        /// Width of the image projection in space.
109        width: f64,
110        /// Height of the image projection in space.
111        height: f64,
112    },
113}
114
115impl AttributeData {
116    /// True if the attribute data length is zero.
117    pub fn is_empty(&self) -> bool {
118        self.len() == 0
119    }
120
121    /// Length of the attribute data; zero for projected textures.
122    pub fn len(&self) -> u64 {
123        match self {
124            Self::Number { values, .. } => values.item_count(),
125            Self::Vector { values } => values.item_count(),
126            Self::Text { values } => values.item_count(),
127            Self::Category { values, .. } => values.item_count(),
128            Self::Boolean { values } => values.item_count(),
129            Self::Color { values, .. } => values.item_count(),
130            Self::MappedTexture { texcoords, .. } => texcoords.item_count(),
131            Self::ProjectedTexture { .. } => 0,
132        }
133    }
134
135    pub(crate) fn type_name(&self) -> &'static str {
136        match self {
137            Self::Number { .. } => "Number",
138            Self::Vector { .. } => "Vector",
139            Self::Text { .. } => "String",
140            Self::Category { .. } => "Category",
141            Self::Boolean { .. } => "Boolean",
142            Self::Color { .. } => "Color",
143            Self::MappedTexture { .. } => "MappedTexture",
144            Self::ProjectedTexture { .. } => "ProjectedTexture",
145        }
146    }
147}
148
149/// Describes data attached to an [`Element`](crate::Element).
150///
151/// Each [`Element`](crate::Element) can have zero or more attributes,
152/// each attached to different parts of the element and each containing different types of data.
153/// On a set of points, one attribute might contain gold assay results and another rock-type classifications.
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
155pub struct Attribute {
156    /// Attribute name. Should be unique within the containing element.
157    pub name: String,
158    /// Optional attribute description.
159    #[serde(default, skip_serializing_if = "String::is_empty")]
160    pub description: String,
161    /// Optional unit of the attribute data, default empty.
162    ///
163    /// OMF does not currently attempt to standardize the strings you can use here, but our
164    /// recommendations are:
165    ///
166    /// - Use full names, so "kilometers" rather than "km". The abbreviations for non-metric units
167    ///   aren't consistent and complex units can be confusing.
168    ///
169    /// - Use plurals, so "feet" rather than "foot".
170    ///
171    /// - Avoid ambiguity, so "long tons" rather than just "tons".
172    ///
173    /// - Accept American and British spellings, so "meter" and "metre" are the same.
174    #[serde(default, skip_serializing_if = "String::is_empty")]
175    pub units: String,
176    /// Attribute metadata.
177    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
178    pub metadata: serde_json::Map<String, Value>,
179    /// Selects which part of the element the attribute is attached to.
180    ///
181    /// See the documentation for each [`Geometry`](crate::Geometry) variant for a list of what
182    /// locations are valid.
183    pub location: Location,
184    /// The attribute data.
185    pub data: AttributeData,
186}
187
188impl Attribute {
189    pub(crate) fn new(name: impl Into<String>, location: Location, data: AttributeData) -> Self {
190        Self {
191            name: name.into(),
192            description: Default::default(),
193            units: Default::default(),
194            metadata: Default::default(),
195            location,
196            data,
197        }
198    }
199
200    /// Convenience function to create a number attribute.
201    pub fn from_numbers(
202        name: impl Into<String>,
203        location: Location,
204        values: Array<array_type::Number>,
205    ) -> Self {
206        Self::new(
207            name,
208            location,
209            AttributeData::Number {
210                values,
211                colormap: None,
212            },
213        )
214    }
215
216    /// Convenience function to create a number attribute with a continuous colormap.
217    pub fn from_numbers_continuous_colormap(
218        name: impl Into<String>,
219        location: Location,
220        values: Array<array_type::Number>,
221        range: impl Into<NumberRange>,
222        gradient: Array<array_type::Gradient>,
223    ) -> Self {
224        Self::new(
225            name,
226            location,
227            AttributeData::Number {
228                values,
229                colormap: Some(NumberColormap::Continuous {
230                    range: range.into(),
231                    gradient,
232                }),
233            },
234        )
235    }
236
237    /// Convenience function to create a number attribute with a discrete colormap.
238    pub fn from_numbers_discrete_colormap(
239        name: impl Into<String>,
240        location: Location,
241        values: Array<array_type::Number>,
242        boundaries: Array<array_type::Boundary>,
243        gradient: Array<array_type::Gradient>,
244    ) -> Self {
245        Self::new(
246            name,
247            location,
248            AttributeData::Number {
249                values,
250                colormap: Some(NumberColormap::Discrete {
251                    boundaries,
252                    gradient,
253                }),
254            },
255        )
256    }
257
258    /// Convenience function to create a vector attribute.
259    pub fn from_vectors(
260        name: impl Into<String>,
261        location: Location,
262        values: Array<array_type::Vector>,
263    ) -> Self {
264        Self::new(name, location, AttributeData::Vector { values })
265    }
266
267    /// Convenience function to create a string attribute.
268    pub fn from_strings(
269        name: impl Into<String>,
270        location: Location,
271        values: Array<array_type::Text>,
272    ) -> Self {
273        Self::new(name, location, AttributeData::Text { values })
274    }
275
276    /// Convenience function to create a category attribute.
277    pub fn from_categories(
278        name: impl Into<String>,
279        location: Location,
280        values: Array<array_type::Index>,
281        names: Array<array_type::Name>,
282        gradient: Option<Array<array_type::Gradient>>,
283        attributes: impl IntoIterator<Item = Attribute>,
284    ) -> Self {
285        Self::new(
286            name,
287            location,
288            AttributeData::Category {
289                values,
290                names,
291                gradient,
292                attributes: attributes.into_iter().collect(),
293            },
294        )
295    }
296
297    /// Convenience function to create a boolean attribute.
298    pub fn from_booleans(
299        name: impl Into<String>,
300        location: Location,
301        values: Array<array_type::Boolean>,
302    ) -> Self {
303        Self::new(name, location, AttributeData::Boolean { values })
304    }
305
306    /// Convenience function to create a color attribute.
307    pub fn from_colors(
308        name: impl Into<String>,
309        location: Location,
310        values: Array<array_type::Color>,
311    ) -> Self {
312        Self::new(name, location, AttributeData::Color { values })
313    }
314
315    /// Convenience function to create a mapped texture attribute.
316    pub fn from_texture_map(
317        name: impl Into<String>,
318        image: Array<array_type::Image>,
319        location: Location,
320        texcoords: Array<array_type::Texcoord>,
321    ) -> Self {
322        Self::new(
323            name,
324            location,
325            AttributeData::MappedTexture { image, texcoords },
326        )
327    }
328
329    /// Convenience function to create a projected texture attribute.
330    pub fn from_texture_project(
331        name: impl Into<String>,
332        image: Array<array_type::Image>,
333        orient: Orient2,
334        width: f64,
335        height: f64,
336    ) -> Self {
337        Self::new(
338            name,
339            Location::Projected,
340            AttributeData::ProjectedTexture {
341                image,
342                orient,
343                width,
344                height,
345            },
346        )
347    }
348
349    /// Returns the length of the attribute, or zero for a projected texture.
350    pub fn len(&self) -> u64 {
351        self.data.len()
352    }
353
354    pub fn is_empty(&self) -> bool {
355        self.len() == 0
356    }
357}
358
359/// Describes what part of the geometry an attribute attaches to.
360///
361/// See the documentation for each [`Geometry`](crate::Geometry) variant for a list of what
362/// locations are valid.
363#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
364#[repr(i32)]
365pub enum Location {
366    /// The attribute contains one value for each point, vertex, or block corner.
367    #[default]
368    Vertices,
369    /// The attribute contains one value for each line segment, triangle, or block.
370    /// For sub-blocked block models that means parent blocks.
371    Primitives,
372    /// The attribute contains one value for each sub-block in a block model.
373    Subblocks,
374    /// The attribute contains one value for each sub-element in a
375    /// [`Composite`](crate::Geometry::Composite).
376    Elements,
377    /// Used by [projected texture](crate::AttributeData::ProjectedTexture) attributes.
378    /// The texture is projected onto the element
379    Projected,
380    /// Used for category sub-attributes.
381    /// The attribute contains one value for each category.
382    Categories,
383}
384
385impl Validate for Attribute {
386    fn validate_inner(&mut self, val: &mut Validator) {
387        val.enter("Attribute").name(&self.name).obj(&mut self.data);
388    }
389}
390
391impl Validate for AttributeData {
392    fn validate_inner(&mut self, val: &mut Validator) {
393        match self {
394            Self::Number { values, colormap } => {
395                val.enter("AttributeData::Number")
396                    .array(values, Constraint::Number, "values")
397                    .obj(colormap);
398            }
399            Self::Vector { values } => {
400                val.enter("AttributeData::Vector")
401                    .array(values, Constraint::Vector, "values");
402            }
403            Self::Text { values } => {
404                val.enter("AttributeData::String")
405                    .array(values, Constraint::String, "values");
406            }
407            Self::Category {
408                names,
409                gradient,
410                values,
411                attributes,
412            } => {
413                val.enter("AttributeData::Category")
414                    .array(values, Constraint::Index(names.item_count()), "values")
415                    .array(names, Constraint::Name, "names")
416                    .array_opt(gradient.as_mut(), Constraint::Gradient, "colors")
417                    .array_size_opt(
418                        gradient.as_ref().map(|a| a.item_count()),
419                        names.item_count(),
420                        "colors",
421                    )
422                    .objs(attributes.iter_mut())
423                    .attrs_on_attribute(attributes, names.item_count());
424            }
425            Self::Boolean { values } => {
426                val.enter("AttributeData::Boolean")
427                    .array(values, Constraint::Boolean, "values");
428            }
429            Self::Color { values } => {
430                val.enter("AttributeData::Color")
431                    .array(values, Constraint::Color, "values");
432            }
433            Self::ProjectedTexture {
434                orient,
435                width,
436                height,
437                image,
438            } => {
439                val.enter("AttributeData::ProjectedTexture")
440                    .obj(orient)
441                    .finite(*width, "width")
442                    .finite(*height, "height")
443                    .above_zero(*width, "width")
444                    .above_zero(*height, "height")
445                    .array(image, Constraint::Image, "image");
446            }
447            Self::MappedTexture { texcoords, image } => {
448                val.enter("AttributeData::MappedTexture")
449                    .array(texcoords, Constraint::Texcoord, "texcoords")
450                    .array(image, Constraint::Image, "image");
451            }
452        }
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use crate::{Array, colormap::NumberRange};
459
460    use super::*;
461
462    #[test]
463    fn serialize_attribute() {
464        let mut attributes = vec![
465            Attribute {
466                name: "filter".to_owned(),
467                description: "description of filter".to_owned(),
468                units: Default::default(),
469                metadata: Default::default(),
470                location: Location::Vertices,
471                data: AttributeData::Boolean {
472                    values: Array::new("1.parquet".to_owned(), 100).into(),
473                },
474            },
475            Attribute {
476                name: "assay".to_owned(),
477                description: "description of assay".to_owned(),
478                units: "parts per million".to_owned(),
479                metadata: Default::default(),
480                location: Location::Primitives,
481                data: AttributeData::Number {
482                    values: Array::new("2.parquet".to_owned(), 100).into(),
483                    colormap: Some(NumberColormap::Continuous {
484                        range: NumberRange::Float {
485                            min: 0.0,
486                            max: 100.0,
487                        },
488                        gradient: Array::new("3.parquet".to_owned(), 128),
489                    }),
490                },
491            },
492        ];
493        for a in &mut attributes {
494            a.validate().unwrap();
495            let s = serde_json::to_string(a).unwrap();
496            let b = serde_json::from_str(&s).unwrap();
497            assert_eq!(a, &b);
498        }
499    }
500}