SXXXXXXX_GeoElevation/geoelevation/process_targets.py
2025-05-14 09:31:53 +02:00

362 lines
19 KiB
Python

# geoelevation/process_targets.py
"""
Contains multiprocessing target functions executed in separate processes.
These functions perform visualization tasks (2D/3D plots, map viewer).
"""
# Standard library imports needed at the top level for process entry points
import multiprocessing
import sys
import logging
from typing import Optional, Tuple, List, Dict, Any, TYPE_CHECKING # Needed for type hinting
# Local application/package imports that the target functions will use.
# Imports needed *within* the target functions should be handled carefully
# (e.g., within try blocks or using local imports) as the child process
# might have a different import context or dependencies.
# Configure logging for the process target module itself.
# Note: The run_map_viewer_process_target explicitly configures logging,
# but other process targets might not, so a basic config here is safer.
# Using NullHandler by default, similar to how libraries are configured,
# letting the *actual process entry point* decide on the final handlers.
logger = logging.getLogger(__name__)
if not logger.hasHandlers():
logger.addHandler(logging.NullHandler())
# The run_map_viewer_process_target function will configure a StreamHandler.
# Define the multiprocessing target functions
def process_target_show_image(image_path: str, tile_name: str, window_title: str, extent: Optional[List[float]] = None) -> None:
"""
Multiprocessing target function to load and display a 2D image.
Expected to be run in a separate process.
"""
try:
# Local imports needed by this target function
from geoelevation.image_processor import load_prepare_single_browse as local_lpst
from geoelevation.image_processor import PIL_AVAILABLE as local_pil_ok
from geoelevation.visualizer import show_image_matplotlib as local_sim
from geoelevation.visualizer import MATPLOTLIB_AVAILABLE as local_mpl_ok
import os as local_os
# Basic check for critical libraries within the child process
if not (local_pil_ok and local_mpl_ok):
# Use print for crucial messages in process targets, as logging might not be fully set up yet.
print("PROCESS ERROR (show_image): Pillow/Matplotlib missing in child process. Cannot display image.")
return
# Load and prepare the image (adds overlay)
prepared_image = local_lpst(image_path, tile_name)
if prepared_image:
# Display the image using Matplotlib
print(f"PROCESS (show_image): Showing '{window_title}' (Extent: {'Yes' if extent is not None else 'No'})...")
local_sim(prepared_image, window_title, extent=extent) # Pass the extent
# Explicitly close matplotlib figures to free memory in the child process
# Not strictly necessary as process exits, but good practice.
try:
import matplotlib.pyplot as local_plt # Import pyplot locally for closing
local_plt.close('all')
# print("PROCESS (show_image): Closed Matplotlib figures.")
except Exception:
pass # Ignore errors if matplotlib closing fails
else:
print(f"PROCESS ERROR (show_image): Could not prepare image from {local_os.path.basename(image_path)}")
except Exception as e_proc_img:
# Log/print unexpected errors with traceback
print(f"PROCESS ERROR in process_target_show_image: {e_proc_img}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
# The process will exit when this function returns.
def process_target_show_3d(
hgt_data: Optional["np_typing.ndarray"],
plot_title: str,
initial_subsample: int,
smooth_sigma: Optional[float],
interpolation_factor: int,
plot_grid_points: int
) -> None:
"""
Multiprocessing target function to display a 3D elevation plot.
Expected to be run in a separate process.
"""
try:
# Local imports needed by this target function
from geoelevation.visualizer import show_3d_matplotlib as local_s3d
from geoelevation.visualizer import MATPLOTLIB_AVAILABLE as local_mpl_ok
from geoelevation.visualizer import SCIPY_AVAILABLE as local_scipy_ok
import numpy as local_np # Import numpy locally if not already available
# Basic check for critical libraries
if not local_mpl_ok:
print("PROCESS ERROR (show_3d): Matplotlib missing in child process. Cannot display 3D plot.")
return
effective_smooth_sigma = smooth_sigma
effective_interpolation_factor = interpolation_factor
# Check SciPy availability for advanced features
if (interpolation_factor > 1 or smooth_sigma is not None) and not local_scipy_ok:
print("PROCESS WARNING (show_3d): SciPy missing in child. Disabling 3D plot smoothing/interpolation features.")
effective_smooth_sigma = None
effective_interpolation_factor = 1
if hgt_data is not None and isinstance(hgt_data, local_np.ndarray): # Ensure data is a numpy array
# Perform the 3D visualization
print(f"PROCESS (show_3d): Plotting '{plot_title}' (InitialSub:{initial_subsample}, Smooth:{effective_smooth_sigma}, Interp:{effective_interpolation_factor}x, PlotGridTarget:{plot_grid_points})...")
local_s3d(
hgt_data,
plot_title,
initial_subsample,
effective_smooth_sigma,
effective_interpolation_factor,
plot_grid_points
)
# Explicitly close matplotlib figures
try:
import matplotlib.pyplot as local_plt # Import pyplot locally for closing
local_plt.close('all')
# print("PROCESS (show_3d): Closed Matplotlib figures.")
except Exception:
pass # Ignore errors if matplotlib closing fails
else:
print("PROCESS ERROR (show_3d): No valid HGT data array received for plotting.")
except Exception as e_proc_3d:
# Log/print unexpected errors with traceback
print(f"PROCESS ERROR in process_target_show_3d: {e_proc_3d}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
# The process will exit when this function returns.
def process_target_create_show_area(tile_info_list_data: List[Dict], window_title_str: str, extent: Optional[List[float]] = None) -> None:
"""
Multiprocessing target function to create a composite image for an area
and display it. Expected to be run in a separate process.
"""
try:
# Local imports needed by this target function
from geoelevation.image_processor import create_composite_area_image as local_ccai
from geoelevation.image_processor import PIL_AVAILABLE as local_pil_ok
from geoelevation.visualizer import show_image_matplotlib as local_sim
from geoelevation.visualizer import MATPLOTLIB_AVAILABLE as local_mpl_ok
# Basic check for critical libraries
if not (local_pil_ok and local_mpl_ok):
print("PROCESS ERROR (show_area): Pillow/Matplotlib libraries are missing in the child process. Cannot create/display composite.")
return
print("PROCESS (show_area): Creating composite area image...")
composite_pil_image = local_ccai(tile_info_list_data)
if composite_pil_image:
# Display the composite image
print(f"PROCESS (show_area): Displaying composite image '{window_title_str}' (Extent: {'Yes' if extent is not None else 'No'})...")
local_sim(composite_pil_image, window_title_str, extent=extent) # Pass the extent
# Explicitly close matplotlib figures
try:
import matplotlib.pyplot as local_plt # Import pyplot locally for closing
local_plt.close('all')
# print("PROCESS (show_area): Closed Matplotlib figures.")
except Exception:
pass # Ignore errors if matplotlib closing fails
else:
print("PROCESS ERROR (show_area): Failed to create composite area image.")
except Exception as e_proc_area:
# Log/print unexpected errors with traceback
print(f"PROCESS ERROR in process_target_create_show_area: {e_proc_area}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
# The process will exit when this function returns.
def run_map_viewer_process_target(
map_interaction_q: multiprocessing.Queue,
operation_mode: str,
center_latitude: Optional[float],
center_longitude: Optional[float], # Corrected type hint, Optional[Optional[float]] is redundant
area_bounding_box: Optional[Tuple[float, float, float, float]],
dem_data_cache_dir: str,
display_scale_factor: float
) -> None:
"""
Multiprocessing target function to run the interactive map viewer.
Expected to be run in a separate process.
"""
# --- Initial Process Setup and Logging ---
# Configure logging for the child process explicitly.
# This ensures its logs are visible and don't interfere with a parent's logging setup.
child_logger = logging.getLogger("GeoElevationMapViewerChildProcess")
# Check if handlers already exist (e.g., from basicConfig in __main__.py).
# If not, set up basic stream handling.
if not child_logger.hasHandlers():
# Use basicConfig if no handlers exist in this process's logger or its ancestors.
# Using basicConfig might affect the root logger if no other configuration exists globally in this process.
# A safer approach if deeper control is needed might be to manually add handlers.
# For a dedicated process target, basicConfig is usually fine.
logging.basicConfig(
level=logging.INFO, # Default level for this process, can be adjusted
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
stream=sys.stdout # Send output to stdout, which is captured by parent process's console.
)
# Ensure the 'geoelevation' package logger in this process has a handler
# if its level is below the root default.
geoelevation_proc_logger = logging.getLogger("geoelevation")
if not geoelevation_proc_logger.hasHandlers():
geoelevation_proc_logger.addHandler(logging.StreamHandler(sys.stdout)) # Add a stream handler
child_logger.info("Map viewer process started.")
# --- Import Critical Libraries for Map Viewer Process ---
child_map_viewer_instance: Optional[Any] = None
critical_libs_available = False
try:
# Local imports needed by this target function
from geoelevation.map_viewer.geo_map_viewer import GeoElevationMapViewer as ChildGeoMapViewer
from geoelevation.elevation_manager import ElevationManager as ChildElevationManager
import cv2 as child_cv2 # Ensure cv2 is available in the child process
# Also check other libraries that geo_map_viewer depends on and checks internally
# If GeoElevationMapViewer import succeeds, it means PIL and Mercantile were likely available,
# as GeoElevationMapViewer's __init__ raises ImportError if they're missing.
# We still need cv2 and numpy for the loop itself.
import numpy as child_np # Ensure numpy is available
critical_libs_available = True
except ImportError as e_child_imp_map:
child_logger.critical(f"CRITICAL: Map viewer components or essential libraries not found in child process: {e_child_imp_map}", exc_info=True)
# Send an error message back to the GUI queue if critical imports fail in child process.
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
"latitude_dms_str": "Error", "longitude_dms_str": "Error",
"elevation_str": f"Fatal Error: {type(e_child_imp_map).__name__} (Import)",
"map_area_size_str": "Map System N/A"}
try: map_interaction_q.put(error_payload)
except Exception as e_put_err: child_logger.error(f"Failed to put error payload to queue before exit: {e_put_err}")
return # Exit if critical libraries are missing
# --- Initialize Map Viewer and Run Main Loop ---
try:
child_logger.info(f"Initializing GeoElevationMapViewer instance for mode '{operation_mode}', display scale: {display_scale_factor:.2f}...")
# Initialize ElevationManager and GeoElevationMapViewer within the child process
# Each process needs its own instance, but they share the filesystem cache.
local_em = ChildElevationManager(tile_directory=dem_data_cache_dir)
child_map_viewer_instance = ChildGeoMapViewer(
elevation_manager_instance=local_em,
gui_output_communication_queue=map_interaction_q,
initial_display_scale=display_scale_factor
)
child_logger.info("Child GeoElevationMapViewer instance initialized.")
# Call the appropriate display method based on the requested operation mode
if operation_mode == "point" and center_latitude is not None and center_longitude is not None:
child_logger.info(f"Calling display_map_for_point for ({center_latitude:.5f},{center_longitude:.5f}).")
child_map_viewer_instance.display_map_for_point(center_latitude, center_longitude)
elif operation_mode == "area" and area_bounding_box:
child_logger.info(f"Calling display_map_for_area for BBox {area_bounding_box}.")
child_map_viewer_instance.display_map_for_area(area_bounding_box)
else:
child_logger.error(f"Invalid operation mode ('{operation_mode}') or missing parameters passed to map process target.")
# Send error message to GUI
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
"latitude_dms_str": "Error", "longitude_dms_str": "Error",
"elevation_str": f"Fatal Error: Invalid Map Args",
"map_area_size_str": "Invalid Args"}
try: map_interaction_q.put(error_payload)
except Exception as e_put_err: child_logger.error(f"Failed to put error payload to queue: {e_put_err}")
# No need to call shutdown here, as instance might not be fully initialized.
return # Exit on invalid parameters
child_logger.info("Initial map display call complete. Entering OpenCV event loop.")
# Main loop to keep the OpenCV window alive and process events.
# cv2.waitKey() is essential for processing window events and mouse callbacks.
# A non-zero argument (e.g., 100ms) makes it wait for a key press for that duration,
# but more importantly, it allows OpenCV to process the window's message queue.
# Without regular calls to waitKey, window updates and callbacks won't happen.
is_map_active = True
while is_map_active:
# Pass a short delay to yield CPU time
key = child_cv2.waitKey(100) # Process events every 100ms
# Check if the map window is still alive.
# The MapDisplayWindow instance holds the OpenCV window name and can check its property.
if child_map_viewer_instance.map_display_window_controller:
if child_map_viewer_instance.map_display_window_controller.is_window_alive():
# Check for specific key presses (like 'q' or Escape) to allow closing the window via keyboard
if key != -1: # A key was pressed
# Convert key code if needed and check for close commands
try:
# Check for 'q' or 'Q' keys (example)
if chr(key & 0xFF) in ('q', 'Q'):
child_logger.info("Map window closing due to 'q'/'Q' key press.")
is_map_active = False # Signal loop to exit
# Check for Escape key (common window close shortcut)
if key == 27:
child_logger.info("Map window closing due to Escape key press.")
is_map_active = False # Signal loop to exit
except ValueError: # Ignore non-printable keys
pass # Do nothing for non-printable keys
else:
# is_window_alive() returned False, meaning the window was closed (e.g., by user clicking the 'X').
child_logger.info("Map window detected as closed.")
is_map_active = False # Signal loop to exit
else:
# The controller instance is None, indicating an issue or shutdown signal.
child_logger.info("MapDisplayWindow controller is None. Exiting map process loop.")
is_map_active = False # Signal loop to exit
child_logger.info("OpenCV event loop finished. Map viewer process target returning.")
except Exception as e_map_proc_fatal:
# Catch any unhandled exceptions during the map viewer's operation loop.
child_logger.critical(f"FATAL: Unhandled exception in map viewer process: {e_map_proc_fatal}", exc_info=True)
# Send a fatal error message back to the GUI.
# MODIFIED: Include DMS fields with error state for consistency with GUI update logic.
error_payload = {"type": "map_info_update", "latitude": None, "longitude": None,
"latitude_dms_str": "Error", "longitude_dms_str": "Error",
"elevation_str": f"Fatal Error: {type(e_map_proc_fatal).__name__}",
"map_area_size_str": "Fatal Error"}
try: map_interaction_q.put(error_payload)
except Exception as e_put_err: child_logger.error(f"Failed to put error payload to queue before exit: {e_put_err}")
finally:
# Ensure cleanup is performed regardless of how the function exits (success, planned loop exit, exception).
child_logger.info("Map viewer process target finally block reached. Performing cleanup.")
if child_map_viewer_instance and hasattr(child_map_viewer_instance, 'shutdown'):
child_map_viewer_instance.shutdown() # Call the instance's shutdown method
child_logger.info("Child map viewer instance shutdown complete.")
# cv2.destroyAllWindows() is often called here in simple examples, but destroying the specific window
# via the instance's shutdown method is usually better practice if multiple OpenCV windows might exist.
# Let's add a safety destroyAll just in case, although the instance shutdown should handle it.
try:
if 'child_cv2' in locals() and child_cv2 is not None:
child_cv2.destroyAllWindows()
child_logger.debug("Called cv2.destroyAllWindows() in child process.")
except Exception as e_destroy_all:
child_logger.debug(f"Error calling cv2.destroyAllWindows() in child process: {e_destroy_all}")
child_logger.info("Map viewer process target finished.")
# The process automatically terminates when this function returns.