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
11 changed files with 788 additions and 126 deletions
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"
+6
View File
@@ -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.
+2 -2
View File
@@ -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
+34 -55
View File
@@ -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 VistaPros 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.
+40 -23
View File
@@ -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
+167
View File
@@ -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
+46 -40
View File
@@ -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
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"
);
}
}