feat: add optional GeoTIFF importer #10

Merged
moldybits merged 4 commits from feat/import-geotiff into main 2026-05-16 16:06:19 -04:00
5 changed files with 493 additions and 6 deletions
Showing only changes of commit e8253b5426 - Show all commits
Generated
+315 -1
View File
@@ -37,6 +37,12 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-activity"
version = "0.6.1"
@@ -461,6 +467,25 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -657,6 +682,12 @@ dependencies = [
"winit",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "emath"
version = "0.32.3"
@@ -712,6 +743,12 @@ version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "fax"
version = "0.2.7"
@@ -749,6 +786,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -809,6 +852,41 @@ dependencies = [
"slab",
]
[[package]]
name = "geotiff-core"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63dcef5fa901867a96414d2e7f41bc29d8ab62f0c386982759d0592d23907622"
[[package]]
name = "geotiff-reader"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59c23512e155c5be744bed8b22b52ac14ec445f4b8301a85c3a8f9a9a8927392"
dependencies = [
"geotiff-core",
"lru",
"memmap2",
"ndarray",
"parking_lot",
"thiserror 2.0.18",
"tiff-reader",
]
[[package]]
name = "geotiff-writer"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff56075759ffbb8531bb7a752f587d8f33ed0ad7dc80e4825558ddaa93a6b3c"
dependencies = [
"geotiff-core",
"ndarray",
"tempfile",
"thiserror 2.0.18",
"tiff-core",
"tiff-writer",
]
[[package]]
name = "gethostname"
version = "1.1.0"
@@ -938,7 +1016,18 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
@@ -1165,6 +1254,21 @@ dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
dependencies = [
"rayon",
]
[[package]]
name = "jpeg-encoder"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b454d911ac55068f53495488d8ccd0646eaa540c033a28ee15b07838afafb01f"
[[package]]
name = "js-sys"
version = "0.3.98"
@@ -1183,6 +1287,38 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "lerc-band-materialize"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07ad02b62a38008d9615a52159fd3b7e56aca97ee013fdec55cfe80040cc5aa5"
[[package]]
name = "lerc-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f428c60749fada87c7a95d08bc3942d05e4a9e3e0543a460a3331bb751dd2ddc"
[[package]]
name = "lerc-reader"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68327488da0851ab38d662f6aa0baf29ee16192a5e286429aa02b3ffd71a5565"
dependencies = [
"lerc-band-materialize",
"lerc-core",
"ndarray",
]
[[package]]
name = "lerc-writer"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d281f7aaa603b0562ceb1901c9b85bec7f19b1f2b9fc651cbc122906f4422411"
dependencies = [
"lerc-core",
]
[[package]]
name = "libc"
version = "0.2.186"
@@ -1256,6 +1392,25 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
dependencies = [
"hashbrown 0.16.1",
]
[[package]]
name = "matrixmultiply"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "memchr"
version = "2.8.0"
@@ -1324,6 +1479,21 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "ndarray"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
dependencies = [
"matrixmultiply",
"num-complex",
"num-integer",
"num-traits",
"portable-atomic",
"portable-atomic-util",
"rawpointer",
]
[[package]]
name = "ndk"
version = "0.9.0"
@@ -1360,6 +1530,24 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -1681,7 +1869,10 @@ version = "0.1.0"
dependencies = [
"clap",
"eframe",
"geotiff-reader",
"geotiff-writer",
"image",
"ndarray",
"serde",
"toml",
]
@@ -1811,6 +2002,15 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -1886,6 +2086,32 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
@@ -2221,6 +2447,19 @@ dependencies = [
"syn",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
]
[[package]]
name = "termcolor"
version = "1.4.1"
@@ -2284,6 +2523,53 @@ dependencies = [
"zune-jpeg",
]
[[package]]
name = "tiff-core"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baaa9ce13dd11a58b8fd1f834c97eb6f752e42f497cb3ec24fa293932a475f4b"
dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "tiff-reader"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98c4f45d57ee731f4a27baf7a4db00e79a6ccecc64f5dd62dc23327b82af9a24"
dependencies = [
"flate2",
"jpeg-decoder",
"lerc-core",
"lerc-reader",
"lru",
"memmap2",
"ndarray",
"parking_lot",
"rayon",
"smallvec",
"thiserror 2.0.18",
"tiff-core",
"weezl",
"zstd",
]
[[package]]
name = "tiff-writer"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22edef41291232fd124f76f05de090143dde08874d919a7bc11882728aa05842"
dependencies = [
"flate2",
"jpeg-encoder",
"lerc-core",
"lerc-writer",
"thiserror 2.0.18",
"tiff-core",
"weezl",
"zstd",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -3236,6 +3522,34 @@ dependencies = [
"syn",
]
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zune-core"
version = "0.4.12"
+6
View File
@@ -8,15 +8,21 @@ default = []
app = ["dep:eframe"]
hgt = []
ascii-grid-import = []
import-geotiff = ["dep:geotiff-reader"]
[dependencies]
clap = { version = "4.6.1", features = ["derive"] }
eframe = { version = "0.32.3", optional = true, default-features = false, features = ["default_fonts", "wayland", "wgpu", "x11"] }
#wgpu = { version = "25.0.2", features = ["metal"] }
image = { version = "0.25.9", default-features = false, features = ["png"] }
geotiff-reader = { version = "0.4.0", optional = true, default-features = false, features = ["local"] }
serde = { version = "1", features = ["derive"] }
toml = "0.8"
[dev-dependencies]
geotiff-writer = { version = "0.4.0", default-features = false }
ndarray = "0.17"
[[bin]]
name = "openvistapro_app"
path = "src/bin/openvistapro_app.rs"
+66 -5
View File
@@ -151,19 +151,67 @@ pub fn supported_presets() -> &'static [&'static str] {
}
pub fn supported_importers() -> &'static [&'static str] {
#[cfg(all(feature = "hgt", feature = "ascii-grid-import"))]
#[cfg(all(
feature = "hgt",
feature = "ascii-grid-import",
feature = "import-geotiff"
))]
{
&["heightmap", "hgt", "esri-ascii-grid", "geotiff"]
}
#[cfg(all(
feature = "hgt",
feature = "ascii-grid-import",
not(feature = "import-geotiff")
))]
{
&["heightmap", "hgt", "esri-ascii-grid"]
}
#[cfg(all(feature = "hgt", not(feature = "ascii-grid-import")))]
#[cfg(all(
feature = "hgt",
not(feature = "ascii-grid-import"),
feature = "import-geotiff"
))]
{
&["heightmap", "hgt", "geotiff"]
}
#[cfg(all(
feature = "hgt",
not(feature = "ascii-grid-import"),
not(feature = "import-geotiff")
))]
{
&["heightmap", "hgt"]
}
#[cfg(all(not(feature = "hgt"), feature = "ascii-grid-import"))]
#[cfg(all(
not(feature = "hgt"),
feature = "ascii-grid-import",
feature = "import-geotiff"
))]
{
&["heightmap", "esri-ascii-grid", "geotiff"]
}
#[cfg(all(
not(feature = "hgt"),
feature = "ascii-grid-import",
not(feature = "import-geotiff")
))]
{
&["heightmap", "esri-ascii-grid"]
}
#[cfg(all(not(feature = "hgt"), not(feature = "ascii-grid-import")))]
#[cfg(all(
not(feature = "hgt"),
not(feature = "ascii-grid-import"),
feature = "import-geotiff"
))]
{
&["heightmap", "geotiff"]
}
#[cfg(all(
not(feature = "hgt"),
not(feature = "ascii-grid-import"),
not(feature = "import-geotiff")
))]
{
&["heightmap"]
}
@@ -322,7 +370,7 @@ mod tests {
}
#[test]
#[cfg(not(feature = "hgt"))]
#[cfg(all(not(feature = "hgt"), not(feature = "import-geotiff")))]
fn hgt_importer_is_hidden_when_feature_is_disabled() {
assert!(!supported_importers().contains(&"hgt"));
}
@@ -340,6 +388,19 @@ mod tests {
assert!(text.contains("hgt"), "got: {text:?}");
}
#[test]
#[cfg(feature = "import-geotiff")]
fn supported_importers_lists_geotiff_when_feature_is_enabled() {
assert!(supported_importers().contains(&"geotiff"));
}
#[test]
#[cfg(feature = "import-geotiff")]
fn info_text_lists_geotiff_importer_when_feature_is_enabled() {
let text = info_text();
assert!(text.contains("geotiff"), "got: {text:?}");
}
#[test]
fn info_text_contains_program_name_and_version() {
let text = info_text();
+11
View File
@@ -64,9 +64,20 @@
//! row-major [`f32`] samples and the source unit is recorded as metres. The
//! parser is gated behind the feature so the default build keeps only the
//! project-owned `ovp-text` importer.
//!
//! # The `geotiff` format
//!
//! With the optional `import-geotiff` Cargo feature enabled, [`geotiff::parse_geotiff_bytes`]
//! reads tiny synthetic GeoTIFF elevation tiles using the pure-Rust
//! `geotiff-reader` crate. The importer starts with a deliberately small
//! supported subset so default builds stay lean.
use std::fmt;
#[cfg(feature = "import-geotiff")]
#[path = "import/geotiff.rs"]
pub mod geotiff;
use crate::terrain::{HeightGrid, TerrainError};
/// Vertical unit of the elevation samples in an imported terrain source.
+95
View File
@@ -0,0 +1,95 @@
//! GeoTIFF terrain importer behind the optional `import-geotiff` feature.
//!
//! This importer parses tiny synthetic GeoTIFF elevation tiles entirely in
//! memory using the pure-Rust `geotiff-reader` crate. It supports a
//! deliberately small subset: a single-band raster decoded as `f32`.
use crate::import::{ElevationUnit, ImportError, ImportedTerrain, TerrainSourceMetadata};
use crate::terrain::HeightGrid;
use geotiff_reader::GeoTiffFile;
/// Format identifier recorded for terrains parsed by [`parse_geotiff_bytes`].
const GEOTIFF_FORMAT: &str = "geotiff";
/// Parse a GeoTIFF payload from memory into an [`ImportedTerrain`].
///
/// Only single-band rasters are supported; the raster is decoded as `f32` and
/// flattened in row-major order into a [`HeightGrid`]. Any reader failure
/// (including non-GeoTIFF input) is reported as [`ImportError::MalformedSource`].
pub fn parse_geotiff_bytes(source: &[u8]) -> Result<ImportedTerrain, ImportError> {
let file = GeoTiffFile::from_bytes(source.to_vec())
.map_err(|e| ImportError::MalformedSource(format!("could not read GeoTIFF: {e}")))?;
let band_count = file.band_count();
if band_count != 1 {
return Err(ImportError::MalformedSource(format!(
"expected a single-band GeoTIFF, got {band_count} bands"
)));
}
let raster = file.read_raster::<f32>().map_err(|e| {
ImportError::MalformedSource(format!("could not decode GeoTIFF raster: {e}"))
})?;
let samples: Vec<f32> = raster.iter().copied().collect();
let width = file.width();
let height = file.height();
let grid = HeightGrid::new(width, height, samples)?;
let metadata = TerrainSourceMetadata::new(GEOTIFF_FORMAT, width, height, ElevationUnit::Meters);
Ok(ImportedTerrain::new(grid, metadata))
}
#[cfg(test)]
mod tests {
use super::parse_geotiff_bytes;
use crate::import::{ElevationUnit, ImportError};
use geotiff_writer::GeoTiffBuilder;
use ndarray::array;
use std::io::Cursor;
fn tiny_synthetic_geotiff_bytes() -> Vec<u8> {
let data = array![[10.0_f32, 20.0, 30.0], [40.0, 50.0, 60.0]];
let mut cursor = Cursor::new(Vec::new());
GeoTiffBuilder::new(3, 2)
.epsg(4326)
.pixel_scale(1.0, 1.0)
.origin(0.0, 2.0)
.write_2d_to(&mut cursor, data.view())
.expect("synthetic GeoTIFF fixture should write");
cursor.into_inner()
}
#[test]
fn parses_tiny_synthetic_geotiff_dimensions_and_samples() {
let bytes = tiny_synthetic_geotiff_bytes();
let imported = parse_geotiff_bytes(&bytes).expect("GeoTIFF fixture should parse");
assert_eq!(imported.grid().width(), 3);
assert_eq!(imported.grid().height(), 2);
assert_eq!(imported.grid().sample(0, 0), Some(10.0));
assert_eq!(imported.grid().sample(2, 1), Some(60.0));
assert_eq!(imported.metadata().format(), "geotiff");
assert_eq!(imported.metadata().width(), 3);
assert_eq!(imported.metadata().height(), 2);
assert_eq!(imported.metadata().elevation_unit(), ElevationUnit::Meters);
}
#[test]
fn rejects_non_geotiff_payloads() {
let err = parse_geotiff_bytes(b"not a geotiff").expect_err("bad bytes must be rejected");
assert!(
matches!(err, ImportError::MalformedSource(_)),
"got: {err:?}"
);
}
#[test]
fn tiny_fixture_is_not_empty() {
let bytes = tiny_synthetic_geotiff_bytes();
assert!(
bytes.len() > 64,
"fixture should contain real GeoTIFF bytes"
);
}
}