use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub enum KnownSemanticSurfaceType {
RoofSurface,
GroundSurface,
WallSurface,
ClosureSurface,
OuterCeilingSurface,
OuterFloorSurface,
Window,
Door,
InteriorWallSurface,
CeilingSurface,
FloorSurface,
WaterSurface,
WaterGroundSurface,
WaterClosureSurface,
TrafficArea,
AuxiliaryTrafficArea,
TransportationMarking,
TransportationHole,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(untagged)]
pub enum SemanticSurfaceType {
Known(KnownSemanticSurfaceType),
Extension(String),
}
#[allow(non_upper_case_globals)]
impl SemanticSurfaceType {
pub const RoofSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::RoofSurface);
pub const GroundSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::GroundSurface);
pub const WallSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::WallSurface);
pub const ClosureSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::ClosureSurface);
pub const OuterCeilingSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::OuterCeilingSurface);
pub const OuterFloorSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::OuterFloorSurface);
pub const Window: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::Window);
pub const Door: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::Door);
pub const InteriorWallSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::InteriorWallSurface);
pub const CeilingSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::CeilingSurface);
pub const FloorSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::FloorSurface);
pub const WaterSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::WaterSurface);
pub const WaterGroundSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::WaterGroundSurface);
pub const WaterClosureSurface: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::WaterClosureSurface);
pub const TrafficArea: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::TrafficArea);
pub const AuxiliaryTrafficArea: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::AuxiliaryTrafficArea);
pub const TransportationMarking: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::TransportationMarking);
pub const TransportationHole: SemanticSurfaceType =
SemanticSurfaceType::Known(KnownSemanticSurfaceType::TransportationHole);
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct SemanticsSurface {
#[serde(rename = "type")]
pub thetype: SemanticSurfaceType,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<usize>>,
#[serde(flatten)]
pub other: HashMap<String, Value>,
}
pub type SemanticsShell = Vec<Option<usize>>;
pub type SemanticsSolid = Vec<Option<SemanticsShell>>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum SemanticsValues {
Surfaces(Vec<Option<usize>>),
Shells(Vec<Option<SemanticsShell>>),
Solids(Vec<Option<SemanticsSolid>>),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Semantics {
pub surfaces: Vec<SemanticsSurface>,
pub values: Option<SemanticsValues>,
#[serde(flatten)]
pub other: HashMap<String, Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semantics_keeps_members_the_schema_does_not_name() {
let input = serde_json::json!({
"surfaces": [{"type": "RoofSurface"}],
"values": [0],
"vendorData": {"x": 1}
});
let s: Semantics = serde_json::from_value(input.clone()).unwrap();
assert_eq!(s.other["vendorData"], serde_json::json!({"x": 1}));
assert_eq!(serde_json::to_value(&s).unwrap(), input);
}
#[test]
fn semantics_without_extras_serializes_unchanged() {
let input = serde_json::json!({
"surfaces": [{"type": "RoofSurface"}],
"values": null
});
let s: Semantics = serde_json::from_value(input.clone()).unwrap();
assert!(s.other.is_empty());
assert_eq!(serde_json::to_value(&s).unwrap(), input);
}
#[test]
fn solid_semantics_values_are_one_per_surface_per_shell() {
let s: Semantics = serde_json::from_value(serde_json::json!({
"surfaces": [{"type": "RoofSurface"}, {"type": "WallSurface"}],
"values": [[0, 1, null]]
}))
.unwrap();
assert_eq!(
serde_json::to_value(&s).unwrap()["values"],
serde_json::json!([[0, 1, null]])
);
}
#[test]
fn null_semantics_index_stays_null() {
let s: Semantics = serde_json::from_value(serde_json::json!({
"surfaces": [{"type": "RoofSurface"}], "values": [null, 0]
}))
.unwrap();
assert_eq!(
serde_json::to_value(&s).unwrap()["values"],
serde_json::json!([null, 0])
);
}
#[test]
fn semantics_surface_and_semantics_implement_eq() {
fn assert_impls_eq<T: Eq>() {}
assert_impls_eq::<SemanticsSurface>();
assert_impls_eq::<SemanticsValues>();
assert_impls_eq::<Semantics>();
}
#[test]
fn surface_extras_and_hierarchy_round_trip() {
let input = serde_json::json!({
"surfaces": [
{"type": "RoofSurface", "slope": 33.4, "children": [1]},
{"type": "Window", "parent": 0, "direction": "north"}
],
"values": [0, 1]
});
let s: Semantics = serde_json::from_value(input.clone()).unwrap();
assert_eq!(s.surfaces[0].children, Some(vec![1]));
assert_eq!(s.surfaces[1].parent, Some(0));
assert_eq!(
s.surfaces[1].other["direction"],
serde_json::json!("north"),
"extra members must be kept, not dropped"
);
assert_eq!(serde_json::to_value(&s).unwrap(), input);
}
#[test]
fn surface_with_no_extras_round_trips_without_spurious_members() {
let input = serde_json::json!({
"surfaces": [{"type": "RoofSurface"}],
"values": [0]
});
let s: Semantics = serde_json::from_value(input.clone()).unwrap();
assert!(s.surfaces[0].other.is_empty());
assert_eq!(serde_json::to_value(&s).unwrap(), input);
}
#[test]
fn each_shape_lands_in_the_variant_its_depth_implies() {
let parse =
|v: serde_json::Value| -> SemanticsValues { serde_json::from_value(v).unwrap() };
assert!(matches!(
parse(serde_json::json!([0, 1, null])),
SemanticsValues::Surfaces(_)
));
assert!(matches!(
parse(serde_json::json!([[0, 1, null]])),
SemanticsValues::Shells(_)
));
assert!(matches!(
parse(serde_json::json!([[[0, 1, null]]])),
SemanticsValues::Solids(_)
));
let empty = parse(serde_json::json!([]));
assert!(matches!(empty, SemanticsValues::Surfaces(_)));
assert_eq!(serde_json::to_value(&empty).unwrap(), serde_json::json!([]));
assert!(serde_json::from_value::<SemanticsValues>(serde_json::json!([[[[0]]]])).is_err());
}
#[test]
fn a_null_semantics_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: SemanticsValues = 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 a_null_semantics_values_member_round_trips() {
let input = serde_json::json!({
"surfaces": [{"type": "RoofSurface"}], "values": null
});
let s: Semantics =
serde_json::from_value(input.clone()).expect("`values: null` is schema-valid CityJSON");
assert_eq!(s.values, None);
assert_eq!(serde_json::to_value(&s).unwrap(), input);
}
#[test]
fn null_sub_arrays_do_not_disturb_the_semantics_ladder() {
let parse =
|v: serde_json::Value| -> SemanticsValues { serde_json::from_value(v).unwrap() };
assert!(matches!(
parse(serde_json::json!([null, 1])),
SemanticsValues::Surfaces(_)
));
assert!(matches!(
parse(serde_json::json!([[0, 1], null])),
SemanticsValues::Shells(_)
));
assert!(matches!(
parse(serde_json::json!([[[0, 1]], null])),
SemanticsValues::Solids(_)
));
for input in [
serde_json::json!([]),
serde_json::json!([[]]),
serde_json::json!([[[]]]),
] {
let v = parse(input.clone());
assert_eq!(serde_json::to_value(&v).unwrap(), input, "{input}");
}
assert!(matches!(
parse(serde_json::json!([])),
SemanticsValues::Surfaces(_)
));
assert!(matches!(
parse(serde_json::json!([[]])),
SemanticsValues::Shells(_)
));
assert!(matches!(
parse(serde_json::json!([[[]]])),
SemanticsValues::Solids(_)
));
}
#[test]
fn extension_semantic_surface_type_roundtrips_with_its_plus() {
let t: SemanticSurfaceType =
serde_json::from_value(serde_json::json!("+ThermalSurface")).unwrap();
assert_eq!(t, SemanticSurfaceType::Extension("+ThermalSurface".into()));
assert_eq!(
serde_json::to_value(&t).unwrap(),
serde_json::json!("+ThermalSurface")
);
}
#[test]
fn plus_prefixed_known_surface_name_lands_in_extension() {
let t: SemanticSurfaceType =
serde_json::from_value(serde_json::json!("+RoofSurface")).unwrap();
assert_eq!(t, SemanticSurfaceType::Extension("+RoofSurface".into()));
assert_ne!(t, SemanticSurfaceType::RoofSurface);
}
#[test]
fn extension_typed_semantics_surface_roundtrips_byte_for_byte_inside_a_geometry() {
let input = concat!(
r#"{"type":"MultiSurface","boundaries":[[[0,1,2]]],"#,
r#""semantics":{"surfaces":[{"type":"+ThermalSurface","azimuth":30.5}],"values":[0]}}"#
);
let parsed: crate::geometry::Geometry = serde_json::from_str(input).unwrap();
let reserialized = serde_json::to_string(&parsed).unwrap();
assert_eq!(
reserialized, input,
"an Extension-typed SemanticsSurface with an extra flattened member inside a \
Geometry must round-trip byte-for-byte"
);
}
#[test]
fn every_known_semantic_surface_type_round_trips_as_its_unit_variant() {
let known: &[(&str, SemanticSurfaceType)] = &[
("RoofSurface", SemanticSurfaceType::RoofSurface),
("GroundSurface", SemanticSurfaceType::GroundSurface),
("WallSurface", SemanticSurfaceType::WallSurface),
("ClosureSurface", SemanticSurfaceType::ClosureSurface),
(
"OuterCeilingSurface",
SemanticSurfaceType::OuterCeilingSurface,
),
("OuterFloorSurface", SemanticSurfaceType::OuterFloorSurface),
("Window", SemanticSurfaceType::Window),
("Door", SemanticSurfaceType::Door),
(
"InteriorWallSurface",
SemanticSurfaceType::InteriorWallSurface,
),
("CeilingSurface", SemanticSurfaceType::CeilingSurface),
("FloorSurface", SemanticSurfaceType::FloorSurface),
("WaterSurface", SemanticSurfaceType::WaterSurface),
(
"WaterGroundSurface",
SemanticSurfaceType::WaterGroundSurface,
),
(
"WaterClosureSurface",
SemanticSurfaceType::WaterClosureSurface,
),
("TrafficArea", SemanticSurfaceType::TrafficArea),
(
"AuxiliaryTrafficArea",
SemanticSurfaceType::AuxiliaryTrafficArea,
),
(
"TransportationMarking",
SemanticSurfaceType::TransportationMarking,
),
(
"TransportationHole",
SemanticSurfaceType::TransportationHole,
),
];
for (name, expected) in known {
let parsed: SemanticSurfaceType =
serde_json::from_value(serde_json::json!(name)).unwrap();
assert_eq!(&parsed, expected, "{name} did not parse to its own variant");
assert_eq!(
serde_json::to_value(&parsed).unwrap(),
serde_json::json!(name),
"{name} did not round-trip back to its own string"
);
}
}
}