#!/usr/bin/env python3 """CLI entrypoint to export GeoData assets for Unity.""" from __future__ import annotations import argparse import os import sys from typing import Iterable try: import tomllib except ImportError: # pragma: no cover - tomllib is required tomllib = None from geodata_download import run_download from geodata_pipeline.config import Config, DEFAULT_CONFIG_PATH, ensure_default_config from geodata_pipeline.buildings import export_buildings from geodata_pipeline.buildings_enhanced import export_buildings_enhanced from geodata_pipeline.heightmaps import export_heightmaps from geodata_pipeline.heightmaps_enhanced import export_heightmaps_enhanced from geodata_pipeline.lpolpg_split import split_lpolpg from geodata_pipeline.orthophotos import export_orthophotos from geodata_pipeline.river_erosion import erode_rivers from geodata_pipeline.setup_helpers import ensure_directories, materialize_archives from geodata_pipeline.street_furniture import export_street_furniture from geodata_pipeline.trees import export_trees from geodata_pipeline.trees_enhanced import export_trees_enhanced def parse_args(argv: Iterable[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Export GeoData assets for Unity.") parser.add_argument( "--config", default=DEFAULT_CONFIG_PATH, help="Path to config TOML (created on --setup if missing).", ) parser.add_argument( "--export", choices=[ "heightmap", "textures", "buildings", "trees", "all", "heightmap-enhanced", "buildings-enhanced", "trees-enhanced", "street-furniture", "all-enhanced" ], default=None, help="Which assets to export. Enhanced options use point cloud data.", ) parser.add_argument("--raw-dgm1-path", dest="raw_dgm1_path", help="Override raw DGM1 directory.") parser.add_argument("--raw-dop20-path", dest="raw_dop20_path", help="Override raw DOP20 JP2 directory.") parser.add_argument( "--build-from-archive", action="store_true", help="Populate raw inputs from archives (unzips zips, leaves dop20 filelist in archive).", ) parser.add_argument( "--setup", action="store_true", help="Create default directory structure and config if missing; does not export.", ) parser.add_argument( "--force-vrt", action="store_true", help="Rebuild VRTs even if present (useful after moving raw data).", ) parser.add_argument( "--download", action="store_true", help="Download raw datasets before export using geodata_download.toml.", ) parser.add_argument( "--download-config", default="geodata_download.toml", help="Path to download config TOML.", ) parser.add_argument( "--download-datasets", help="Comma-separated dataset keys to download (default: enabled datasets).", ) parser.add_argument( "--download-start", nargs=2, type=int, metavar=("X", "Y"), help="Override tile range start for download (x y).", ) parser.add_argument( "--download-end", nargs=2, type=int, metavar=("X", "Y"), help="Override tile range end for download (x y).", ) parser.add_argument( "--clean-downloads", action="store_true", help="Delete selected dataset folders before downloading.", ) parser.add_argument( "--download-ca-bundle", help="Override CA bundle path for downloads.", ) parser.add_argument( "--split-lpolpg", action="store_true", help="Split combined lpolpg LAZ into LPG/LPO outputs.", ) parser.add_argument( "--split-lpolpg-formats", default="laz,xyz", help="Comma-separated output formats for lpolpg split (laz,xyz).", ) parser.add_argument( "--split-lpolpg-ground-classes", default="2", help="Comma-separated LAS classification codes treated as ground.", ) parser.add_argument( "--split-lpolpg-overwrite", action="store_true", help="Overwrite existing LPG/LPO outputs when splitting lpolpg.", ) parser.add_argument( "--split-lpolpg-delete-source", action="store_true", help="Delete lpolpg source files after a successful split.", ) parser.add_argument( "--erode-rivers", action="store_true", help="Erode heightmap tiles using HydroRIVERS after heightmap export (requires tile_index.csv).", ) return parser.parse_args(argv) def load_config(args: argparse.Namespace) -> Config: if args.setup: cfg = Config.default() cfg.save(args.config) return cfg.with_overrides(raw_dgm1_path=args.raw_dgm1_path, raw_dop20_path=args.raw_dop20_path) if os.path.exists(args.config): cfg = Config.load(args.config) else: cfg = Config.default() cfg.save(args.config) return cfg.with_overrides(raw_dgm1_path=args.raw_dgm1_path, raw_dop20_path=args.raw_dop20_path) def _download_requests_lpolpg(download_config: str, requested: list[str] | None) -> bool: lpolpg_keys = {"lpolpg", "lpg", "lpo"} if requested: return any(name in lpolpg_keys for name in requested) if tomllib is None: return False try: with open(download_config, "rb") as fh: cfg = tomllib.load(fh) except OSError: return False datasets = cfg.get("datasets", {}) if not isinstance(datasets, dict): return False for key in lpolpg_keys: dataset_cfg = datasets.get(key) if isinstance(dataset_cfg, dict) and dataset_cfg.get("enabled", True): return True return False def main(argv: Iterable[str] | None = None) -> int: args = parse_args(argv) cfg = load_config(args) target_export = None action_flags = args.download or args.split_lpolpg or args.erode_rivers if args.export is not None: target_export = args.export elif not action_flags: target_export = "all" if args.setup: ensure_directories(cfg) print(f"Directories ensured. Config at {args.config}.") if args.build_from_archive: materialize_archives(cfg) if args.export is None and not args.download: return 0 if args.build_from_archive and not args.setup: materialize_archives(cfg) if args.download: datasets = ( [name.strip() for name in args.download_datasets.split(",")] if args.download_datasets else None ) start = tuple(args.download_start) if args.download_start else None end = tuple(args.download_end) if args.download_end else None if (start is None) != (end is None): raise SystemExit("--download-start and --download-end must be provided together.") download_exit = run_download( config_path=args.download_config, requested_datasets=datasets, start_override=start, end_override=end, clean_downloads=args.clean_downloads, ca_bundle_override=args.download_ca_bundle, ) if download_exit != 0: return download_exit if not args.split_lpolpg and _download_requests_lpolpg(args.download_config, datasets): print("[download] lpolpg detected; splitting into lpg/lpo.") args.split_lpolpg = True if args.split_lpolpg: formats = [fmt.strip() for fmt in args.split_lpolpg_formats.split(",") if fmt.strip()] ground = [int(val) for val in args.split_lpolpg_ground_classes.split(",") if val.strip()] split_exit = split_lpolpg( cfg, formats=formats, ground_classes=ground, overwrite=args.split_lpolpg_overwrite, delete_source=args.split_lpolpg_delete_source, ) if split_exit != 0: return split_exit exit_codes = [] if target_export is not None: # Standard exports if target_export in ("heightmap", "all"): exit_codes.append(export_heightmaps(cfg, force_vrt=args.force_vrt)) if target_export in ("textures", "all"): exit_codes.append(export_orthophotos(cfg, force_vrt=args.force_vrt)) if target_export in ("buildings", "all"): exit_codes.append(export_buildings(cfg)) if target_export in ("trees", "all"): exit_codes.append(export_trees(cfg, force_vrt=args.force_vrt)) # Enhanced exports (use point cloud data) # Order matters: heightmap-enhanced creates tile_index.csv needed by others # street-furniture must run before trees-enhanced (for exclusion mask) if target_export in ("heightmap-enhanced", "all-enhanced"): exit_codes.append(export_heightmaps_enhanced(cfg)) if target_export in ("all-enhanced",): exit_codes.append(export_orthophotos(cfg, force_vrt=args.force_vrt)) if target_export in ("street-furniture", "all-enhanced"): exit_codes.append(export_street_furniture(cfg)) if target_export in ("buildings-enhanced", "all-enhanced"): exit_codes.append(export_buildings_enhanced(cfg)) if target_export in ("trees-enhanced", "all-enhanced"): exit_codes.append(export_trees_enhanced(cfg)) if args.erode_rivers: exit_codes.append(erode_rivers(cfg)) return max(exit_codes) if exit_codes else 0 if __name__ == "__main__": sys.exit(main())