spatialrust_interchange/tiles3d/
tileset.rs1use crate::json::{parse_json, serialize_json, Json};
4use crate::{InterchangeError, InterchangeResult};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum Refinement {
9 Add,
11 Replace,
13}
14
15impl Refinement {
16 fn as_str(self) -> &'static str {
17 match self {
18 Refinement::Add => "ADD",
19 Refinement::Replace => "REPLACE",
20 }
21 }
22
23 fn from_str(value: &str) -> Option<Self> {
24 match value {
25 "ADD" => Some(Refinement::Add),
26 "REPLACE" => Some(Refinement::Replace),
27 _ => None,
28 }
29 }
30}
31
32#[derive(Clone, Debug, PartialEq)]
34pub enum BoundingVolume {
35 Box([f64; 12]),
37}
38
39impl BoundingVolume {
40 pub fn box_from_bounds(min: [f64; 3], max: [f64; 3]) -> InterchangeResult<Self> {
42 for axis in 0..3 {
43 if !min[axis].is_finite() || !max[axis].is_finite() || min[axis] > max[axis] {
44 return Err(InterchangeError::InvalidConfiguration(
45 "tile box bounds must be finite and ordered".into(),
46 ));
47 }
48 }
49 let center = [(min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5, (min[2] + max[2]) * 0.5];
50 let half = [(max[0] - min[0]) * 0.5, (max[1] - min[1]) * 0.5, (max[2] - min[2]) * 0.5];
51 let mut box_value = [0.0f64; 12];
52 box_value[0..3].copy_from_slice(¢er);
53 box_value[3] = half[0];
54 box_value[7] = half[1];
55 box_value[11] = half[2];
56 Ok(BoundingVolume::Box(box_value))
57 }
58
59 fn to_json(&self) -> Json {
60 match self {
61 BoundingVolume::Box(value) => Json::object(vec![(
62 "box",
63 Json::Array(value.iter().map(|v| Json::Number(v.to_string())).collect()),
64 )]),
65 }
66 }
67
68 fn from_json(json: &Json) -> InterchangeResult<Self> {
69 let box_values = json
70 .get("box")
71 .and_then(Json::as_array)
72 .ok_or_else(|| InterchangeError::InvalidConfiguration("missing tile box".into()))?;
73 if box_values.len() != 12 {
74 return Err(InterchangeError::InvalidConfiguration(
75 "tile box must contain twelve values".into(),
76 ));
77 }
78 let mut value = [0.0f64; 12];
79 for (index, item) in box_values.iter().enumerate() {
80 value[index] = item.as_f64().ok_or_else(|| {
81 InterchangeError::InvalidConfiguration("tile box value is not numeric".into())
82 })?;
83 if !value[index].is_finite() {
84 return Err(InterchangeError::InvalidConfiguration(
85 "tile box must contain finite values".into(),
86 ));
87 }
88 }
89 Ok(BoundingVolume::Box(value))
90 }
91}
92
93#[derive(Clone, Debug, PartialEq)]
95pub struct TileContent {
96 pub uri: String,
98}
99
100#[derive(Clone, Debug, PartialEq)]
102pub struct Tile {
103 pub bounding_volume: BoundingVolume,
105 pub geometric_error: f64,
107 pub refine: Option<Refinement>,
109 pub content: Option<TileContent>,
111 pub children: Vec<Tile>,
113}
114
115impl Tile {
116 fn validate(&self) -> InterchangeResult<()> {
117 if !self.geometric_error.is_finite() || self.geometric_error < 0.0 {
118 return Err(InterchangeError::InvalidConfiguration(
119 "tile geometric error must be finite and non-negative".into(),
120 ));
121 }
122 let child_ids: Vec<_> = self.children.iter().map(|child| child.hash_key()).collect();
123 for index in 0..child_ids.len() {
124 if child_ids.iter().skip(index + 1).any(|id| *id == child_ids[index]) {
125 return Err(InterchangeError::InvalidConfiguration(
126 "tileset contains duplicate child tiles".into(),
127 ));
128 }
129 }
130 for child in &self.children {
131 child.validate()?;
132 }
133 Ok(())
134 }
135
136 fn hash_key(&self) -> String {
137 let mut hasher = std::collections::hash_map::DefaultHasher::new();
138 use std::hash::{Hash, Hasher};
139 serialize_json(&self.bounding_volume.to_json()).hash(&mut hasher);
140 hasher.finish().to_string()
141 }
142
143 fn to_json(&self) -> Json {
144 let mut members = vec![
145 ("boundingVolume", self.bounding_volume.to_json()),
146 ("geometricError", Json::Number(self.geometric_error.to_string())),
147 ];
148 if let Some(refine) = self.refine {
149 members.push(("refine", Json::String(refine.as_str().to_owned())));
150 }
151 if let Some(content) = &self.content {
152 members
153 .push(("content", Json::object(vec![("uri", Json::String(content.uri.clone()))])));
154 }
155 if !self.children.is_empty() {
156 members
157 .push(("children", Json::Array(self.children.iter().map(Tile::to_json).collect())));
158 }
159 Json::object(members)
160 }
161
162 fn from_json(json: &Json, depth: usize) -> InterchangeResult<Self> {
163 if depth > 64 {
164 return Err(InterchangeError::InvalidConfiguration(
165 "tileset nesting exceeds 64 levels".into(),
166 ));
167 }
168 let bounding_volume =
169 json.get("boundingVolume").map(BoundingVolume::from_json).transpose()?.ok_or_else(
170 || InterchangeError::InvalidConfiguration("tile missing boundingVolume".into()),
171 )?;
172 let geometric_error =
173 json.get("geometricError").and_then(Json::as_f64).ok_or_else(|| {
174 InterchangeError::InvalidConfiguration("missing geometricError".into())
175 })?;
176 let refine = json.get("refine").and_then(Json::as_str).and_then(Refinement::from_str);
177 let content = match json.get("content") {
178 None => None,
179 Some(content) => Some(TileContent {
180 uri: content
181 .get("uri")
182 .and_then(Json::as_str)
183 .ok_or_else(|| {
184 InterchangeError::InvalidConfiguration("content missing uri".into())
185 })?
186 .to_owned(),
187 }),
188 };
189 let children = match json.get("children") {
190 None => Vec::new(),
191 Some(Json::Array(children)) => children
192 .iter()
193 .map(|child| Tile::from_json(child, depth + 1))
194 .collect::<InterchangeResult<Vec<_>>>()?,
195 Some(_) => {
196 return Err(InterchangeError::InvalidConfiguration(
197 "tile children must be an array".into(),
198 ));
199 }
200 };
201 let tile = Tile { bounding_volume, geometric_error, refine, content, children };
202 tile.validate()?;
203 Ok(tile)
204 }
205}
206
207#[derive(Clone, Debug, PartialEq)]
209pub struct Tileset {
210 pub geometric_error: f64,
212 pub root: Tile,
214}
215
216impl Tileset {
217 #[must_use]
219 pub fn to_json(&self) -> String {
220 let document = Json::object(vec![
221 ("asset", Json::object(vec![("version", Json::String("1.1".into()))])),
222 ("geometricError", Json::Number(self.geometric_error.to_string())),
223 ("root", self.root.to_json()),
224 ]);
225 serialize_json(&document)
226 }
227}
228
229pub fn serialize_tileset_json(tileset: &Tileset) -> InterchangeResult<String> {
231 validate_tileset(tileset)?;
232 Ok(tileset.to_json())
233}
234
235pub fn parse_tileset_json(document: &str) -> InterchangeResult<Tileset> {
237 let json = parse_json(document)?;
238 let asset = json
239 .get("asset")
240 .ok_or_else(|| InterchangeError::InvalidConfiguration("tileset missing asset".into()))?;
241 let version = asset.get("version").and_then(Json::as_str).ok_or_else(|| {
242 InterchangeError::InvalidConfiguration("tileset asset missing version".into())
243 })?;
244 if version != "1.1" && version != "1.0" {
245 return Err(InterchangeError::InvalidConfiguration(format!(
246 "unsupported tileset version {version}"
247 )));
248 }
249 let geometric_error = json.get("geometricError").and_then(Json::as_f64).ok_or_else(|| {
250 InterchangeError::InvalidConfiguration("tileset missing geometricError".into())
251 })?;
252 let root = Tile::from_json(
253 json.get("root")
254 .ok_or_else(|| InterchangeError::InvalidConfiguration("tileset missing root".into()))?,
255 0,
256 )?;
257 let tileset = Tileset { geometric_error, root };
258 validate_tileset(&tileset)?;
259 Ok(tileset)
260}
261
262fn validate_tileset(tileset: &Tileset) -> InterchangeResult<()> {
263 if !tileset.geometric_error.is_finite() || tileset.geometric_error < 0.0 {
264 return Err(InterchangeError::InvalidConfiguration(
265 "tileset geometric error must be finite and non-negative".into(),
266 ));
267 }
268 tileset.root.validate()?;
269 Ok(())
270}
271
272#[cfg(test)]
273mod tests {
274 use super::{
275 parse_tileset_json, serialize_tileset_json, BoundingVolume, Refinement, Tile, TileContent,
276 Tileset,
277 };
278
279 fn sample_tileset() -> Tileset {
280 let child = Tile {
281 bounding_volume: BoundingVolume::box_from_bounds([0.0, 0.0, 0.0], [1.0, 1.0, 1.0])
282 .unwrap(),
283 geometric_error: 0.0,
284 refine: None,
285 content: Some(TileContent { uri: "1.pnts".into() }),
286 children: Vec::new(),
287 };
288 Tileset {
289 geometric_error: 10.0,
290 root: Tile {
291 bounding_volume: BoundingVolume::box_from_bounds([0.0, 0.0, 0.0], [2.0, 2.0, 2.0])
292 .unwrap(),
293 geometric_error: 10.0,
294 refine: Some(Refinement::Replace),
295 content: Some(TileContent { uri: "0.pnts".into() }),
296 children: vec![child],
297 },
298 }
299 }
300
301 #[test]
302 fn round_trips_hierarchy() {
303 let tileset = sample_tileset();
304 let document = serialize_tileset_json(&tileset).unwrap();
305 let parsed = parse_tileset_json(&document).unwrap();
306 assert_eq!(parsed, tileset);
307 }
308
309 #[test]
310 fn rejects_negative_geometric_error() {
311 let mut tileset = sample_tileset();
312 tileset.root.geometric_error = -1.0;
313 assert!(serialize_tileset_json(&tileset).is_err());
314 }
315
316 #[test]
317 fn rejects_unknown_refine() {
318 let document = r#"{"asset":{"version":"1.1"},"geometricError":1,"root":{"boundingVolume":{"box":[0,0,0,1,0,0,0,1,0,0,0,1]},"geometricError":0,"refine":"SWAP"}}"#;
319 let parsed = parse_tileset_json(document).unwrap();
320 assert_eq!(parsed.root.refine, None);
321 }
322
323 #[test]
324 fn rejects_deep_nesting() {
325 let mut document = String::from(
326 r#"{"asset":{"version":"1.1"},"geometricError":1,"root":{"boundingVolume":{"box":[0,0,0,1,0,0,0,1,0,0,0,1]},"geometricError":0,"children":["#,
327 );
328 for _ in 0..80 {
329 document.push_str(r#"{"boundingVolume":{"box":[0,0,0,1,0,0,0,1,0,0,0,1]},"geometricError":0,"children":["#);
330 }
331 for _ in 0..80 {
332 document.push_str("]}");
333 }
334 document.push_str("]}}");
335 assert!(parse_tileset_json(&document).is_err());
336 }
337}