1use std::fmt;
49use std::str::FromStr;
50
51pub mod cesium;
52#[cfg(any(feature = "png", feature = "webp", feature = "avif"))]
53pub mod container;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub enum HeightmapFormat {
60 Terrarium,
62 Mapbox,
64 Gsi,
66}
67
68impl HeightmapFormat {
69 pub const ALL: [HeightmapFormat; 3] = [Self::Terrarium, Self::Mapbox, Self::Gsi];
72
73 pub const fn name(self) -> &'static str {
75 match self {
76 Self::Terrarium => "terrarium",
77 Self::Mapbox => "mapbox",
78 Self::Gsi => "gsi",
79 }
80 }
81}
82
83impl fmt::Display for HeightmapFormat {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.write_str(self.name())
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct ParseHeightmapFormatError {
92 pub input: String,
94}
95
96impl fmt::Display for ParseHeightmapFormatError {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(
99 f,
100 "unknown heightmap format `{}` (expected one of: terrarium, mapbox, gsi)",
101 self.input
102 )
103 }
104}
105
106impl std::error::Error for ParseHeightmapFormatError {}
107
108impl FromStr for HeightmapFormat {
109 type Err = ParseHeightmapFormatError;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
115 match s.to_ascii_lowercase().as_str() {
116 "terrarium" => Ok(Self::Terrarium),
117 "mapbox" | "mapbox-rgb" | "terrain-rgb" => Ok(Self::Mapbox),
118 "gsi" | "gsi-dem" => Ok(Self::Gsi),
119 _ => Err(ParseHeightmapFormatError {
120 input: s.to_string(),
121 }),
122 }
123 }
124}
125
126#[inline]
129pub fn encode_pixel(format: HeightmapFormat, elevation: f32) -> [u8; 3] {
130 match format {
131 HeightmapFormat::Terrarium => terrarium::encode_pixel(elevation),
132 HeightmapFormat::Mapbox => mapbox::encode_pixel(elevation),
133 HeightmapFormat::Gsi => gsi::encode_pixel(elevation),
134 }
135}
136
137#[inline]
140pub fn decode_pixel(format: HeightmapFormat, rgb: [u8; 3]) -> f32 {
141 match format {
142 HeightmapFormat::Terrarium => terrarium::decode_pixel(rgb),
143 HeightmapFormat::Mapbox => mapbox::decode_pixel(rgb),
144 HeightmapFormat::Gsi => gsi::decode_pixel(rgb),
145 }
146}
147
148pub fn encode(format: HeightmapFormat, elevations: &[f32], width: u32, height: u32) -> Vec<u8> {
155 match format {
156 HeightmapFormat::Terrarium => terrarium::encode(elevations, width, height),
157 HeightmapFormat::Mapbox => mapbox::encode(elevations, width, height),
158 HeightmapFormat::Gsi => gsi::encode(elevations, width, height),
159 }
160}
161
162pub fn decode(format: HeightmapFormat, rgb: &[u8], width: u32, height: u32) -> Vec<f32> {
169 match format {
170 HeightmapFormat::Terrarium => terrarium::decode(rgb, width, height),
171 HeightmapFormat::Mapbox => mapbox::decode(rgb, width, height),
172 HeightmapFormat::Gsi => gsi::decode(rgb, width, height),
173 }
174}
175
176pub mod terrarium {
182 #[inline]
188 pub fn encode_pixel(elevation: f32) -> [u8; 3] {
189 let v = if elevation.is_nan() {
190 0.0
191 } else {
192 (elevation + 32768.0) * 256.0
193 };
194 let v = v.clamp(0.0, (1u32 << 24) as f32 - 1.0) as u32;
195 [
196 ((v >> 16) & 0xff) as u8,
197 ((v >> 8) & 0xff) as u8,
198 (v & 0xff) as u8,
199 ]
200 }
201
202 #[inline]
204 pub fn decode_pixel(rgb: [u8; 3]) -> f32 {
205 let r = rgb[0] as f32;
206 let g = rgb[1] as f32;
207 let b = rgb[2] as f32;
208 r * 256.0 + g + b / 256.0 - 32768.0
209 }
210
211 pub fn encode(elevations: &[f32], width: u32, height: u32) -> Vec<u8> {
220 let expected = (width as usize) * (height as usize);
221 assert_eq!(
222 elevations.len(),
223 expected,
224 "elevations length mismatch: expected {expected}, got {}",
225 elevations.len()
226 );
227 let mut out = Vec::with_capacity(expected * 3);
228 for &e in elevations {
229 out.extend_from_slice(&encode_pixel(e));
230 }
231 out
232 }
233
234 pub fn decode(rgb: &[u8], width: u32, height: u32) -> Vec<f32> {
240 let pixels = (width as usize) * (height as usize);
241 assert_eq!(
242 rgb.len(),
243 pixels * 3,
244 "rgb length mismatch: expected {}, got {}",
245 pixels * 3,
246 rgb.len()
247 );
248 rgb.chunks_exact(3)
249 .map(|c| decode_pixel([c[0], c[1], c[2]]))
250 .collect()
251 }
252}
253
254pub mod mapbox {
260 #[inline]
266 pub fn encode_pixel(elevation: f32) -> [u8; 3] {
267 let v = if elevation.is_nan() {
268 0.0
269 } else {
270 ((elevation + 10000.0) * 10.0).round()
271 };
272 let v = v.clamp(0.0, (1u32 << 24) as f32 - 1.0) as u32;
273 [
274 ((v >> 16) & 0xff) as u8,
275 ((v >> 8) & 0xff) as u8,
276 (v & 0xff) as u8,
277 ]
278 }
279
280 #[inline]
283 pub fn decode_pixel(rgb: [u8; 3]) -> f32 {
284 let r = rgb[0] as f32;
285 let g = rgb[1] as f32;
286 let b = rgb[2] as f32;
287 -10000.0 + (r * 65536.0 + g * 256.0 + b) * 0.1
288 }
289
290 pub fn encode(elevations: &[f32], width: u32, height: u32) -> Vec<u8> {
296 let expected = (width as usize) * (height as usize);
297 assert_eq!(
298 elevations.len(),
299 expected,
300 "elevations length mismatch: expected {expected}, got {}",
301 elevations.len()
302 );
303 let mut out = Vec::with_capacity(expected * 3);
304 for &e in elevations {
305 out.extend_from_slice(&encode_pixel(e));
306 }
307 out
308 }
309
310 pub fn decode(rgb: &[u8], width: u32, height: u32) -> Vec<f32> {
316 let pixels = (width as usize) * (height as usize);
317 assert_eq!(
318 rgb.len(),
319 pixels * 3,
320 "rgb length mismatch: expected {}, got {}",
321 pixels * 3,
322 rgb.len()
323 );
324 rgb.chunks_exact(3)
325 .map(|c| decode_pixel([c[0], c[1], c[2]]))
326 .collect()
327 }
328}
329
330pub mod gsi {
344 pub const SENTINEL_RGB: [u8; 3] = [0x80, 0x00, 0x00];
346 const SIGN_BIT: u32 = 1 << 23;
350 const RANGE: i64 = 1 << 24;
352
353 #[inline]
359 pub fn encode_pixel(elevation: f32) -> [u8; 3] {
360 if elevation.is_nan() {
361 return SENTINEL_RGB;
362 }
363 let raw = (elevation as f64 * 100.0).round() as i64;
364 let x = raw.rem_euclid(RANGE) as u32;
365 [
366 ((x >> 16) & 0xff) as u8,
367 ((x >> 8) & 0xff) as u8,
368 (x & 0xff) as u8,
369 ]
370 }
371
372 #[inline]
376 pub fn decode_pixel(rgb: [u8; 3]) -> f32 {
377 let r = rgb[0] as u32;
378 let g = rgb[1] as u32;
379 let b = rgb[2] as u32;
380 let x = (r << 16) | (g << 8) | b;
381 if x == SIGN_BIT {
382 f32::NAN
383 } else if x >= SIGN_BIT {
384 (x as i64 - RANGE) as f32 * 0.01
385 } else {
386 x as f32 * 0.01
387 }
388 }
389
390 pub fn encode(elevations: &[f32], width: u32, height: u32) -> Vec<u8> {
396 let expected = (width as usize) * (height as usize);
397 assert_eq!(
398 elevations.len(),
399 expected,
400 "elevations length mismatch: expected {expected}, got {}",
401 elevations.len()
402 );
403 let mut out = Vec::with_capacity(expected * 3);
404 for &e in elevations {
405 out.extend_from_slice(&encode_pixel(e));
406 }
407 out
408 }
409
410 pub fn decode(rgb: &[u8], width: u32, height: u32) -> Vec<f32> {
418 let pixels = (width as usize) * (height as usize);
419 assert_eq!(
420 rgb.len(),
421 pixels * 3,
422 "rgb length mismatch: expected {}, got {}",
423 pixels * 3,
424 rgb.len()
425 );
426 rgb.chunks_exact(3)
427 .map(|c| decode_pixel([c[0], c[1], c[2]]))
428 .collect()
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 fn approx_eq(a: f32, b: f32, tol: f32) -> bool {
437 (a - b).abs() <= tol
438 }
439
440 #[test]
441 fn terrarium_pixel_roundtrip() {
442 for e in [-100.0_f32, 0.0, 100.0, 1234.5, 8000.0, -500.0] {
443 let back = terrarium::decode_pixel(terrarium::encode_pixel(e));
444 assert!(approx_eq(e, back, 0.01), "{e} → {back}");
445 }
446 }
447
448 #[test]
449 fn terrarium_zero_sea_level_is_8000() {
450 assert_eq!(terrarium::encode_pixel(0.0), [0x80, 0x00, 0x00]);
452 }
453
454 #[test]
455 fn terrarium_bulk_matches_pixel() {
456 let elevations: Vec<f32> = vec![-100.0, 0.0, 100.0, 1234.5, 8000.0, -500.0];
457 let bulk = terrarium::encode(&elevations, 6, 1);
458 let from_pixels: Vec<u8> = elevations
459 .iter()
460 .flat_map(|&e| terrarium::encode_pixel(e))
461 .collect();
462 assert_eq!(bulk, from_pixels);
463 }
464
465 #[test]
466 fn mapbox_pixel_roundtrip() {
467 for e in [-100.0_f32, 0.0, 100.0, 1234.5, 8000.0, -500.0] {
468 let back = mapbox::decode_pixel(mapbox::encode_pixel(e));
469 assert!(approx_eq(e, back, 0.1), "{e} → {back}");
470 }
471 }
472
473 #[test]
474 fn mapbox_minimum_value_is_minus_10000() {
475 assert_eq!(mapbox::encode_pixel(-10000.0), [0, 0, 0]);
476 assert_eq!(mapbox::decode_pixel([0, 0, 0]), -10000.0);
477 }
478
479 #[test]
480 fn gsi_sentinel_decodes_to_nan() {
481 assert!(gsi::decode_pixel([0x80, 0x00, 0x00]).is_nan());
482 }
483
484 #[test]
485 fn gsi_nan_encodes_to_sentinel() {
486 assert_eq!(gsi::encode_pixel(f32::NAN), [0x80, 0x00, 0x00]);
487 }
488
489 #[test]
490 fn gsi_pixel_roundtrip_positive_and_negative() {
491 for e in [0.0_f32, 100.0, 3776.24, -10.5, -429.4] {
492 let back = gsi::decode_pixel(gsi::encode_pixel(e));
493 assert!(approx_eq(e, back, 0.01), "{e} → {back}");
494 }
495 }
496
497 #[test]
498 fn gsi_zero_is_all_zero_rgb() {
499 assert_eq!(gsi::encode_pixel(0.0), [0, 0, 0]);
500 }
501
502 #[test]
503 fn format_from_str_accepts_aliases() {
504 assert_eq!("terrarium".parse(), Ok(HeightmapFormat::Terrarium));
505 assert_eq!("TERRARIUM".parse(), Ok(HeightmapFormat::Terrarium));
506 assert_eq!("mapbox".parse(), Ok(HeightmapFormat::Mapbox));
507 assert_eq!("mapbox-rgb".parse(), Ok(HeightmapFormat::Mapbox));
508 assert_eq!("terrain-rgb".parse(), Ok(HeightmapFormat::Mapbox));
509 assert_eq!("gsi".parse(), Ok(HeightmapFormat::Gsi));
510 assert_eq!("gsi-dem".parse(), Ok(HeightmapFormat::Gsi));
511 assert!("bogus".parse::<HeightmapFormat>().is_err());
512 }
513
514 #[test]
515 fn format_display_roundtrips_through_from_str() {
516 for fmt in HeightmapFormat::ALL {
517 let parsed: HeightmapFormat = fmt.to_string().parse().unwrap();
518 assert_eq!(parsed, fmt);
519 }
520 }
521
522 #[test]
523 fn dispatch_matches_per_module_for_every_format() {
524 let elevations: Vec<f32> = vec![-100.0, 0.0, 100.0, 1234.5, -500.0];
525 for fmt in HeightmapFormat::ALL {
526 let dispatched = encode(fmt, &elevations, elevations.len() as u32, 1);
528 let direct = match fmt {
529 HeightmapFormat::Terrarium => terrarium::encode(&elevations, 5, 1),
530 HeightmapFormat::Mapbox => mapbox::encode(&elevations, 5, 1),
531 HeightmapFormat::Gsi => gsi::encode(&elevations, 5, 1),
532 };
533 assert_eq!(dispatched, direct, "encode mismatch for {fmt}");
534
535 for &e in &elevations {
537 let px = encode_pixel(fmt, e);
538 let back = decode_pixel(fmt, px);
539 assert!((e - back).abs() <= 0.1, "[{fmt}] {e} → {px:?} → {back}");
541 }
542 }
543 }
544
545 #[test]
546 fn gsi_bulk_matches_pixel() {
547 let elevations: Vec<f32> = vec![0.0, 100.0, -10.5, f32::NAN, 3776.24];
548 let bulk = gsi::encode(&elevations, elevations.len() as u32, 1);
549 let from_pixels: Vec<u8> = elevations
550 .iter()
551 .flat_map(|&e| gsi::encode_pixel(e))
552 .collect();
553 assert_eq!(bulk, from_pixels);
554 }
555}