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}