370 lines
20 KiB
Python
370 lines
20 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], # 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 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)
|
|
|
|
# MODIFIED: Pass initial view parameters to the GeoElevationMapViewer constructor.
|
|
# WHY: The class now loads the initial view internally based on these parameters.
|
|
# HOW: Added initial_operation_mode, initial_point_coords, initial_area_bbox arguments.
|
|
# The constructor will handle which parameters are relevant based on initial_operation_mode.
|
|
child_map_viewer_instance = ChildGeoMapViewer(
|
|
elevation_manager_instance=local_em,
|
|
gui_output_communication_queue=map_interaction_q,
|
|
initial_display_scale=display_scale_factor,
|
|
initial_operation_mode=operation_mode,
|
|
initial_point_coords=(center_latitude, center_longitude) if operation_mode == "point" else None,
|
|
initial_area_bbox=area_bounding_box if operation_mode == "area" else None
|
|
)
|
|
child_logger.info("Child GeoElevationMapViewer instance initialized.")
|
|
|
|
|
|
# MODIFIED: REMOVE the old block that called display_map_for_point/area.
|
|
# WHY: The initial map view loading is now handled by the GeoElevationMapViewer constructor.
|
|
# HOW: Deleted the following if/elif/else block:
|
|
# 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.
|
|
# 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
|
|
child_logger.debug(f"Map window received key press: {key}")
|
|
# Convert key code to character if it's printable for logging
|
|
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.") |