SXXXXXXX_GeoElevation/geoelevation/__main__.py
2025-05-13 15:11:07 +02:00

212 lines
11 KiB
Python

# 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