feat: add optional GeoTIFF importer #10
Generated
+315
-1
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -36,6 +36,9 @@ Importer status:
|
||||
- `ovp-text`: project-owned plain-text heightfield fixture format used for import-boundary tests.
|
||||
- `hgt`: enabled by the optional `hgt` Cargo feature; parses SRTM HGT payloads as square grids of big-endian signed 16-bit metre samples. The implementation and tests use open specifications and synthetic/tiny fixtures only.
|
||||
- `esri-ascii-grid`: enabled by the optional `ascii-grid-import` Cargo feature; parses open ESRI ASCII Grid text with synthetic/project-owned fixtures only.
|
||||
- `geotiff`: enabled by the optional `import-geotiff` Cargo feature; parses single-band GeoTIFF elevation tiles in memory via the pure-Rust `geotiff-reader` crate (no GDAL, no native dependency). It supports a deliberately narrow subset — a single-band raster decoded as `f32` — and is reported by `openvistapro info` only when the feature is built.
|
||||
|
||||
All importer tests use tiny synthetic, project-owned fixture data: HGT uses inline synthetic byte arrays, ESRI ASCII Grid uses tiny project-owned text fixtures, and the GeoTIFF tests generate a tiny single-band tile in memory rather than reading committed binaries or real geodata.
|
||||
|
||||
To verify the importer feature surface:
|
||||
|
||||
@@ -47,6 +50,9 @@ cargo test ascii_grid --features ascii-grid-import
|
||||
cargo run --features hgt --bin openvistapro -- info
|
||||
cargo run --features ascii-grid-import --bin openvistapro -- info
|
||||
cargo test --no-default-features
|
||||
cargo test geotiff --features import-geotiff
|
||||
cargo run --features import-geotiff --bin openvistapro -- info
|
||||
cargo test --all-features
|
||||
```
|
||||
|
||||
The default `render` mode writes a deterministic top-down elevation preview.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Start simple, then split into crates when module boundaries stabilize.
|
||||
|
||||
- `src/terrain.rs`: height grid, bounds, sampling, and deterministic terrain fixtures.
|
||||
- `src/import.rs`: importers for open/safe formats (`ovp-text`, feature-gated HGT, feature-gated ESRI ASCII Grid); historical compatibility later.
|
||||
- `src/import.rs`: importers for open/safe formats (`ovp-text`, feature-gated HGT, feature-gated ESRI ASCII Grid, and feature-gated GeoTIFF via `src/import/geotiff.rs`); historical compatibility later. Each importer yields the same internal `HeightGrid` plus `TerrainSourceMetadata`, keeping source formats out of renderer code.
|
||||
- `src/scene.rs` and `src/scene_file.rs`: camera, light, atmosphere, water/vegetation thresholds, and `.ovp.toml` persistence.
|
||||
- `src/render.rs`: deterministic CPU top-down renderer plus CPU perspective demo renderer; WGPU renderer later.
|
||||
- `src/script.rs` and `src/script_exec.rs`: parse and execute project-owned OpenVistaPro script commands.
|
||||
@@ -22,7 +22,7 @@ Start simple, then split into crates when module boundaries stabilize.
|
||||
- `image` for PNG output and texture loading.
|
||||
- `nalgebra` or `glam` for vector/matrix math.
|
||||
- `wgpu` plus `winit`/`egui` for the eventual interactive app.
|
||||
- `gdal` support should be optional because native GDAL dependency setup can be heavy.
|
||||
- GeoTIFF support uses the pure-Rust `geotiff-reader` crate behind the optional `import-geotiff` feature, so default builds stay free of native dependencies. `gdal` is intentionally not used; any future GDAL-backed path would be a separate, opt-in feature because native GDAL setup can be heavy.
|
||||
|
||||
## Development strategy
|
||||
|
||||
|
||||
@@ -1,63 +1,42 @@
|
||||
# Feature Inventory
|
||||
|
||||
This is a first-pass inventory from the VistaPro 2/3 manuals, MakePath guide, screenshots, and public descriptions.
|
||||
This is a normalized reconciliation of the VistaPro manuals, MakePath guide, screenshots, and current OpenVistaPro implementation.
|
||||
|
||||
## Terrain sources
|
||||
Status counts by normalized feature family:
|
||||
- Implemented: 7
|
||||
- Partial: 7
|
||||
- Planned: 6
|
||||
|
||||
- VistaPro landscape files.
|
||||
- USGS DEM elevation data.
|
||||
- NASA/planetary or other gridded elevation datasets.
|
||||
- Fractal landscape generation.
|
||||
- User-supplied two-dimensional integer height arrays, up to historical limits around 1024x1024.
|
||||
Notes:
|
||||
- “Implemented” means the current codebase has a working, tested slice for that family.
|
||||
- “Partial” means the codebase covers part of the family but not the full manual-described workflow.
|
||||
- “Planned” means the family is still absent or only mentioned as roadmap context.
|
||||
|
||||
## Scene controls
|
||||
## Normalized feature families
|
||||
|
||||
- Camera and target X/Y/Z positioning.
|
||||
- Lens/range controls.
|
||||
- Bank, heading, and pitch.
|
||||
- Sea level and water planes.
|
||||
- Lake and river controls.
|
||||
- Timber/tree line, tree drawing, and tree density.
|
||||
- Snow line.
|
||||
- Haze/fog distance.
|
||||
- Sky/stars options.
|
||||
- Light direction and custom lighting.
|
||||
- Vertical exaggeration.
|
||||
- Color maps/palettes.
|
||||
- Texture image loading, including PCX in the Windows manual.
|
||||
| Feature family | Manual / reference evidence | OpenVistaPro status | Implementation evidence | Gap / next step |
|
||||
|---|---|---|---|---|
|
||||
| Terrain sources and compatibility boundary | VistaPro 2 manual: Load Landscape / Save Landscape; VistaPro 3 manual: Load, Save, Exp/Imp menus and DEM/PCX/Targa24 references. | Partial | `src/import.rs`, `src/cli.rs` (`supported_importers()`), `README.md` importer status. | OpenVistaPro intentionally keeps the clean internal model separate and does not claim legacy VistaPro format compatibility. |
|
||||
| Project-owned plain-text heightfields (`ovp-text`) | Clean-room project fixture format, not part of the legacy manuals; used to model the import boundary safely. | Implemented | `src/import.rs` (`import_ovp_text`), tests in `src/import.rs`, fixture in `tests/fixtures/open/`. | No gap for the MVP slice; this is the project-owned test/import path. |
|
||||
| SRTM / HGT terrain import | VistaPro manuals describe loading DEM-like landscape data; the open equivalent is the SRTM/HGT family. | Implemented | `src/import.rs` (`import_hgt` behind `hgt`), `README.md`, tests in `src/import.rs`. | Still only the open SRTM slice; broader compatibility formats remain separate. |
|
||||
| GeoTIFF terrain import | Modern open terrain source, not a legacy VistaPro format. | Implemented | `src/import/geotiff.rs` behind `import-geotiff`, tests in that module. | Deliberately narrow subset: tiny synthetic single-band raster support only. |
|
||||
| Fractal / synthetic terrain generation | VistaPro overview calls out fractal landscapes and generated terrain. | Partial | `src/terrain.rs` (`plane`, `radial_hill`), `src/app_state.rs` presets. | Current terrain generation is only deterministic fixtures, not a true fractal/noise terrain engine. |
|
||||
| Camera and target placement | VistaPro 2 / 3 manuals: “Setting Camera and Target”; screenshot workflow uses camera/target gadgets. | Implemented | `src/scene.rs` (`Camera`), `src/app.rs` (camera position/target controls), `src/app_state.rs`. | Only the core position/target slice exists; there is no map-click placement UI yet. |
|
||||
| Lens / range / orientation controls | VistaPro manuals describe lens/range, bank, heading, and pitch controls. | Partial | `src/scene.rs` (`Camera.fov_degrees`), `src/render.rs` perspective renderer. | No explicit bank/heading/pitch model or legacy lens/range UI yet. |
|
||||
| Water / sea level, tree line, snow line, haze | Manuals repeatedly mention tree line, snow line, water level, haze, and atmospheric tuning. | Implemented | `src/scene.rs`, `src/app.rs` sliders, `src/colormap.rs`, `src/render.rs`. | Rivers/lakes are still missing, but the core elevation-band controls are present. |
|
||||
| Rivers and lakes | VistaPro manuals explicitly mention rivers and lakes as adjustable landscape features. | Planned | Not yet represented in `Scene` or renderer code. | Add hydrology controls/data model before claiming this family. |
|
||||
| Light direction and custom lighting | Manuals discuss sunlight placement and lighting experiments. | Partial | `src/scene.rs` (`Light`), `src/render.rs`, `src/app.rs` (light state exists in the scene model even if UI is minimal). | The current model is much simpler than VistaPro’s lighting workflow and lacks richer light controls. |
|
||||
| Vertical exaggeration | VistaPro manuals describe vertical scaling / scene exaggeration controls. | Planned | No dedicated field or control in the current scene model. | Add an explicit vertical-scale parameter and render integration. |
|
||||
| Color maps / palettes / texture image loading | VistaPro 3 manual includes loading PCX images, adding texture, and saving/loading color maps. | Partial | `src/colormap.rs` fixed bands, `src/render.rs` uses scene thresholds. | No color-map editor, no palette import/export, and no PCX/texture loading yet. |
|
||||
| Preview / final render workflow | VistaPro manuals describe rough preview rendering and full render output. | Implemented | `src/render.rs` (`render_top_down`, `render_perspective`), `src/cli.rs` (`render`), tests in `src/render.rs`. | The preview/final split is still simplified, but the core render outputs are working. |
|
||||
| Render quality presets / smoothing / detail tradeoffs | VistaPro manuals describe quality menus and poly/detail tradeoffs. | Planned | No dedicated quality preset system in current code. | Add explicit quality presets or a render-quality profile object. |
|
||||
| Scene file save/load (`.ovp.toml`) | Not a VistaPro legacy format; this is the clean-room OpenVistaPro scene format. | Implemented | `src/scene_file.rs`, `src/cli.rs` (`scene export`), tests in `src/scene_file.rs`. | No gap for the project-owned scene format slice. |
|
||||
| Script language parser | MakePath guide and VistaPro manual describe scripts and “Run Script” workflows. | Partial | `src/script.rs` parser, tests in `src/script.rs`, `README.md` script section. | Parser exists, but script execution is intentionally deferred. |
|
||||
| Script execution and animation frames | MakePath guide says scripts should render full animations and VistaPro can run scripts from the Script menu. | Planned | No script runner or frame-sequencing engine exists yet. | Add execution semantics once the command model is stable. |
|
||||
| MakePath-style path generation and motion models | MakePath guide describes spline nodes, previewing a path, and vehicle models (jet, glider, dune buggy, motorcycle, helicopter, cruise missile, custom). | Planned | No path generator or motion-model layer exists yet. | This is a separate planner/animation feature, not just a script parser. |
|
||||
| UI shell, menus, dialogs, and numeric gadgets | VistaPro screenshots/manuals show dense menus, dialogs, map tools, and numeric gadgets. | Partial | `src/app.rs`, `src/app_state.rs`, `src/bin/openvistapro_app.rs`. | Current UI is an egui CPU-preview shell with a small control set, not the legacy menu hierarchy. |
|
||||
| Legacy image / landscape export formats | VistaPro manuals mention saving rendered images and landscapes in formats like IFF/IFF24/RGB and DEM/binary landscape files. | Planned | Current output is PNG plus project-owned `.ovp.toml` scenes. | Add separate compatibility/export work only after the clean internal pipeline is stable. |
|
||||
|
||||
## Rendering and quality
|
||||
## Current reconciliation summary
|
||||
|
||||
- Wire/topographic preview workflow.
|
||||
- Progressive/final render action.
|
||||
- Quality menu/settings for smoothing/detail/performance tradeoffs.
|
||||
- Save rendered images; historical formats included IFF/IFF24/RGB on Amiga and PC-era formats on Windows.
|
||||
- 24-bit output was a differentiator in user examples.
|
||||
|
||||
## Scripting and animation
|
||||
|
||||
- Script files list camera/target positions and commands.
|
||||
- Scripts are landscape-independent enough to rerun against different terrain/settings.
|
||||
- Commands include camera coordinates, vertical scale, and render actions.
|
||||
- VistaPro could generate simple linear camera-to-target paths.
|
||||
- MakePath Flight Director created spline paths over a topographic map and exported VistaPro scripts.
|
||||
- MakePath vehicle models included jet, glider, dune buggy, motorcycle, helicopter, cruise missile, and custom vehicles.
|
||||
|
||||
## UI surfaces observed in screenshots
|
||||
|
||||
- Main workspace with topographic map/preview on the left and dense control panels on the right.
|
||||
- Pull-down menus: Project, Load, Save, Exp/Imp, Script, Image, Quality, and related dialogs.
|
||||
- File dialogs and confirmation/progress dialogs.
|
||||
- Numeric/text gadgets for direct value editing.
|
||||
- Multiple lower control panels for color-map/palette editing, fractal settings, lighting, and scene/environment settings.
|
||||
- Color-map editor with vertical channel/range sliders and palette swatches.
|
||||
- Map/radar-like view for camera/target placement.
|
||||
|
||||
## Modernization opportunities
|
||||
|
||||
- Replace dense fixed control panels with an immediate-mode or dockable UI.
|
||||
- Use modern terrain formats: GeoTIFF/COG, DEM, HGT/SRTM, PNG/RAW heightmaps.
|
||||
- Use WGPU for cross-platform rendering.
|
||||
- Add reproducible scene files in human-readable formats such as RON/TOML/JSON.
|
||||
- Provide CLI rendering for tests and batch workflows.
|
||||
- Keep compatibility import/export separate from the clean internal model.
|
||||
OpenVistaPro already covers the core clean-room pipeline: terrain grids, open importers, scene state, preview/final rendering, project-owned scene files, and a small script parser. The remaining VistaPro-specific gaps cluster around legacy compatibility, richer scene controls, script execution, MakePath-style animation tooling, and the old dense UI/menu workflow.
|
||||
|
||||
@@ -19,7 +19,7 @@ The repository already has:
|
||||
- `src/scene_file.rs`: project-owned `.ovp.toml` files with `schema = "openvistapro.scene"`, `version = 1`, and a serialized `Scene` payload.
|
||||
- `src/colormap.rs`: deterministic elevation-band colors.
|
||||
- `src/render.rs`: deterministic top-down PNG renderer plus spike-quality CPU perspective raymarcher.
|
||||
- `src/import.rs`: open-format import boundary with `ovp-text`, feature-gated SRTM/HGT bytes, and feature-gated ESRI ASCII Grid parsing.
|
||||
- `src/import.rs`: open-format import boundary with `ovp-text`, feature-gated SRTM/HGT bytes, feature-gated ESRI ASCII Grid parsing, and feature-gated GeoTIFF parsing.
|
||||
- `src/script.rs` and `src/script_exec.rs`: project-owned script parsing and execution for presets, grayscale PNG heightmaps, threshold changes, and PNG render outputs.
|
||||
- `src/path.rs`: deterministic MakePath-inspired camera keyframe interpolation.
|
||||
- `src/app_state.rs`, `src/app.rs`, and `src/bin/openvistapro_app.rs`: feature-gated `app` shell using `eframe`/`egui` with scene controls and CPU preview rendering.
|
||||
@@ -348,35 +348,52 @@ Run: `cargo test dem --features import-dem`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task C3: Add GeoTIFF spike behind an optional feature
|
||||
### Task C3: GeoTIFF importer behind an optional feature (implemented)
|
||||
|
||||
**Objective:** Decide whether to use a pure-Rust TIFF path or optional GDAL without making default builds fragile.
|
||||
**Status:** Done. Landed as the optional `import-geotiff` feature; this section
|
||||
records the implemented design. The crate survey behind it is
|
||||
[`docs/research/geotiff-import-strategy.md`](../research/geotiff-import-strategy.md).
|
||||
|
||||
**Objective:** Parse single-band GeoTIFF elevation tiles into the internal
|
||||
`HeightGrid` without making default builds fragile or pulling a native
|
||||
dependency.
|
||||
|
||||
**Outcome:** A pure-Rust path was chosen over GDAL. The importer uses the
|
||||
`geotiff-reader` crate (`local` feature, `cog`/network feature off), declared as
|
||||
an optional dependency gated by the `import-geotiff` Cargo feature. There is no
|
||||
GDAL dependency and no native toolchain requirement, so `import-geotiff` builds
|
||||
and tests run anywhere `cargo` runs — the "documented skip if native GDAL is not
|
||||
installed" escape hatch is not needed and is reserved for any future,
|
||||
separately reviewed GDAL-backed feature.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/import/geotiff.rs`
|
||||
- Modify: `Cargo.toml`
|
||||
- Modify: `docs/plans/phase-4-formats-scripts-ui.md` or follow-up notes if the spike changes direction
|
||||
- Test: tiny openly licensed or generated GeoTIFF fixture only if licensing is clear
|
||||
- `src/import/geotiff.rs`: `cfg(feature = "import-geotiff")` module exposing
|
||||
`parse_geotiff_bytes(&[u8]) -> Result<ImportedTerrain, ImportError>`. It reads
|
||||
the payload in memory, rejects non-single-band rasters, decodes the raster as
|
||||
`f32`, and builds a `HeightGrid` plus `TerrainSourceMetadata` (format
|
||||
`"geotiff"`). Reader/decode failures map to `ImportError::MalformedSource`.
|
||||
- `src/import.rs`: declares the `geotiff` submodule under the feature cfg.
|
||||
- `Cargo.toml`: `import-geotiff = ["dep:geotiff-reader"]`; `geotiff-writer` and
|
||||
`ndarray` are dev-dependencies used only to generate the test fixture.
|
||||
- `src/cli.rs`: `supported_importers()` and `info_text()` list `geotiff` only
|
||||
when the feature is built. (A `render --import-geotiff` flag mirroring the HGT
|
||||
path remains future work.)
|
||||
|
||||
**Step 1: Write failing test or spike acceptance check**
|
||||
**Fixture hygiene:** Tests do not commit any GeoTIFF binary. They generate a
|
||||
tiny single-band elevation tile in memory with `geotiff-writer` (dev-dependency)
|
||||
and parse it back, mirroring the synthetic-fixture approach used for HGT. No
|
||||
proprietary data and no large real-world DEMs enter the repository; real tiles
|
||||
may be used only for local manual verification under the already-ignored
|
||||
`reference/` and `.work/` directories.
|
||||
|
||||
Prefer a generated fixture committed under an explicit license. If that is not practical, write parser-interface tests and keep the full fixture local until license is resolved.
|
||||
**Validation**
|
||||
|
||||
**Step 2: Verify RED**
|
||||
Run: `cargo test --no-default-features`, `cargo test geotiff --features import-geotiff`,
|
||||
and `cargo test --all-features`.
|
||||
|
||||
Run: `cargo test geotiff --features import-geotiff`
|
||||
|
||||
Expected: FAIL until the selected crate and parser stub exist.
|
||||
|
||||
**Step 3: Implement minimal code**
|
||||
|
||||
Add only enough to detect/parse a tiny supported subset. If GDAL is required, keep the feature opt-in and document native setup.
|
||||
|
||||
**Step 4: Verify GREEN**
|
||||
|
||||
Run: `cargo test --features import-geotiff`
|
||||
|
||||
Expected: PASS on systems with the optional dependency available, or a documented skip if native GDAL is not installed.
|
||||
Expected: default and `--no-default-features` builds stay GeoTIFF-free; the
|
||||
feature build and `--all-features` exercise the parser and pass without any
|
||||
native dependency.
|
||||
|
||||
## Milestone D: Scene/project metadata evolution
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
# Terrain Generation Roadmap
|
||||
|
||||
## Purpose
|
||||
|
||||
Define the next terrain-generation workstream so future slices stay small, deterministic, and testable. This plan is intentionally clean-room: it uses only project-owned synthetic fixtures and open math/algorithm ideas, never proprietary VistaPro material.
|
||||
|
||||
## Current baseline
|
||||
|
||||
OpenVistaPro already has:
|
||||
|
||||
- `src/terrain.rs` with immutable `HeightGrid` storage, safe indexing, min/max, and deterministic `plane` / `radial_hill` fixtures.
|
||||
- `src/render.rs` with a deterministic top-down preview and a CPU perspective spike that only depends on `HeightGrid` + `Scene`.
|
||||
- `src/scene.rs` and `src/app_state.rs` for scene controls and preview wiring.
|
||||
- `src/import.rs` for the open-format import boundary.
|
||||
- `docs/plans/initial-roadmap.md` and `docs/plans/phase-4-formats-scripts-ui.md` for the broader project sequence.
|
||||
|
||||
The missing piece is a dedicated procedural terrain generator pipeline that can produce richer synthetic landscapes without mixing algorithm state into `HeightGrid`.
|
||||
|
||||
## Decision summary
|
||||
|
||||
### First generator family: seeded 2D value-noise fBm
|
||||
|
||||
Implement a deterministic, seedable fractal terrain family first, built from 2D value noise with fBm-style octaves.
|
||||
|
||||
Why this first:
|
||||
|
||||
- It is fully clean-room and easy to describe/test.
|
||||
- It produces organic terrain that is more representative than the current plane/hill fixtures.
|
||||
- It works well with tiny synthetic grids, which keeps tests fast and stable.
|
||||
- It can grow into ridged / warped / terraced variants later without changing the external boundary.
|
||||
|
||||
Initial scope should stay modest: generate a single height grid from a seed, dimensions, and a small set of parameters. Do not add erosion, climate, or streaming at first.
|
||||
|
||||
## API boundary
|
||||
|
||||
Keep `HeightGrid` as pure data plus basic helpers.
|
||||
|
||||
Recommended split:
|
||||
|
||||
- `src/terrain.rs`: immutable grid storage, validation, indexing, min/max, and tiny deterministic fixtures like `plane` and `radial_hill`.
|
||||
- New generator module, e.g. `src/terrain/generation.rs` or `src/generation.rs`: procedural generation logic, seed handling, interpolation/noise helpers, and generator presets.
|
||||
- Public API should return `HeightGrid` and nothing renderer-specific.
|
||||
- Any generator metadata should live in a separate spec/config type, not in the grid itself.
|
||||
|
||||
A good minimal boundary is:
|
||||
|
||||
- `TerrainGenerationSpec` or similar: dimensions, seed, octave count, lacunarity, gain, base frequency, amplitude.
|
||||
- `generate(spec) -> Result<HeightGrid, TerrainError>`
|
||||
|
||||
If the implementation later needs richer provenance, add a lightweight wrapper beside the spec, but keep the renderer and scene code consuming `HeightGrid` only.
|
||||
|
||||
## Roadmap slices
|
||||
|
||||
### Slice 1: generator module skeleton
|
||||
|
||||
Goal: introduce the procedural terrain namespace without changing renderer behavior.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- module scaffolding for generator code
|
||||
- a small spec type with seed and size fields
|
||||
- a deterministic construction path that still returns a `HeightGrid`
|
||||
|
||||
Acceptance:
|
||||
|
||||
- `HeightGrid` stays unchanged as a storage type
|
||||
- generator tests compile without touching render code
|
||||
|
||||
### Slice 2: deterministic value-noise core
|
||||
|
||||
Goal: implement the smallest reusable noise primitive.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- seeded lattice/value-noise helper
|
||||
- interpolation across grid cells
|
||||
- deterministic output for identical inputs
|
||||
|
||||
Acceptance:
|
||||
|
||||
- same seed + same spec always yields identical samples
|
||||
- different seeds produce different grids
|
||||
- tiny grids do not panic at edges
|
||||
|
||||
### Slice 3: fBm terrain composition
|
||||
|
||||
Goal: layer octaves into usable synthetic landscapes.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- octave accumulation
|
||||
- amplitude/frequency controls
|
||||
- normalization into the range expected by the renderer/palette logic
|
||||
|
||||
Acceptance:
|
||||
|
||||
- output min/max remain bounded and predictable
|
||||
- generated terrain exercises multiple elevation bands in the preview
|
||||
|
||||
### Slice 4: preset profiles
|
||||
|
||||
Goal: provide a few named generator presets for common shapes.
|
||||
|
||||
Suggested first presets:
|
||||
|
||||
- `plain`-like flat terrain via zeroed noise amplitude
|
||||
- `island` / `continental` profile with a radial falloff mask
|
||||
- `mountain` profile with stronger octave contrast
|
||||
|
||||
Acceptance:
|
||||
|
||||
- preset selection is deterministic
|
||||
- presets remain thin wrappers around the shared generator core
|
||||
|
||||
### Slice 5: later enhancements
|
||||
|
||||
Only after the core is stable:
|
||||
|
||||
- ridged multifractal terrain
|
||||
- domain warping
|
||||
- terraces / plateaus
|
||||
- simple erosion pass
|
||||
- derived slope / normal helpers if the renderer needs them
|
||||
- CLI/script integration so generated terrain can be rendered and scripted like imported terrain
|
||||
|
||||
## Test matrix
|
||||
|
||||
Use tiny synthetic fixtures only.
|
||||
|
||||
### Unit tests
|
||||
|
||||
- dimensions are preserved exactly
|
||||
- zero dimensions are rejected
|
||||
- identical seed/spec pairs produce identical grids
|
||||
- different seeds change output
|
||||
- sample access stays in-bounds and row-major expectations remain intact
|
||||
- min/max are sane after normalization
|
||||
- presets produce the expected broad shape characteristics
|
||||
|
||||
### Behavior tests
|
||||
|
||||
- a small generated grid renders successfully through the existing top-down path
|
||||
- the perspective spike accepts generated grids without special-case code
|
||||
- the generator does not mutate renderer or scene state
|
||||
|
||||
### Validation commands
|
||||
|
||||
Run these for each generator slice:
|
||||
|
||||
```bash
|
||||
cargo fmt --check
|
||||
cargo test terrain
|
||||
cargo test
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
```
|
||||
|
||||
Add one feature-specific smoke command for the slice once the generator has a public entry point, for example a tiny render or sample-generation command that writes to `/tmp` and proves the generated grid is usable end-to-end.
|
||||
|
||||
## Definition of done for the workstream
|
||||
|
||||
The terrain-generation workstream is ready to close when:
|
||||
|
||||
- the generator module is separate from `HeightGrid`
|
||||
- at least one seeded procedural family exists
|
||||
- tests prove determinism and shape invariants
|
||||
- generated terrain can flow into the existing render pipeline without special handling
|
||||
- future slices can add more generator families without changing the grid storage contract
|
||||
@@ -1,11 +1,13 @@
|
||||
# GeoTIFF Import Strategy Research Note
|
||||
|
||||
> **Status:** Spike / decision note. Documentation-only — no code or `Cargo.toml`
|
||||
> changes accompany this note.
|
||||
> **Status:** Decision implemented. The recommended pure-Rust path has landed
|
||||
> as the optional `import-geotiff` feature; the crate survey below is kept for
|
||||
> provenance.
|
||||
>
|
||||
> **Context:** Phase 4 Milestone C, Task C3 ("Add GeoTIFF spike behind an
|
||||
> optional feature"). This note records the crate survey and the recommended
|
||||
> direction so the implementation slice can start from a settled decision.
|
||||
> **Context:** Phase 4 Milestone C, Task C3 ("GeoTIFF importer behind an
|
||||
> optional feature"). This note recorded the crate survey and recommended
|
||||
> direction; the "Implementation notes" section below describes what was
|
||||
> actually built.
|
||||
|
||||
## Recommendation
|
||||
|
||||
@@ -136,44 +138,48 @@ Consistent with Task C1's existing feature scaffold (`import-dem`,
|
||||
- The committed fixture should be only as large as the test needs (target:
|
||||
well under a kilobyte).
|
||||
|
||||
## Follow-up implementation slice
|
||||
## Implementation notes
|
||||
|
||||
Concrete next slice for Task C3, to be done TDD (RED → minimal GREEN →
|
||||
validation) on a separate implementation branch:
|
||||
The recommended pure-Rust path has landed. What was built:
|
||||
|
||||
1. **Add the feature + dependency.** In `Cargo.toml`, make `geotiff-reader` an
|
||||
optional dependency gated by `import-geotiff` (default `cog` feature
|
||||
disabled). Confirm `cargo test --no-default-features` and the default build
|
||||
stay GeoTIFF-free.
|
||||
2. **Create `src/import/geotiff.rs`.** A `cfg(feature = "import-geotiff")`
|
||||
module exposing something like
|
||||
`parse_geotiff_bytes(&[u8]) -> Result<ImportedTerrain, ImportError>`,
|
||||
reusing the `ImportedTerrain` / `ImportError` / `TerrainSourceMetadata`
|
||||
types from Milestone A.
|
||||
3. **Generate the synthetic fixture.** Add a tiny generated single-band
|
||||
elevation GeoTIFF as a project-owned fixture (separate commit from the
|
||||
parser, per the plan's commit strategy).
|
||||
4. **RED test.** `cargo test geotiff --features import-geotiff` — assert
|
||||
dimensions, sample values, and metadata; assert malformed input yields a
|
||||
typed error.
|
||||
5. **Minimal GREEN.** Parse only the tiny supported subset (single band,
|
||||
uncompressed, known sample type). Reject everything else explicitly;
|
||||
document that broad real-world GeoTIFF coverage is future work.
|
||||
6. **CLI plumbing (optional, can be a later slice).** Mirror the HGT pattern
|
||||
(Task B3): an `--import-geotiff <path>` render input, gated so it only
|
||||
appears when the feature is built.
|
||||
7. **Validation.** `cargo test --no-default-features`,
|
||||
`cargo test --features import-geotiff`, `cargo test --all-features`,
|
||||
`cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`,
|
||||
`git diff --check`.
|
||||
8. **Update `openvistapro info`** to report GeoTIFF support honestly — only
|
||||
once a real parser and fixture exist (consistent with Task A3).
|
||||
1. **Feature + dependency.** `Cargo.toml` declares
|
||||
`import-geotiff = ["dep:geotiff-reader"]`; `geotiff-reader` is an optional
|
||||
dependency built with its `local` feature and `default-features = false`, so
|
||||
the network-oriented `cog` feature stays off. Default and
|
||||
`--no-default-features` builds remain GeoTIFF-free.
|
||||
2. **`src/import/geotiff.rs`.** A `cfg(feature = "import-geotiff")` module
|
||||
exposing `parse_geotiff_bytes(&[u8]) -> Result<ImportedTerrain, ImportError>`,
|
||||
reusing the `ImportedTerrain` / `ImportError` / `TerrainSourceMetadata` types
|
||||
from Milestone A. It reads the payload in memory, rejects rasters that are
|
||||
not single-band, decodes the raster as `f32` into a row-major `HeightGrid`,
|
||||
and records `TerrainSourceMetadata` with format `"geotiff"`. Reader and
|
||||
decode failures map to `ImportError::MalformedSource`.
|
||||
3. **Synthetic fixtures only.** No GeoTIFF binary is committed. Tests generate a
|
||||
tiny single-band elevation tile in memory with the `geotiff-writer`
|
||||
dev-dependency (alongside `ndarray`) and parse it back — mirroring the
|
||||
inline-bytes approach used for HGT. Real USGS/NASA tiles may be used for
|
||||
local manual verification only, under the already-ignored `reference/` and
|
||||
`.work/` directories.
|
||||
4. **Tests.** `cargo test geotiff --features import-geotiff` asserts dimensions,
|
||||
sample values, and metadata, and checks that non-GeoTIFF input yields a typed
|
||||
`ImportError::MalformedSource`.
|
||||
5. **Supported subset.** Only the narrow single-band `f32` case is parsed;
|
||||
broader real-world GeoTIFF coverage (multi-band, exotic compression, full
|
||||
geo-tag interpretation) remains future work.
|
||||
6. **`openvistapro info`.** `supported_importers()` and `info_text()` list
|
||||
`geotiff` only when the feature is built, keeping `info` honest.
|
||||
7. **GDAL is not part of the landed feature.** The importer has zero native
|
||||
dependencies; a GDAL-backed path stays out of scope until a separate review
|
||||
approves the native-dependency cost, and would use its own opt-in feature
|
||||
name rather than `import-geotiff`.
|
||||
|
||||
If `geotiff-reader` proves insufficient during the spike (missing geo-tag
|
||||
coverage, API gaps), fall back in this order: `georaster` → `wbgeotiff` →
|
||||
hand-rolled geo-tag reading on top of `image`'s TIFF decoder. A GDAL-backed
|
||||
path stays out of scope until a separate review approves the native-dependency
|
||||
cost.
|
||||
**Not yet done:** CLI render plumbing. A `render --import-geotiff <path>` input
|
||||
mirroring the HGT pattern (Task B3) is still future work; the parser is reachable
|
||||
through the library API and reported by `info`, but not yet wired into `render`.
|
||||
|
||||
If `geotiff-reader` later proves insufficient (missing geo-tag coverage, API
|
||||
gaps), the documented fallback order remains `georaster` → `wbgeotiff` →
|
||||
hand-rolled geo-tag reading on top of `image`'s TIFF decoder.
|
||||
|
||||
## Sources
|
||||
|
||||
|
||||
+66
-5
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user