# geoelevation/__main__.py """ Main entry point for the GeoElevation package when executed as a module (e.g., `python -m geoelevation`). This script handles command-line arguments to either: 1. Retrieve and print the elevation for a specific geographic point (CLI mode). 2. Launch the GeoElevation Graphical User Interface (GUI mode). """ # Standard library imports import argparse import sys import logging # For configuring logging at the application's entry point import os # For providing hints in case of import errors during development import math # For math.isnan in CLI output handling # --- Application Entry Point Logging Configuration --- # Configure basic logging for the entire application session when this __main__.py is run. # This setup will apply if the package is run as an application. # If 'geoelevation' is imported as a library, its __init__.py configures a NullHandler # by default, and the consuming application should set up its own logging. logging.basicConfig( level=logging.INFO, # Default level, can be overridden by the --verbose argument format="%(asctime)s - %(levelname)s - [%(name)s] - %(message)s", # Include logger name datefmt="%Y-%m-%d %H:%M:%S" ) # Create a logger specific to this main entry script for its own messages main_entry_logger = logging.getLogger("geoelevation.cli_entry") def SCRIPT_ENTRY_POINT_ROUTINE() -> None: # PEP8: function names should be lowercase_with_underscores """ Parses command-line arguments and either performs a CLI action (get point elevation) or launches the main GUI application. This is the main operational function called when the script is executed. """ cli_argument_parser = argparse.ArgumentParser( description="GeoElevation Tool: Retrieve elevation for a point via CLI or launch the GUI.", formatter_class=argparse.RawTextHelpFormatter, # Allows for newlines in epilog/help epilog=( "Usage Examples:\n" " python -m geoelevation --lat 45.0 --lon 7.0\n" " python -m geoelevation --lat 45.0 --lon 7.0 --show-progress\n" " python -m geoelevation --gui\n" " python -m geoelevation (launches GUI by default if no point coordinates are specified)" ) ) cli_argument_parser.add_argument( "--lat", type=float, metavar="LATITUDE", help="Latitude of the target point in decimal degrees (e.g., 45.0)." ) cli_argument_parser.add_argument( "--lon", type=float, metavar="LONGITUDE", help="Longitude of the target point in decimal degrees (e.g., 7.0)." ) cli_argument_parser.add_argument( "--show-progress", action="store_true", # If present, args.show_progress will be True help="Display a visual progress dialog during data retrieval (applies to CLI point elevation mode)." ) cli_argument_parser.add_argument( "--verbose", "-v", action="store_true", help="Enable verbose (DEBUG level) logging output for the entire 'geoelevation' package." ) cli_argument_parser.add_argument( "--gui", action="store_true", help="Force the launch of the GUI application, overriding other CLI arguments if present." ) # Parse the command-line arguments provided by the user parsed_command_line_args = cli_argument_parser.parse_args() # Configure logging level based on verbosity argument if parsed_command_line_args.verbose: # Set the logging level for the root logger of the 'geoelevation' package to DEBUG # This will affect all loggers within this package unless they have a more specific level set. logging.getLogger("geoelevation").setLevel(logging.DEBUG) main_entry_logger.debug("Verbose logging (DEBUG level) has been enabled for the 'geoelevation' package.") else: # Ensure the 'geoelevation' package logger is at least INFO if not verbose. # This prevents it from being too quiet if the root logger (from basicConfig) # was set to WARNING or ERROR by another part of a larger system (unlikely here). geoelevation_package_logger = logging.getLogger("geoelevation") if geoelevation_package_logger.getEffectiveLevel() > logging.INFO: geoelevation_package_logger.setLevel(logging.INFO) # --- Import Public API Functions --- # Attempt to import the primary public API functions from the geoelevation package's __init__.py. # This is the recommended way for an application entry point to access package functionality. try: from geoelevation import get_point_elevation # API for CLI from geoelevation import run_gui_application # API for launching GUI except ImportError as e_fatal_api_import: main_entry_logger.critical( f"CRITICAL FAILURE: Could not import core API functions (get_point_elevation, run_gui_application) " f"from the GeoElevation package. This indicates a fundamental problem with the package " f"installation, structure, or missing critical dependencies. Original Error: {e_fatal_api_import}", exc_info=True # Include traceback for critical errors ) # Provide a hint to the user if they might be running the command from an incorrect directory # during development (e.g., inside the package folder instead of its parent). current_dir = os.getcwd() if "geoelevation" not in current_dir.lower() and \ os.path.isdir(os.path.join(os.path.dirname(current_dir), "geoelevation")): # Heuristic main_entry_logger.info( "DEVELOPMENT HINT: If running with `python -m geoelevation ...`, " "ensure you are in the directory *above* the 'geoelevation' package folder." ) sys.exit(1) # Exit with an error code if core API cannot be loaded # --- Determine Action Based on Parsed Arguments --- if parsed_command_line_args.gui: main_entry_logger.info("Launching GeoElevation GUI (explicitly requested via --gui argument)...") run_gui_application() elif parsed_command_line_args.lat is not None and parsed_command_line_args.lon is not None: # CLI mode for getting point elevation main_entry_logger.info( f"CLI mode: Requesting elevation for Latitude: {parsed_command_line_args.lat}, " f"Longitude: {parsed_command_line_args.lon}" ) try: # Call the public API function to get the elevation elevation_result = get_point_elevation( latitude=parsed_command_line_args.lat, longitude=parsed_command_line_args.lon, show_progress_dialog_flag=parsed_command_line_args.show_progress ) # Print the result to standard output for CLI users if elevation_result is None: # This indicates an issue like data unavailability or an internal error. print("Elevation: UNAVAILABLE (Data could not be retrieved or an error occurred. Please check logs for details.)") sys.exit(1) # Exit with a non-zero code to indicate error for scripting elif isinstance(elevation_result, float) and math.isnan(elevation_result): # Point is on a NoData area within a valid DEM tile print("Elevation: NODATA (Point is located on a NoData area within the DEM tile)") sys.exit(0) # Successful query, result is NoData else: # Successful elevation retrieval print(f"Elevation: {elevation_result:.2f} meters") sys.exit(0) # Successful CLI operation except RuntimeError as e_cli_runtime_get_elev: # Catch RuntimeErrors (e.g., Rasterio missing, ElevationManager init failure) print(f"CLI ERROR: A critical runtime error occurred: {e_cli_runtime_get_elev}", file=sys.stderr) main_entry_logger.error( f"Runtime error during CLI elevation retrieval: {e_cli_runtime_get_elev}", exc_info=True ) sys.exit(1) except ValueError as e_cli_value_get_elev: # Catch ValueErrors (e.g., invalid coordinates passed to API that were not caught by argparse) print(f"CLI ERROR: Invalid input value provided - {e_cli_value_get_elev}", file=sys.stderr) main_entry_logger.error( f"ValueError during CLI elevation retrieval: {e_cli_value_get_elev}", exc_info=False # Traceback often not needed for user input errors ) sys.exit(1) except Exception as e_cli_unexpected_get_elev: # Catch any other unexpected exceptions during the API call print(f"CLI ERROR: An unexpected error occurred during elevation retrieval: {e_cli_unexpected_get_elev}", file=sys.stderr) main_entry_logger.error( f"Unexpected error during CLI elevation retrieval: {e_cli_unexpected_get_elev}", exc_info=True ) sys.exit(1) else: # Default behavior: Launch GUI if no CLI action for point elevation is fully specified # This condition also catches cases where only --lat or only --lon was given without the other. if parsed_command_line_args.lat is None and parsed_command_line_args.lon is None: # No lat/lon args, and --gui was not specified. Default to GUI. main_entry_logger.info( "No specific CLI arguments for point elevation were provided. " "Launching GeoElevation GUI by default..." ) run_gui_application() else: # Partial arguments for point elevation (e.g., only --lat without --lon, or vice-versa) cli_argument_parser.print_help(sys.stderr) # Print help message to standard error # Add a specific error message for this case print( "\nError: For Command Line Interface (CLI) point elevation retrieval, " "both --lat and --lon arguments must be specified together.", file=sys.stderr ) sys.exit(2) # Use a different exit code for incorrect CLI usage # --- Main Execution Guard --- if __name__ == "__main__": # This block is executed when the script is run as the main program, e.g.: # `python -m geoelevation` # It can also be the entry point for an executable created by PyInstaller # if `geoelevation/__main__.py` is specified. # Note: `multiprocessing.freeze_support()` is now strategically placed within # `run_gui_application` (inside `geoelevation/__init__.py`) because it's only # needed if the GUI (which might use multiprocessing for its tasks) is launched. # This keeps `__main__.py` cleaner and ensures `freeze_support` is called # at the appropriate juncture before any GUI-related processes might be spawned. main_entry_logger.debug( f"GeoElevation __main__.py executed as entry point. Python version: {sys.version_info.major}.{sys.version_info.minor}." "Proceeding to CLI/GUI launch logic." ) SCRIPT_ENTRY_POINT_ROUTINE() # Call the main operational function