# 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], # Type hint corrected 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: use the external map_manager package for map rendering and engine from map_manager.engine import MapEngine as ChildMapEngine from map_manager.visualizer import MapVisualizer as ChildMapVisualizer from geoelevation.elevation_manager import ElevationManager as ChildElevationManager import cv2 as child_cv2 # Optional, used by MapVisualizer if available import numpy as child_np 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) 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 # --- 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 the map engine/visualizer from external package local_em = ChildElevationManager(tile_directory=dem_data_cache_dir) # Initialize map engine and visualizer engine = ChildMapEngine(service_name='osm', cache_dir=dem_data_cache_dir, enable_online=True) visual = ChildMapVisualizer(engine) child_logger.info("Child MapEngine and MapVisualizer instances initialized.") # Show map depending on requested operation mode if operation_mode == 'point' and center_latitude is not None and center_longitude is not None: child_logger.info(f"Showing point {center_latitude},{center_longitude} via MapVisualizer.show_point") try: visual.show_point(center_latitude, center_longitude) except Exception as e_vis: child_logger.error(f"Error while showing point via MapVisualizer: {e_vis}") elif operation_mode == 'area' and area_bounding_box: child_logger.info(f"Creating area image for bbox {area_bounding_box} via MapEngine.get_image_for_area") try: img = engine.get_image_for_area(area_bounding_box, zoom=None, max_size=800) if img: visual.show_pil_image(img) else: child_logger.error("MapEngine returned no image for area request") except Exception as e_area: child_logger.error(f"Error while creating/showing area image: {e_area}") else: child_logger.error(f"Invalid operation mode ('{operation_mode}') or missing parameters passed to map process target.") 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 before exit: {e_put_err}") return # 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.") # 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 before exit: {e_put_err}") # return child_logger.info("Initial map display call complete. Entering OpenCV event loop.") # Main loop to keep the OpenCV window alive and process events. # Use OpenCV window visibility checks rather than relying on an application-specific # `map_display_window_controller` attribute which may not exist with the external visualizer. is_map_active = True window_name = 'Map' if 'child_cv2' in locals() and child_cv2 is not None else None while is_map_active: # Pass a short delay to yield CPU time and let OpenCV process window events key = child_cv2.waitKey(100) if window_name else -1 try: # If there's an OpenCV window, check its visibility property. If the window is closed # by the user, getWindowProperty returns a value < 1. Use that as the signal to exit. if window_name: try: vis = child_cv2.getWindowProperty(window_name, child_cv2.WND_PROP_VISIBLE) if vis < 1: child_logger.info("Map window detected as closed (OpenCV).") break except Exception: # If getWindowProperty is unsupported for some reason, fall back to key checks. pass # Check for specific key presses (like 'q' or Escape) to allow closing via keyboard if key != -1: child_logger.debug(f"Map window received key press: {key}") try: if chr(key & 0xFF) in ('q', 'Q'): child_logger.info("Map window closing due to 'q'/'Q' key press.") break if key == 27: child_logger.info("Map window closing due to Escape key press.") break except Exception: pass except Exception as e_loop_check: child_logger.debug(f"Error during OpenCV loop checks: {e_loop_check}") break 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.")