spatialrust_interchange/tiles3d/
pnts.rs1use crate::json::{parse_json, serialize_json, Json};
4use crate::{InterchangeError, InterchangeResult};
5
6const PNTS_MAGIC: &[u8; 4] = b"pnts";
7const PNTS_VERSION: u32 = 1;
8const HEADER_BYTES: usize = 28;
9const POSITION_COMPONENTS: usize = 3;
10const RGB_COMPONENTS: usize = 3;
11
12#[derive(Clone, Debug, PartialEq)]
17pub struct PntsFeatureTable {
18 pub positions: Vec<f32>,
20 pub rgb: Option<Vec<u8>>,
22 pub rtc_center: Option<[f64; 3]>,
24}
25
26impl PntsFeatureTable {
27 #[must_use]
29 pub fn point_count(&self) -> usize {
30 self.positions.len() / POSITION_COMPONENTS
31 }
32
33 fn validate(&self) -> InterchangeResult<()> {
34 if self.positions.len() % POSITION_COMPONENTS != 0 {
35 return Err(InterchangeError::InvalidConfiguration(
36 "pnts positions length must be a multiple of 3".into(),
37 ));
38 }
39 if let Some(rgb) = &self.rgb {
40 if rgb.len() != self.point_count() * RGB_COMPONENTS {
41 return Err(InterchangeError::InvalidConfiguration(
42 "pnts RGB length must equal three times the point count".into(),
43 ));
44 }
45 }
46 if let Some(center) = self.rtc_center {
47 if center.iter().any(|value| !value.is_finite()) {
48 return Err(InterchangeError::InvalidConfiguration(
49 "pnts RTC_CENTER must contain finite values".into(),
50 ));
51 }
52 }
53 if self.positions.iter().any(|value| !value.is_finite()) {
54 return Err(InterchangeError::InvalidConfiguration(
55 "pnts positions must contain finite values".into(),
56 ));
57 }
58 Ok(())
59 }
60}
61
62pub fn encode_pnts(table: &PntsFeatureTable) -> InterchangeResult<Vec<u8>> {
64 table.validate()?;
65 let point_count = table.point_count();
66 let position_bytes = point_count
67 .checked_mul(POSITION_COMPONENTS)
68 .and_then(|n| n.checked_mul(std::mem::size_of::<f32>()))
69 .ok_or_else(|| {
70 InterchangeError::InvalidConfiguration("pnts position size overflow".into())
71 })?;
72 let rgb_bytes = table.rgb.as_ref().map_or(0, |rgb| rgb.len());
73 let binary_len = position_bytes + rgb_bytes;
74
75 let rgb_offset = position_bytes as u64;
76 let mut members = vec![
77 ("POINTS_LENGTH", Json::Number(point_count.to_string())),
78 ("POSITION", Json::object(vec![("byteOffset", Json::Number("0".into()))])),
79 ];
80 if let Some(center) = table.rtc_center {
81 members.push((
82 "RTC_CENTER",
83 Json::Array(center.iter().map(|v| Json::Number(format_f64(*v))).collect()),
84 ));
85 }
86 if table.rgb.is_some() {
87 members.push((
88 "RGB",
89 Json::object(vec![("byteOffset", Json::Number(rgb_offset.to_string()))]),
90 ));
91 }
92 let mut feature_json = serialize_json(&Json::object(members)).into_bytes();
93 pad_to_8(&mut feature_json);
94
95 let total = HEADER_BYTES
96 .checked_add(feature_json.len())
97 .and_then(|n| n.checked_add(binary_len))
98 .ok_or_else(|| {
99 InterchangeError::InvalidConfiguration("pnts byte length overflow".into())
100 })?;
101 let mut out = Vec::with_capacity(total);
102 out.extend_from_slice(PNTS_MAGIC);
103 out.extend_from_slice(&PNTS_VERSION.to_le_bytes());
104 out.extend_from_slice(&(total as u32).to_le_bytes());
105 out.extend_from_slice(&(feature_json.len() as u32).to_le_bytes());
106 out.extend_from_slice(&(binary_len as u32).to_le_bytes());
107 out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&feature_json);
110 for value in &table.positions {
111 out.extend_from_slice(&value.to_le_bytes());
112 }
113 if let Some(rgb) = &table.rgb {
114 out.extend_from_slice(rgb);
115 }
116 Ok(out)
117}
118
119pub fn decode_pnts(bytes: &[u8]) -> InterchangeResult<PntsFeatureTable> {
121 if bytes.len() < HEADER_BYTES {
122 return Err(InterchangeError::InvalidConfiguration(
123 "pnts tile is shorter than its header".into(),
124 ));
125 }
126 if &bytes[0..4] != PNTS_MAGIC {
127 return Err(InterchangeError::InvalidConfiguration("pnts tile has invalid magic".into()));
128 }
129 let version = read_u32(bytes, 4)?;
130 if version != PNTS_VERSION {
131 return Err(InterchangeError::InvalidConfiguration(format!(
132 "unsupported pnts version {version}"
133 )));
134 }
135 let declared_len = read_u32(bytes, 8)? as usize;
136 if declared_len != bytes.len() {
137 return Err(InterchangeError::InvalidConfiguration(
138 "pnts declared byte length does not match the input".into(),
139 ));
140 }
141 let feature_json_len = read_u32(bytes, 12)? as usize;
142 let feature_binary_len = read_u32(bytes, 16)? as usize;
143 let batch_json_len = read_u32(bytes, 20)? as usize;
144 let batch_binary_len = read_u32(bytes, 24)? as usize;
145
146 let json_start = HEADER_BYTES;
147 let json_end = json_start
148 .checked_add(feature_json_len)
149 .ok_or_else(|| InterchangeError::InvalidConfiguration("pnts JSON range overflow".into()))?;
150 let binary_end = json_end.checked_add(feature_binary_len).ok_or_else(|| {
151 InterchangeError::InvalidConfiguration("pnts binary range overflow".into())
152 })?;
153 let batch_end = binary_end
154 .checked_add(batch_json_len)
155 .and_then(|n| n.checked_add(batch_binary_len))
156 .ok_or_else(|| {
157 InterchangeError::InvalidConfiguration("pnts batch range overflow".into())
158 })?;
159 if batch_end > bytes.len() {
160 return Err(InterchangeError::InvalidConfiguration(
161 "pnts lengths exceed the tile byte length".into(),
162 ));
163 }
164
165 let json_text = std::str::from_utf8(&bytes[json_start..json_end])
166 .map_err(|_| InterchangeError::InvalidConfiguration("pnts JSON is not UTF-8".into()))?;
167 let feature = parse_json(json_text)?;
168 let point_count = feature
169 .get("POINTS_LENGTH")
170 .and_then(Json::as_u64)
171 .ok_or_else(|| InterchangeError::InvalidConfiguration("missing POINTS_LENGTH".into()))?
172 as usize;
173
174 let position_offset = feature
175 .get("POSITION")
176 .and_then(|position| position.get("byteOffset"))
177 .and_then(Json::as_u64)
178 .ok_or_else(|| {
179 InterchangeError::InvalidConfiguration("missing POSITION byteOffset".into())
180 })? as usize;
181 let position_bytes = point_count
182 .checked_mul(POSITION_COMPONENTS)
183 .and_then(|n| n.checked_mul(std::mem::size_of::<f32>()))
184 .ok_or_else(|| {
185 InterchangeError::InvalidConfiguration("pnts position size overflow".into())
186 })?;
187 let position_end = position_offset.checked_add(position_bytes).ok_or_else(|| {
188 InterchangeError::InvalidConfiguration("pnts position range overflow".into())
189 })?;
190 if position_end > feature_binary_len {
191 return Err(InterchangeError::InvalidConfiguration(
192 "pnts POSITION range exceeds the feature binary".into(),
193 ));
194 }
195
196 let mut positions = Vec::with_capacity(position_bytes / 4);
197 for chunk in bytes[json_end + position_offset..json_end + position_end].chunks_exact(4) {
198 positions.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
199 }
200
201 let rgb = match feature.get("RGB") {
202 None => None,
203 Some(rgb) => {
204 let rgb_offset = rgb.get("byteOffset").and_then(Json::as_u64).ok_or_else(|| {
205 InterchangeError::InvalidConfiguration("missing RGB byteOffset".into())
206 })? as usize;
207 let rgb_bytes = point_count.checked_mul(RGB_COMPONENTS).ok_or_else(|| {
208 InterchangeError::InvalidConfiguration("pnts RGB size overflow".into())
209 })?;
210 let rgb_end = rgb_offset.checked_add(rgb_bytes).ok_or_else(|| {
211 InterchangeError::InvalidConfiguration("pnts RGB range overflow".into())
212 })?;
213 if rgb_end > feature_binary_len {
214 return Err(InterchangeError::InvalidConfiguration(
215 "pnts RGB range exceeds the feature binary".into(),
216 ));
217 }
218 Some(bytes[json_end + rgb_offset..json_end + rgb_end].to_vec())
219 }
220 };
221
222 let rtc_center = match feature.get("RTC_CENTER") {
223 None => None,
224 Some(center) => {
225 let values = center.as_array().ok_or_else(|| {
226 InterchangeError::InvalidConfiguration("RTC_CENTER must be an array".into())
227 })?;
228 if values.len() != 3 {
229 return Err(InterchangeError::InvalidConfiguration(
230 "RTC_CENTER must contain three values".into(),
231 ));
232 }
233 let mut out = [0.0f64; 3];
234 for (index, value) in values.iter().enumerate() {
235 out[index] = value.as_f64().ok_or_else(|| {
236 InterchangeError::InvalidConfiguration("RTC_CENTER value is not numeric".into())
237 })?;
238 }
239 Some(out)
240 }
241 };
242
243 Ok(PntsFeatureTable { positions, rgb, rtc_center })
244}
245
246fn read_u32(bytes: &[u8], offset: usize) -> InterchangeResult<u32> {
247 let end = offset
248 .checked_add(4)
249 .ok_or_else(|| InterchangeError::InvalidConfiguration("pnts offset overflow".into()))?;
250 let window = bytes
251 .get(offset..end)
252 .ok_or_else(|| InterchangeError::InvalidConfiguration("pnts header is truncated".into()))?;
253 Ok(u32::from_le_bytes([window[0], window[1], window[2], window[3]]))
254}
255
256fn pad_to_8(bytes: &mut Vec<u8>) {
257 while bytes.len() % 8 != 0 {
258 bytes.push(b' ');
259 }
260}
261
262fn format_f64(value: f64) -> String {
263 if value == value.trunc() && value.abs() < 1e15 {
264 format!("{value:.1}")
265 } else {
266 format!("{value}")
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::{decode_pnts, encode_pnts, PntsFeatureTable};
273
274 #[test]
275 fn round_trips_positions_only() {
276 let table = PntsFeatureTable {
277 positions: vec![0.0, 0.0, 0.0, 1.0, 2.0, 3.0],
278 rgb: None,
279 rtc_center: None,
280 };
281 let encoded = encode_pnts(&table).unwrap();
282 let decoded = decode_pnts(&encoded).unwrap();
283 assert_eq!(decoded, table);
284 assert_eq!(decoded.point_count(), 2);
285 }
286
287 #[test]
288 fn round_trips_rgb_and_center() {
289 let table = PntsFeatureTable {
290 positions: vec![1.0, 2.0, 3.0],
291 rgb: Some(vec![10, 20, 30]),
292 rtc_center: Some([1000.0, 2000.0, 3000.0]),
293 };
294 let encoded = encode_pnts(&table).unwrap();
295 let decoded = decode_pnts(&encoded).unwrap();
296 assert_eq!(decoded, table);
297 }
298
299 #[test]
300 fn rejects_invalid_magic() {
301 let mut encoded = encode_pnts(&PntsFeatureTable {
302 positions: vec![0.0, 0.0, 0.0],
303 rgb: None,
304 rtc_center: None,
305 })
306 .unwrap();
307 encoded[0] = b'X';
308 assert!(decode_pnts(&encoded).is_err());
309 }
310
311 #[test]
312 fn rejects_truncated_tile() {
313 let encoded = encode_pnts(&PntsFeatureTable {
314 positions: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
315 rgb: None,
316 rtc_center: None,
317 })
318 .unwrap();
319 assert!(decode_pnts(&encoded[..encoded.len() - 1]).is_err());
320 assert!(decode_pnts(&encoded[..10]).is_err());
321 }
322
323 #[test]
324 fn rejects_mismatched_rgb_length() {
325 let table = PntsFeatureTable {
326 positions: vec![0.0, 0.0, 0.0],
327 rgb: Some(vec![1, 2]),
328 rtc_center: None,
329 };
330 assert!(encode_pnts(&table).is_err());
331 }
332}