1use cjseq::{CityJSON, Geometry, Ring};
13use std::io::{Result as IoResult, Write};
14
15pub fn to_obj_string(city_json: &CityJSON) -> String {
17 let mut output = Vec::new();
18 to_obj(city_json, &mut output).expect("writing to a Vec cannot fail");
21 String::from_utf8(output).expect("OBJ output is ASCII")
22}
23
24pub fn to_obj<W: Write>(city_json: &CityJSON, writer: &mut W) -> IoResult<()> {
26 writeln!(writer, "# Converted from CityJSON to OBJ")?;
27 writeln!(writer, "# by CJSeq converter")?;
28 writeln!(writer)?;
29
30 let scale = &city_json.transform.scale;
33 let translate = &city_json.transform.translate;
34
35 for vertex in &city_json.vertices {
36 let x = (vertex[0] as f64 * scale[0]) + translate[0];
37 let y = (vertex[1] as f64 * scale[1]) + translate[1];
38 let z = (vertex[2] as f64 * scale[2]) + translate[2];
39 writeln!(writer, "v {x} {y} {z}")?;
40 }
41
42 writeln!(writer)?;
43
44 for city_object in city_json.city_objects.values() {
45 if let Some(geometries) = &city_object.geometry {
46 for geometry in find_highest_lod_geometry(geometries) {
47 for ring in rings(geometry) {
48 write_obj_face(ring, writer)?;
49 }
50 }
51 }
52 }
53
54 Ok(())
55}
56
57fn rings(geometry: &Geometry) -> Box<dyn Iterator<Item = &Ring> + '_> {
60 match geometry {
61 Geometry::MultiPoint { boundaries, .. } | Geometry::GeometryInstance { boundaries, .. } => {
62 Box::new(std::iter::once(boundaries))
63 }
64 Geometry::MultiLineString { boundaries, .. } => Box::new(boundaries.iter()),
65 Geometry::MultiSurface { boundaries, .. }
66 | Geometry::CompositeSurface { boundaries, .. } => Box::new(boundaries.iter().flatten()),
67 Geometry::Solid { boundaries, .. } => Box::new(boundaries.iter().flatten().flatten()),
68 Geometry::MultiSolid { boundaries, .. } | Geometry::CompositeSolid { boundaries, .. } => {
69 Box::new(boundaries.iter().flatten().flatten().flatten())
70 }
71 }
72}
73
74fn find_highest_lod_geometry(geometries: &[Geometry]) -> Vec<&Geometry> {
77 let lod_of = |g: &Geometry| g.lod().and_then(|l| l.parse::<f64>().ok());
78
79 let Some(max_lod) = geometries
80 .iter()
81 .filter_map(lod_of)
82 .fold(None, |max: Option<f64>, lod| {
83 Some(max.map_or(lod, |m| m.max(lod)))
84 })
85 else {
86 return geometries.iter().collect();
87 };
88
89 geometries
90 .iter()
91 .filter(|g| lod_of(g).is_some_and(|lod| (lod - max_lod).abs() < f64::EPSILON))
92 .collect()
93}
94
95fn write_obj_face<W: Write>(indices: &[usize], writer: &mut W) -> IoResult<()> {
97 if indices.is_empty() {
98 return Ok(());
99 }
100
101 write!(writer, "f")?;
102 for idx in indices {
103 write!(writer, " {}", idx + 1)?;
104 }
105 writeln!(writer)?;
106
107 Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use serde_json::json;
114
115 fn city_json(geometry: serde_json::Value) -> CityJSON {
116 let doc = json!({
117 "type": "CityJSON",
118 "version": "2.0",
119 "transform": {"scale": [0.001, 0.001, 0.001], "translate": [1.0, 2.0, 3.0]},
120 "CityObjects": {"co-1": {"type": "Building", "geometry": [geometry]}},
121 "vertices": [[0, 0, 0], [1000, 0, 0], [1000, 1000, 0]]
122 });
123 serde_json::from_value(doc).expect("test document must parse")
124 }
125
126 #[test]
127 fn vertices_are_written_in_real_world_coordinates() {
128 let obj = to_obj_string(&city_json(
129 json!({"type": "MultiSurface", "lod": "2", "boundaries": [[[0, 1, 2]]]}),
130 ));
131 assert!(obj.contains("v 1 2 3"), "{obj}");
132 assert!(obj.contains("v 2 2 3"), "{obj}");
133 assert!(obj.contains("v 2 3 3"), "{obj}");
134 }
135
136 #[test]
139 fn every_geometry_type_yields_its_rings_as_faces() {
140 for (geometry, expected) in [
141 (
142 json!({"type": "MultiLineString", "lod": "1", "boundaries": [[0, 1, 2]]}),
143 "f 1 2 3",
144 ),
145 (
146 json!({"type": "MultiSurface", "lod": "1", "boundaries": [[[0, 1, 2]]]}),
147 "f 1 2 3",
148 ),
149 (
150 json!({"type": "Solid", "lod": "1", "boundaries": [[[[0, 1, 2]]]]}),
151 "f 1 2 3",
152 ),
153 (
154 json!({"type": "CompositeSolid", "lod": "1", "boundaries": [[[[[0, 1, 2]]]]]}),
155 "f 1 2 3",
156 ),
157 ] {
158 let obj = to_obj_string(&city_json(geometry));
159 assert!(obj.contains(expected), "{obj}");
160 }
161 }
162
163 #[test]
164 fn only_the_highest_lod_is_written() {
165 let doc = json!({
166 "type": "CityJSON",
167 "version": "2.0",
168 "transform": {"scale": [1.0, 1.0, 1.0], "translate": [0.0, 0.0, 0.0]},
169 "CityObjects": {"co-1": {"type": "Building", "geometry": [
170 {"type": "MultiSurface", "lod": "1", "boundaries": [[[0, 1, 2]]]},
171 {"type": "MultiSurface", "lod": "2", "boundaries": [[[2, 1, 0]]]}
172 ]}},
173 "vertices": [[0, 0, 0], [1, 0, 0], [1, 1, 0]]
174 });
175 let cj: CityJSON = serde_json::from_value(doc).expect("document must parse");
176 let obj = to_obj_string(&cj);
177 assert!(obj.contains("f 3 2 1"), "{obj}");
178 assert!(!obj.contains("f 1 2 3"), "{obj}");
179 }
180}