use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
pub type MaterialShell = Vec<Option<usize>>;
pub type MaterialSolid = Vec<Option<MaterialShell>>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum MaterialValues {
Surfaces(Vec<Option<usize>>),
Shells(Vec<Option<MaterialShell>>),
Solids(Vec<Option<MaterialSolid>>),
}
impl MaterialValues {
pub(crate) fn indices_mut(&mut self) -> Box<dyn Iterator<Item = &mut usize> + '_> {
match self {
MaterialValues::Surfaces(v) => Box::new(v.iter_mut().flatten()),
MaterialValues::Shells(v) => Box::new(v.iter_mut().flatten().flatten().flatten()),
MaterialValues::Solids(v) => Box::new(
v.iter_mut()
.flatten()
.flatten()
.flatten()
.flatten()
.flatten(),
),
}
}
}
pub type TexturedRing = Vec<Option<usize>>;
pub type TexturedSurface = Vec<TexturedRing>;
pub type TexturedShell = Vec<TexturedSurface>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum TextureValues {
Surface(Vec<TexturedSurface>),
Shell(Vec<TexturedShell>),
Solid(Vec<Vec<TexturedShell>>),
}
impl TextureValues {
pub(crate) fn rings_mut(&mut self) -> Box<dyn Iterator<Item = &mut TexturedRing> + '_> {
match self {
TextureValues::Surface(v) => Box::new(v.iter_mut().flatten()),
TextureValues::Shell(v) => Box::new(v.iter_mut().flatten().flatten()),
TextureValues::Solid(v) => Box::new(v.iter_mut().flatten().flatten().flatten()),
}
}
}
fn present_even_if_null<'de, T, D>(de: D) -> std::result::Result<Option<T>, D::Error>
where
T: Deserialize<'de>,
D: serde::Deserializer<'de>,
{
T::deserialize(de).map(Some)
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct MaterialReference {
#[serde(
default,
deserialize_with = "present_even_if_null",
skip_serializing_if = "Option::is_none"
)]
pub values: Option<Option<MaterialValues>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<usize>,
#[serde(flatten)]
pub other: HashMap<String, Value>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct TextureReference {
#[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<TextureValues>,
#[serde(flatten)]
pub other: HashMap<String, Value>,
}
pub type Color = [f64; 3];
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum BorderColor {
Rgb(Color),
Rgba([f64; 4]),
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TextureFormat {
PNG,
JPG,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum WrapMode {
None,
Wrap,
Mirror,
Clamp,
Border,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum TextureType {
Unknown,
Specific,
Typical,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct MaterialObject {
pub name: String,
#[serde(rename = "ambientIntensity", skip_serializing_if = "Option::is_none")]
pub ambient_intensity: Option<f64>,
#[serde(rename = "diffuseColor", skip_serializing_if = "Option::is_none")]
pub diffuse_color: Option<Color>,
#[serde(rename = "emissiveColor", skip_serializing_if = "Option::is_none")]
pub emissive_color: Option<Color>,
#[serde(rename = "specularColor", skip_serializing_if = "Option::is_none")]
pub specular_color: Option<Color>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shininess: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency: Option<f64>,
#[serde(rename = "isSmooth", skip_serializing_if = "Option::is_none")]
pub is_smooth: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct TextureObject {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub thetype: Option<TextureFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(rename = "wrapMode", skip_serializing_if = "Option::is_none")]
pub wrap_mode: Option<WrapMode>,
#[serde(rename = "textureType", skip_serializing_if = "Option::is_none")]
pub texture_type: Option<TextureType>,
#[serde(rename = "borderColor", skip_serializing_if = "Option::is_none")]
pub border_color: Option<BorderColor>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Appearance {
#[serde(skip_serializing_if = "Option::is_none")]
pub materials: Option<Vec<MaterialObject>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub textures: Option<Vec<TextureObject>>,
#[serde(rename = "vertices-texture")]
#[serde(skip_serializing_if = "Option::is_none")]
pub vertices_texture: Option<Vec<[f64; 2]>>,
#[serde(rename = "default-theme-texture")]
#[serde(skip_serializing_if = "Option::is_none")]
pub default_theme_texture: Option<String>,
#[serde(rename = "default-theme-material")]
#[serde(skip_serializing_if = "Option::is_none")]
pub default_theme_material: Option<String>,
}
impl Appearance {
pub(crate) fn new() -> Self {
Appearance {
materials: None,
textures: None,
vertices_texture: None,
default_theme_texture: None,
default_theme_material: None,
}
}
pub(crate) fn add_material(&mut self, jm: MaterialObject) -> usize {
let ls = self.materials.get_or_insert_with(Vec::new);
match ls.iter().position(|e| *e == jm) {
Some(y) => y,
None => {
ls.push(jm);
ls.len() - 1
}
}
}
pub(crate) fn add_texture(&mut self, jm: TextureObject) -> usize {
let ls = self.textures.get_or_insert_with(Vec::new);
match ls.iter().position(|e| *e == jm) {
Some(y) => y,
None => {
ls.push(jm);
ls.len() - 1
}
}
}
pub(crate) fn add_vertices_texture(&mut self, mut vs: Vec<[f64; 2]>) {
self.vertices_texture
.get_or_insert_with(Vec::new)
.append(&mut vs);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_material_or_texture_theme_keeps_members_the_schema_does_not_name() {
let m: MaterialReference =
serde_json::from_value(serde_json::json!({"value": 0, "vendorData": true})).unwrap();
assert_eq!(m.value, Some(0));
assert_eq!(
serde_json::to_value(&m).unwrap(),
serde_json::json!({"value": 0, "vendorData": true})
);
let t: TextureReference =
serde_json::from_value(serde_json::json!({"values": [[[0, 1, 2]]], "note": "x"}))
.unwrap();
assert_eq!(
serde_json::to_value(&t).unwrap(),
serde_json::json!({"values": [[[0, 1, 2]]], "note": "x"})
);
}
#[test]
fn the_catch_all_does_not_disturb_absent_versus_null() {
let empty: MaterialReference = serde_json::from_value(serde_json::json!({})).unwrap();
assert!(empty.other.is_empty());
assert_eq!(serde_json::to_value(&empty).unwrap(), serde_json::json!({}));
let null: MaterialReference =
serde_json::from_value(serde_json::json!({"values": null})).unwrap();
assert_eq!(null.values, Some(None));
assert_eq!(
serde_json::to_value(&null).unwrap(),
serde_json::json!({"values": null})
);
}
#[test]
fn material_indices_serialize_as_numbers_and_null() {
let m: MaterialReference = serde_json::from_value(serde_json::json!({
"values": [null, 1]
}))
.unwrap();
assert_eq!(
serde_json::to_value(&m).unwrap(),
serde_json::json!({"values": [null, 1]})
);
}
#[test]
fn solid_material_values_keep_their_shell_level() {
let m: MaterialReference = serde_json::from_value(serde_json::json!({
"values": [[0, 1]]
}))
.unwrap();
assert_eq!(
serde_json::to_value(&m).unwrap(),
serde_json::json!({"values": [[0, 1]]})
);
}
#[test]
fn texture_ring_is_index_then_uvs() {
let t: TextureReference = serde_json::from_value(serde_json::json!({
"values": [[[0, 10, 11, 12]]]
}))
.unwrap();
assert_eq!(
serde_json::to_value(&t).unwrap(),
serde_json::json!({"values": [[[0, 10, 11, 12]]]})
);
}
#[test]
fn material_value_is_a_bare_integer() {
let m: MaterialReference = serde_json::from_value(serde_json::json!({"value": 3})).unwrap();
assert_eq!(m.value, Some(3));
assert_eq!(
serde_json::to_value(&m).unwrap(),
serde_json::json!({"value": 3})
);
}
#[test]
fn material_values_land_in_the_variant_their_depth_implies() {
let parse = |v: serde_json::Value| -> MaterialValues { serde_json::from_value(v).unwrap() };
assert!(matches!(
parse(serde_json::json!([0, null, 2])),
MaterialValues::Surfaces(_)
));
assert!(matches!(
parse(serde_json::json!([[0, null, 2]])),
MaterialValues::Shells(_)
));
assert!(matches!(
parse(serde_json::json!([[[0, null, 2]]])),
MaterialValues::Solids(_)
));
let empty = parse(serde_json::json!([]));
assert!(matches!(empty, MaterialValues::Surfaces(_)));
assert_eq!(serde_json::to_value(&empty).unwrap(), serde_json::json!([]));
assert!(serde_json::from_value::<MaterialValues>(serde_json::json!([[[[0]]]])).is_err());
assert!(serde_json::from_value::<MaterialValues>(serde_json::json!(0)).is_err());
assert!(
serde_json::from_value::<MaterialValues>(serde_json::json!([[[[[[0]]]]]])).is_err()
);
}
#[test]
fn a_null_material_sub_array_round_trips_as_null_at_every_level() {
for input in [
serde_json::json!([0, null, 2]),
serde_json::json!([[0, 1], null]),
serde_json::json!([null, [0, 1]]),
serde_json::json!([[[0, 1]], null]),
serde_json::json!([[[0, 1], null], null]),
serde_json::json!([null, null]),
serde_json::json!([[null], null]),
] {
let v: MaterialValues = serde_json::from_value(input.clone())
.unwrap_or_else(|e| panic!("{input} is schema-valid CityJSON: {e}"));
assert_eq!(
serde_json::to_value(&v).unwrap(),
input,
"{input} must round-trip with its nulls intact, never as []"
);
}
}
#[test]
fn null_sub_arrays_do_not_disturb_the_material_ladder() {
let parse = |v: serde_json::Value| -> MaterialValues { serde_json::from_value(v).unwrap() };
assert!(matches!(
parse(serde_json::json!([null, 1])),
MaterialValues::Surfaces(_)
));
assert!(matches!(
parse(serde_json::json!([[0, 1], null])),
MaterialValues::Shells(_)
));
assert!(matches!(
parse(serde_json::json!([[[0, 1]], null])),
MaterialValues::Solids(_)
));
for (input, is_surfaces) in [
(serde_json::json!([]), true),
(serde_json::json!([[]]), false),
(serde_json::json!([[[]]]), false),
] {
let v = parse(input.clone());
assert_eq!(serde_json::to_value(&v).unwrap(), input, "{input}");
assert_eq!(
matches!(v, MaterialValues::Surfaces(_)),
is_surfaces,
"{input} landed in {v:?}"
);
}
assert!(matches!(
parse(serde_json::json!([[]])),
MaterialValues::Shells(_)
));
assert!(matches!(
parse(serde_json::json!([[[]]])),
MaterialValues::Solids(_)
));
}
#[test]
fn texture_nulls_are_leaf_only() {
let t: TextureValues = serde_json::from_value(serde_json::json!([[[null]]])).unwrap();
assert_eq!(
serde_json::to_value(&t).unwrap(),
serde_json::json!([[[null]]])
);
for bad in [
serde_json::json!([null, [[0, 10, 11]]]),
serde_json::json!([[null]]),
serde_json::json!([[[[0, 1]]], null]),
] {
assert!(
serde_json::from_value::<TextureValues>(bad.clone()).is_err(),
"{bad} is not valid CityJSON and must be rejected"
);
}
}
#[test]
fn a_texture_reference_may_carry_no_values() {
let absent: TextureReference = serde_json::from_value(serde_json::json!({})).unwrap();
assert_eq!(absent.values, None);
assert_eq!(
serde_json::to_value(&absent).unwrap(),
serde_json::json!({})
);
let null: TextureReference =
serde_json::from_value(serde_json::json!({"values": null})).unwrap();
assert_eq!(null.values, None);
assert_eq!(serde_json::to_value(&null).unwrap(), serde_json::json!({}));
}
#[test]
fn material_values_distinguishes_absent_from_null_from_array() {
let absent: MaterialReference =
serde_json::from_value(serde_json::json!({"value": 3})).unwrap();
assert_eq!(absent.values, None);
assert_eq!(
serde_json::to_value(&absent).unwrap(),
serde_json::json!({"value": 3}),
"an absent `values` must stay absent"
);
let null: MaterialReference =
serde_json::from_value(serde_json::json!({"values": null})).unwrap();
assert_eq!(null.values, Some(None));
assert_eq!(
serde_json::to_value(&null).unwrap(),
serde_json::json!({"values": null}),
"an explicit `null` must be written back as `null`, not dropped"
);
let array: MaterialReference =
serde_json::from_value(serde_json::json!({"values": [0, 1]})).unwrap();
assert_eq!(
array.values,
Some(Some(MaterialValues::Surfaces(vec![Some(0), Some(1)])))
);
assert_eq!(
serde_json::to_value(&array).unwrap(),
serde_json::json!({"values": [0, 1]})
);
let empty: MaterialReference = serde_json::from_value(serde_json::json!({})).unwrap();
assert_eq!(serde_json::to_value(&empty).unwrap(), serde_json::json!({}));
}
#[test]
fn every_wrap_mode_round_trips_and_nothing_else_is_accepted() {
for mode in ["none", "wrap", "mirror", "clamp", "border"] {
let input = serde_json::json!({"textures": [{"wrapMode": mode}]});
let a: Appearance = serde_json::from_value(input.clone())
.unwrap_or_else(|e| panic!("{mode} is a schema-valid wrapMode: {e}"));
assert_eq!(
serde_json::to_value(&a).unwrap(),
input,
"wrapMode {mode:?} must come back exactly as it went in"
);
}
for bad in ["Wrap", "WRAP", "None", "repeat", ""] {
assert!(
serde_json::from_value::<Appearance>(
serde_json::json!({"textures": [{"wrapMode": bad}]})
)
.is_err(),
"{bad:?} is not one of the schema's five wrapMode spellings"
);
}
}
#[test]
fn every_texture_type_and_format_round_trips_and_nothing_else_is_accepted() {
for t in ["unknown", "specific", "typical"] {
let input = serde_json::json!({"textures": [{"textureType": t}]});
let a: Appearance = serde_json::from_value(input.clone())
.unwrap_or_else(|e| panic!("{t} is a schema-valid textureType: {e}"));
assert_eq!(serde_json::to_value(&a).unwrap(), input);
}
for bad in ["Unknown", "SPECIFIC", "other"] {
assert!(
serde_json::from_value::<Appearance>(
serde_json::json!({"textures": [{"textureType": bad}]})
)
.is_err(),
"{bad:?} is not one of the schema's three textureType spellings"
);
}
for f in ["PNG", "JPG"] {
let input = serde_json::json!({"textures": [{"type": f}]});
let a: Appearance = serde_json::from_value(input.clone())
.unwrap_or_else(|e| panic!("{f} is a schema-valid texture type: {e}"));
assert_eq!(serde_json::to_value(&a).unwrap(), input);
}
for bad in ["png", "jpg", "JPEG", "TIFF"] {
assert!(
serde_json::from_value::<Appearance>(
serde_json::json!({"textures": [{"type": bad}]})
)
.is_err(),
"{bad:?} is not one of the schema's two texture formats"
);
}
}
#[test]
fn a_texture_object_keeps_every_member_it_was_given_and_invents_none() {
let full = serde_json::json!({"textures": [{
"type": "JPG",
"image": "appearances/wood.jpg",
"wrapMode": "wrap",
"textureType": "unknown",
"borderColor": [0.0, 0.0, 0.0, 1.0]
}]});
let a: Appearance = serde_json::from_value(full.clone()).unwrap();
assert_eq!(serde_json::to_value(&a).unwrap(), full);
let bare = serde_json::json!({"textures": [{}]});
let a: Appearance = serde_json::from_value(bare.clone()).unwrap();
assert_eq!(
serde_json::to_value(&a).unwrap(),
bare,
"the Texture schema has no `required`, so an empty texture object \
is valid and must not gain members"
);
}
#[test]
fn border_color_is_three_or_four_numbers() {
for ok in [
serde_json::json!([0.0, 0.0, 0.0]),
serde_json::json!([0.0, 0.0, 0.0, 1.0]),
] {
let input = serde_json::json!({"textures": [{"borderColor": ok}]});
let a: Appearance = serde_json::from_value(input.clone())
.unwrap_or_else(|e| panic!("{ok} is a schema-valid borderColor: {e}"));
assert_eq!(serde_json::to_value(&a).unwrap(), input);
}
for bad in [
serde_json::json!([]),
serde_json::json!([0.0]),
serde_json::json!([0.0, 0.0]),
serde_json::json!([0.0, 0.0, 0.0, 1.0, 1.0]),
] {
assert!(
serde_json::from_value::<Appearance>(
serde_json::json!({"textures": [{"borderColor": bad}]})
)
.is_err(),
"{bad} violates borderColor's minItems 3 / maxItems 4"
);
}
}
#[test]
fn a_material_object_requires_a_name_and_three_number_colours() {
let full = serde_json::json!({"materials": [{
"name": "roof",
"ambientIntensity": 0.4,
"diffuseColor": [0.5, 0.4, 0.3],
"emissiveColor": [0.0, 0.0, 0.0],
"specularColor": [1.0, 1.0, 1.0],
"shininess": 0.2,
"transparency": 0.0,
"isSmooth": false
}]});
let a: Appearance = serde_json::from_value(full.clone()).unwrap();
assert_eq!(serde_json::to_value(&a).unwrap(), full);
assert!(
serde_json::from_value::<Appearance>(
serde_json::json!({"materials": [{"diffuseColor": [0.5, 0.4, 0.3]}]})
)
.is_err(),
"`name` is required by the Material schema"
);
for bad in [
serde_json::json!([0.5, 0.4]),
serde_json::json!([0.5, 0.4, 0.3, 0.2]),
] {
assert!(
serde_json::from_value::<Appearance>(
serde_json::json!({"materials": [{"name": "m", "diffuseColor": bad}]})
)
.is_err(),
"{bad} violates diffuseColor's minItems 3 / maxItems 3"
);
}
let bare = serde_json::json!({"materials": [{"name": "m"}]});
let a: Appearance = serde_json::from_value(bare.clone()).unwrap();
assert_eq!(serde_json::to_value(&a).unwrap(), bare);
}
#[test]
fn material_and_texture_objects_admit_no_members_the_schema_does_not_name() {
assert!(
serde_json::from_value::<Appearance>(
serde_json::json!({"materials": [{"name": "m", "vendorData": true}]})
)
.is_err(),
"the Material schema declares additionalProperties: false"
);
assert!(
serde_json::from_value::<Appearance>(
serde_json::json!({"textures": [{"image": "a.jpg", "vendorData": true}]})
)
.is_err(),
"the Texture schema declares additionalProperties: false"
);
assert!(
serde_json::from_value::<Appearance>(
serde_json::json!({"textures": [{"wrapmode": "wrap"}]})
)
.is_err(),
"`wrapmode` is not `wrapMode`"
);
}
#[test]
fn the_appearance_object_admits_no_members_the_schema_does_not_name() {
for bad in [
serde_json::json!({"vendorData": true}),
serde_json::json!({"default_theme_texture": "winter"}),
serde_json::json!({"vertices_texture": [[0.0, 0.0]]}),
serde_json::json!({"materials": [], "texture": []}),
] {
assert!(
serde_json::from_value::<Appearance>(bad.clone()).is_err(),
"{bad} carries a member `appearance` does not name, and the \
schema declares additionalProperties: false"
);
}
let all_five = serde_json::json!({
"materials": [{"name": "m"}],
"textures": [{"image": "a.jpg"}],
"vertices-texture": [[0.0, 0.0]],
"default-theme-texture": "winter",
"default-theme-material": "irradiation"
});
let a: Appearance = serde_json::from_value(all_five.clone()).unwrap();
assert_eq!(serde_json::to_value(&a).unwrap(), all_five);
}
#[test]
fn a_texture_vertex_is_exactly_two_numbers() {
for bad in [
serde_json::json!({"vertices-texture": [[0.5]]}),
serde_json::json!({"vertices-texture": [[0.5, 0.5, 0.5]]}),
serde_json::json!({"vertices-texture": [[]]}),
serde_json::json!({"vertices-texture": [[0.0, 0.0], [1.0, 1.0, 1.0]]}),
] {
assert!(
serde_json::from_value::<Appearance>(bad.clone()).is_err(),
"{bad} has a texture vertex that is not exactly two numbers"
);
}
let good = serde_json::json!({"vertices-texture": [[0.0, 0.5], [1.0, 1.0]]});
let a: Appearance = serde_json::from_value(good.clone()).unwrap();
assert_eq!(a.vertices_texture, Some(vec![[0.0, 0.5], [1.0, 1.0]]));
assert_eq!(serde_json::to_value(&a).unwrap(), good);
}
#[test]
fn an_empty_appearance_library_stays_empty() {
let input = serde_json::json!({"materials": [], "textures": []});
let a: Appearance = serde_json::from_value(input.clone()).unwrap();
assert_eq!(serde_json::to_value(&a).unwrap(), input);
}
#[test]
fn texture_values_land_in_the_variant_their_depth_implies() {
let parse = |v: serde_json::Value| -> TextureValues { serde_json::from_value(v).unwrap() };
assert!(matches!(
parse(serde_json::json!([[[0, 10, 11, 12]]])),
TextureValues::Surface(_)
));
assert!(matches!(
parse(serde_json::json!([[[[0, 10, 11, 12]]]])),
TextureValues::Shell(_)
));
assert!(matches!(
parse(serde_json::json!([[[[[0, 10, 11, 12]]]]])),
TextureValues::Solid(_)
));
assert!(matches!(
parse(serde_json::json!([[[null]]])),
TextureValues::Surface(_)
));
let empty = parse(serde_json::json!([]));
assert!(matches!(empty, TextureValues::Surface(_)));
assert_eq!(serde_json::to_value(&empty).unwrap(), serde_json::json!([]));
assert!(serde_json::from_value::<TextureValues>(serde_json::json!([[[[[[0]]]]]])).is_err());
assert!(serde_json::from_value::<TextureValues>(serde_json::json!([0, 1])).is_err());
}
}