467 lines
24 KiB
Python
467 lines
24 KiB
Python
# geoelevation/map_viewer/geo_map_viewer.py
|
|
"""
|
|
Orchestrates map display functionalities for the GeoElevation application.
|
|
|
|
This module initializes and manages map services, tile fetching/caching,
|
|
and the map display window. It handles requests to show maps centered on
|
|
specific points or covering defined areas, applying a specified display scale.
|
|
It also processes user interactions (mouse clicks) on the map, converting
|
|
pixel coordinates to geographic coordinates, fetching elevation for those points
|
|
using the core ElevationManager, and sending this information back to the
|
|
main GUI via a queue.
|
|
"""
|
|
|
|
# Standard library imports
|
|
import logging
|
|
import math
|
|
import queue # For type hinting, actual queue object is passed in
|
|
from typing import Optional, Tuple, Dict, Any, List # Added List
|
|
|
|
# Third-party imports
|
|
try:
|
|
from PIL import Image
|
|
ImageType = Image.Image # type: ignore
|
|
PIL_IMAGE_LIB_AVAILABLE = True
|
|
except ImportError:
|
|
Image = None # type: ignore
|
|
ImageType = None # type: ignore
|
|
PIL_IMAGE_LIB_AVAILABLE = False
|
|
logging.error("GeoMapViewer: Pillow (PIL) library not found. Image operations will fail.")
|
|
|
|
try:
|
|
import cv2 # OpenCV for drawing operations
|
|
import numpy as np
|
|
CV2_NUMPY_LIBS_AVAILABLE = True
|
|
except ImportError:
|
|
cv2 = None # type: ignore
|
|
np = None # type: ignore
|
|
CV2_NUMPY_LIBS_AVAILABLE = False
|
|
logging.error("GeoMapViewer: OpenCV or NumPy not found. Drawing and image operations will fail.")
|
|
|
|
# Local application/package imports
|
|
# Imports from other modules within the 'map_viewer' subpackage
|
|
from .map_services import BaseMapService
|
|
from .map_services import OpenStreetMapService # Default service if none specified
|
|
from .map_manager import MapTileManager
|
|
from .map_utils import get_bounding_box_from_center_size
|
|
from .map_utils import get_tile_ranges_for_bbox
|
|
from .map_utils import MapCalculationError
|
|
# from .map_utils import calculate_meters_per_pixel # If scale bar is drawn here
|
|
|
|
# Imports from the parent 'geoelevation' package
|
|
from geoelevation.elevation_manager import ElevationManager
|
|
|
|
# Module-level logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Default configuration values specific to the map viewer's operation
|
|
DEFAULT_MAP_TILE_CACHE_DIRECTORY = "map_tile_cache_ge" # Specific cache for map tiles
|
|
DEFAULT_MAP_DISPLAY_ZOOM_LEVEL = 15
|
|
DEFAULT_MAP_VIEW_AREA_SIZE_KM = 5.0 # Default size of map area around a point in km
|
|
|
|
|
|
class GeoElevationMapViewer:
|
|
"""
|
|
Manages the display of maps (e.g., OpenStreetMap) and user interaction
|
|
for the GeoElevation application. This class is intended to be run in
|
|
a separate process.
|
|
"""
|
|
def __init__(
|
|
self,
|
|
elevation_manager_instance: ElevationManager,
|
|
gui_output_communication_queue: queue.Queue, # For sending data back to GUI
|
|
initial_display_scale: float = 1.0 # Scale factor for the map image
|
|
) -> None:
|
|
"""
|
|
Initializes the GeoElevationMapViewer.
|
|
|
|
Args:
|
|
elevation_manager_instance: An instance of ElevationManager to fetch elevations.
|
|
gui_output_communication_queue: A queue to send interaction data back to the main GUI.
|
|
initial_display_scale: The initial scaling factor for displaying the map image.
|
|
Example: 0.5 means 50% of original stitched map size.
|
|
"""
|
|
logger.info("Initializing GeoElevationMapViewer...")
|
|
if not (CV2_NUMPY_LIBS_AVAILABLE and PIL_IMAGE_LIB_AVAILABLE):
|
|
# Logged during import, but critical for class functionality
|
|
raise ImportError("Pillow, OpenCV, or NumPy not available. GeoElevationMapViewer cannot function.")
|
|
|
|
self.elevation_manager: ElevationManager = elevation_manager_instance
|
|
self.gui_com_queue: queue.Queue = gui_output_communication_queue
|
|
self.current_display_scale_factor: float = initial_display_scale
|
|
|
|
self.map_service_provider: Optional[BaseMapService] = None
|
|
self.map_tile_fetch_manager: Optional[MapTileManager] = None
|
|
self.map_display_window_controller: Optional['MapDisplayWindow'] = None # Forward declaration for type hint
|
|
|
|
# State attributes for the currently displayed map context
|
|
self._current_stitched_map_pil: Optional[ImageType] = None
|
|
self._current_map_geo_bounds_deg: Optional[Tuple[float, float, float, float]] = None
|
|
self._current_map_render_zoom: Optional[int] = None
|
|
# Pixel shape of the *stitched* map before display scaling
|
|
self._current_stitched_map_pixel_shape: Optional[Tuple[int, int]] = None # Height, Width
|
|
|
|
self._last_map_click_pixel_coords_on_displayed_image: Optional[Tuple[int, int]] = None
|
|
|
|
self._initialize_map_viewer_components()
|
|
logger.info("GeoElevationMapViewer initialization complete.")
|
|
|
|
def _initialize_map_viewer_components(self) -> None:
|
|
"""Initializes internal components like map service, tile manager, and display window."""
|
|
logger.debug("Initializing internal map viewer components...")
|
|
try:
|
|
# Import MapDisplayWindow here to manage potential import complexities
|
|
# and ensure it's only imported when GeoElevationMapViewer is instantiated.
|
|
from .map_display import MapDisplayWindow
|
|
|
|
# Default to OpenStreetMap service
|
|
self.map_service_provider = OpenStreetMapService()
|
|
if not self.map_service_provider: # Should not happen if constructor is fine
|
|
raise ValueError("Failed to initialize OpenStreetMapService.")
|
|
logger.info(f"Map service provider '{self.map_service_provider.name}' initialized.")
|
|
|
|
self.map_tile_fetch_manager = MapTileManager(
|
|
map_service=self.map_service_provider, # Corrected parameter name
|
|
cache_root_directory=DEFAULT_MAP_TILE_CACHE_DIRECTORY,
|
|
enable_online_tile_fetching=True # Assume online capability
|
|
)
|
|
logger.info("MapTileManager initialized.")
|
|
|
|
# The MapDisplayWindow will use 'self' (this GeoElevationMapViewer instance)
|
|
# as its 'app_facade' to call back 'handle_map_mouse_click'.
|
|
# It will also access 'self.current_display_scale_factor' for scaling.
|
|
self.map_display_window_controller = MapDisplayWindow(
|
|
app_facade=self,
|
|
window_name="GeoElevation - Interactive Map"
|
|
)
|
|
logger.info("MapDisplayWindow controller initialized.")
|
|
|
|
except ImportError as e_imp_comp:
|
|
logger.critical(f"Failed to import a required map component: {e_imp_comp}", exc_info=True)
|
|
raise # Propagate error to signal fatal initialization failure
|
|
except Exception as e_init_comp:
|
|
logger.critical(f"Failed to initialize map components: {e_init_comp}", exc_info=True)
|
|
raise
|
|
|
|
def display_map_for_point(
|
|
self,
|
|
center_latitude: float,
|
|
center_longitude: float,
|
|
target_map_zoom: Optional[int] = None
|
|
) -> None:
|
|
"""Displays a map centered on the given geographic point, scaled by current factor."""
|
|
if not self.map_tile_fetch_manager or not self.map_display_window_controller:
|
|
logger.error("Map components not ready. Cannot display map for point.")
|
|
return
|
|
|
|
effective_zoom = target_map_zoom if target_map_zoom is not None else DEFAULT_MAP_DISPLAY_ZOOM_LEVEL
|
|
logger.info(
|
|
f"Requesting map display for point: ({center_latitude:.5f}, {center_longitude:.5f}), "
|
|
f"Zoom: {effective_zoom}, Scale: {self.current_display_scale_factor:.2f}"
|
|
)
|
|
|
|
try:
|
|
# Determine the geographic area to fetch tiles for
|
|
# Fetch a slightly larger area (e.g., by factor of 1.2) than the target display size
|
|
# to provide some context around the point.
|
|
map_area_km_for_fetch = DEFAULT_MAP_VIEW_AREA_SIZE_KM * 1.2
|
|
tile_fetch_geographic_bbox = get_bounding_box_from_center_size(
|
|
center_latitude, center_longitude, map_area_km_for_fetch
|
|
)
|
|
if not tile_fetch_geographic_bbox:
|
|
raise MapCalculationError("Bounding box calculation for point display failed.")
|
|
|
|
# Determine the range of map tiles (X, Y) needed for this bounding box and zoom
|
|
map_tile_xy_ranges = get_tile_ranges_for_bbox(tile_fetch_geographic_bbox, effective_zoom)
|
|
if not map_tile_xy_ranges:
|
|
raise MapCalculationError("Tile range calculation for point display failed.")
|
|
|
|
# Retrieve and stitch the map tiles into a single PIL image
|
|
stitched_map_pil_image = self.map_tile_fetch_manager.stitch_map_image(
|
|
effective_zoom, map_tile_xy_ranges[0], map_tile_xy_ranges[1] # x_range, y_range
|
|
)
|
|
if not stitched_map_pil_image:
|
|
logger.error("Failed to stitch map image for point display.")
|
|
self.map_display_window_controller.show_map(None) # Show placeholder
|
|
return
|
|
|
|
# Update internal state with details of the new map
|
|
self._current_stitched_map_pil = stitched_map_pil_image
|
|
# Get actual geographic bounds of the stitched map for accurate coordinate conversions
|
|
self._current_map_geo_bounds_deg = self.map_tile_fetch_manager._get_bounds_for_tile_range(
|
|
effective_zoom, map_tile_xy_ranges
|
|
)
|
|
self._current_map_render_zoom = effective_zoom
|
|
self._current_stitched_map_pixel_shape = (stitched_map_pil_image.height, stitched_map_pil_image.width)
|
|
|
|
# Draw a marker at the specified center point on the map
|
|
map_with_center_marker_pil = self._draw_point_marker_on_map(
|
|
stitched_map_pil_image.copy(), center_latitude, center_longitude # Operate on a copy
|
|
)
|
|
|
|
# Display the (potentially marked) map using MapDisplayWindow
|
|
# MapDisplayWindow will handle the scaling using self.current_display_scale_factor
|
|
self.map_display_window_controller.show_map(map_with_center_marker_pil)
|
|
self._last_user_click_pixel_coords_on_displayed_image = None # Reset last click
|
|
|
|
except MapCalculationError as e_map_calc_pt:
|
|
logger.error(f"Map calculation error during point display: {e_map_calc_pt}")
|
|
if self.map_display_window_controller: self.map_display_window_controller.show_map(None)
|
|
except Exception as e_disp_map_pt:
|
|
logger.exception(f"Unexpected error displaying map for point: {e_disp_map_pt}")
|
|
if self.map_display_window_controller: self.map_display_window_controller.show_map(None)
|
|
|
|
|
|
def display_map_for_area(
|
|
self,
|
|
area_geographic_bbox: Tuple[float, float, float, float], # west, south, east, north
|
|
target_map_zoom: Optional[int] = None
|
|
) -> None:
|
|
"""Displays a map covering the specified geographic area, scaled by current factor."""
|
|
if not self.map_tile_fetch_manager or not self.map_display_window_controller:
|
|
logger.error("Map components not ready. Cannot display map for area.")
|
|
return
|
|
|
|
effective_zoom = target_map_zoom if target_map_zoom is not None else DEFAULT_MAP_DISPLAY_ZOOM_LEVEL
|
|
logger.info(
|
|
f"Requesting map display for area: BBox {area_geographic_bbox}, "
|
|
f"Zoom: {effective_zoom}, Scale: {self.current_display_scale_factor:.2f}"
|
|
)
|
|
|
|
try:
|
|
map_tile_xy_ranges = get_tile_ranges_for_bbox(area_geographic_bbox, effective_zoom)
|
|
if not map_tile_xy_ranges:
|
|
raise MapCalculationError("Tile range calculation for area display failed.")
|
|
|
|
stitched_map_pil_image = self.map_tile_fetch_manager.stitch_map_image(
|
|
effective_zoom, map_tile_xy_ranges[0], map_tile_xy_ranges[1]
|
|
)
|
|
if not stitched_map_pil_image:
|
|
logger.error("Failed to stitch map image for area display.")
|
|
self.map_display_window_controller.show_map(None)
|
|
return
|
|
|
|
self._current_stitched_map_pil = stitched_map_pil_image
|
|
self._current_map_geo_bounds_deg = self.map_tile_fetch_manager._get_bounds_for_tile_range(
|
|
effective_zoom, map_tile_xy_ranges
|
|
)
|
|
self._current_map_render_zoom = effective_zoom
|
|
self._current_stitched_map_pixel_shape = (stitched_map_pil_image.height, stitched_map_pil_image.width)
|
|
|
|
map_with_area_bbox_pil = self._draw_area_bounding_box_on_map(
|
|
stitched_map_pil_image.copy(), area_geographic_bbox # Operate on a copy
|
|
)
|
|
|
|
self.map_display_window_controller.show_map(map_with_area_bbox_pil)
|
|
self._last_user_click_pixel_coords_on_displayed_image = None
|
|
|
|
except MapCalculationError as e_map_calc_area:
|
|
logger.error(f"Map calculation error during area display: {e_map_calc_area}")
|
|
if self.map_display_window_controller: self.map_display_window_controller.show_map(None)
|
|
except Exception as e_disp_map_area:
|
|
logger.exception(f"Unexpected error displaying map for area: {e_disp_map_area}")
|
|
if self.map_display_window_controller: self.map_display_window_controller.show_map(None)
|
|
|
|
def _can_perform_drawing_operations(self) -> bool:
|
|
"""Checks if conditions are met for drawing markers/boxes on the map."""
|
|
return bool(
|
|
self._current_map_geo_bounds_deg and
|
|
self._current_map_render_zoom is not None and
|
|
self._current_stitched_map_pixel_shape and # Shape of the *unscaled* stitched map
|
|
self.map_display_window_controller and # The window handler must exist
|
|
CV2_NUMPY_LIBS_AVAILABLE and PIL_IMAGE_LIB_AVAILABLE # Core drawing libs
|
|
)
|
|
|
|
def _draw_point_marker_on_map(
|
|
self,
|
|
pil_image_to_draw_on: ImageType,
|
|
latitude_deg: float,
|
|
longitude_deg: float
|
|
) -> ImageType:
|
|
"""Draws a marker for a single point on the provided PIL map image."""
|
|
if not self._can_perform_drawing_operations():
|
|
logger.warning("Cannot draw point marker: drawing context or libraries not ready.")
|
|
return pil_image_to_draw_on
|
|
|
|
# Convert geographic coordinates to pixel coordinates on the *unscaled* stitched map
|
|
pixel_coords_on_stitched_map = self.map_display_window_controller.geo_to_pixel_on_current_map( # type: ignore
|
|
latitude_deg, longitude_deg,
|
|
self._current_map_geo_bounds_deg, # type: ignore # Bounds of the unscaled map
|
|
self._current_stitched_map_pixel_shape, # type: ignore # Shape of the unscaled map
|
|
self._current_map_render_zoom # type: ignore # Zoom of the unscaled map
|
|
)
|
|
|
|
if pixel_coords_on_stitched_map and cv2 and np:
|
|
px, py = pixel_coords_on_stitched_map
|
|
logger.debug(f"Drawing point marker at pixel ({px},{py}) on stitched map.")
|
|
# Convert PIL to BGR NumPy array for OpenCV drawing
|
|
map_cv_bgr = cv2.cvtColor(np.array(pil_image_to_draw_on), cv2.COLOR_RGB2BGR) # type: ignore
|
|
# Draw a red cross marker
|
|
cv2.drawMarker(map_cv_bgr, (px, py), (0,0,255), cv2.MARKER_CROSS, markerSize=20, thickness=2) # type: ignore
|
|
# Convert back to PIL Image
|
|
return Image.fromarray(cv2.cvtColor(map_cv_bgr, cv2.COLOR_BGR2RGB)) # type: ignore
|
|
|
|
logger.warning("Failed to convert geo to pixel for point marker, or OpenCV/NumPy missing.")
|
|
return pil_image_to_draw_on # Return original if conversion failed
|
|
|
|
|
|
def _draw_area_bounding_box_on_map(
|
|
self,
|
|
pil_image_to_draw_on: ImageType,
|
|
area_bbox_deg: Tuple[float, float, float, float] # west, south, east, north
|
|
) -> ImageType:
|
|
"""Draws the outline of the area's bounding box on the PIL map image."""
|
|
if not self._can_perform_drawing_operations():
|
|
logger.warning("Cannot draw area BBox: drawing context or libraries not ready.")
|
|
return pil_image_to_draw_on
|
|
|
|
west, south, east, north = area_bbox_deg
|
|
corners_geo_coords = [
|
|
(west, north), (east, north), (east, south), (west, south) # TL, TR, BR, BL
|
|
]
|
|
pixel_corner_coords_list: List[Tuple[int,int]] = []
|
|
for lon, lat in corners_geo_coords:
|
|
px_coords = self.map_display_window_controller.geo_to_pixel_on_current_map( # type: ignore
|
|
lat, lon,
|
|
self._current_map_geo_bounds_deg, # type: ignore
|
|
self._current_stitched_map_pixel_shape, # type: ignore
|
|
self._current_map_render_zoom # type: ignore
|
|
)
|
|
if px_coords:
|
|
pixel_corner_coords_list.append(px_coords)
|
|
|
|
if len(pixel_corner_coords_list) == 4 and cv2 and np:
|
|
logger.debug(f"Drawing area BBox with pixel corners: {pixel_corner_coords_list}")
|
|
map_cv_bgr = cv2.cvtColor(np.array(pil_image_to_draw_on), cv2.COLOR_RGB2BGR) # type: ignore
|
|
pts_for_cv = np.array(pixel_corner_coords_list, np.int32).reshape((-1,1,2)) # type: ignore
|
|
cv2.polylines(map_cv_bgr, [pts_for_cv], isClosed=True, color=(255,0,0), thickness=2) # Blue rectangle # type: ignore
|
|
return Image.fromarray(cv2.cvtColor(map_cv_bgr, cv2.COLOR_BGR2RGB)) # type: ignore
|
|
|
|
logger.warning("Failed to convert all corners for area BBox, or OpenCV/NumPy missing.")
|
|
return pil_image_to_draw_on
|
|
|
|
|
|
def _draw_user_click_marker_on_map(
|
|
self,
|
|
pil_image_to_draw_on: ImageType
|
|
) -> Optional[ImageType]:
|
|
"""Draws a marker at the last user-clicked pixel location on the map image."""
|
|
if not self._last_user_click_pixel_coords_on_displayed_image or \
|
|
pil_image_to_draw_on is None or \
|
|
not self._can_perform_drawing_operations():
|
|
logger.debug("Conditions not met for drawing user click marker.")
|
|
return pil_image_to_draw_on
|
|
|
|
# IMPORTANT: The _last_user_click_pixel_coords_on_displayed_image are in the coordinate
|
|
# system of the SCALED image shown in MapDisplayWindow.
|
|
# To draw on pil_image_to_draw_on (which is the UN SCALED stitched map),
|
|
# we need to un-scale these pixel coordinates.
|
|
|
|
clicked_px_on_displayed, clicked_py_on_displayed = self._last_user_click_pixel_coords_on_displayed_image
|
|
|
|
# Un-scale the click coordinates to match the original stitched image
|
|
unscaled_click_px = int(round(clicked_px_on_displayed / self.current_display_scale_factor))
|
|
unscaled_click_py = int(round(clicked_py_on_displayed / self.current_display_scale_factor))
|
|
|
|
# Clamp to the dimensions of the unscaled stitched image
|
|
if self._current_stitched_map_pixel_shape:
|
|
h_unscaled, w_unscaled = self._current_stitched_map_pixel_shape
|
|
unscaled_click_px = max(0, min(unscaled_click_px, w_unscaled - 1))
|
|
unscaled_click_py = max(0, min(unscaled_click_py, h_unscaled - 1))
|
|
else:
|
|
logger.warning("Cannot accurately unscale click for marker: unscaled map shape unknown.")
|
|
return pil_image_to_draw_on
|
|
|
|
|
|
if cv2 and np:
|
|
try:
|
|
logger.debug(f"Drawing user click marker at unscaled pixel ({unscaled_click_px},{unscaled_click_py})")
|
|
map_cv_bgr = cv2.cvtColor(np.array(pil_image_to_draw_on), cv2.COLOR_RGB2BGR) # type: ignore
|
|
# Red cross marker for user click
|
|
cv2.drawMarker(map_cv_bgr, (unscaled_click_px,unscaled_click_py), (0,0,255), cv2.MARKER_CROSS, 15, 2) # type: ignore
|
|
return Image.fromarray(cv2.cvtColor(map_cv_bgr, cv2.COLOR_BGR2RGB)) # type: ignore
|
|
except Exception as e_draw_user_click:
|
|
logger.exception(f"Error drawing user click marker: {e_draw_user_click}")
|
|
return pil_image_to_draw_on # Return original on drawing error
|
|
return pil_image_to_draw_on
|
|
|
|
|
|
def handle_map_mouse_click(self, pixel_x_on_displayed: int, pixel_y_on_displayed: int) -> None:
|
|
"""
|
|
Handles a mouse click event from MapDisplayWindow (coordinates are on scaled image).
|
|
Converts pixel to geographic, fetches elevation, sends data to GUI queue,
|
|
and redraws the map with the new click marker.
|
|
"""
|
|
logger.debug(f"Map mouse click (on scaled img) received at pixel ({pixel_x_on_displayed}, {pixel_y_on_displayed})")
|
|
self._last_user_click_pixel_coords_on_displayed_image = (pixel_x_on_displayed, pixel_y_on_displayed)
|
|
|
|
if not self._can_perform_drawing_operations() or not self.map_display_window_controller:
|
|
logger.warning("Cannot process map click: map context or display handler not fully loaded.")
|
|
return
|
|
|
|
# The pixel_to_geo_on_current_map method in MapDisplayWindow expects pixel coordinates
|
|
# relative to the *displayed* (potentially scaled) image. It uses its own
|
|
# _last_displayed_image_shape which *is* the shape of the scaled image.
|
|
# The map bounds and zoom, however, always refer to the unscaled map data.
|
|
geo_coords_clicked = self.map_display_window_controller.pixel_to_geo_on_current_map( # type: ignore
|
|
pixel_x_on_displayed, pixel_y_on_displayed,
|
|
self._current_map_geo_bounds_deg, # type: ignore # Geographic bounds of the full unscaled map
|
|
self.map_display_window_controller._last_displayed_image_shape, # Pixel shape of displayed (scaled) map
|
|
self._current_map_render_zoom # type: ignore # Zoom level of the unscaled map
|
|
)
|
|
|
|
elevation_value: Optional[float] = None
|
|
elevation_display_text = "N/A"
|
|
latitude_value: Optional[float] = None
|
|
longitude_value: Optional[float] = None
|
|
|
|
if geo_coords_clicked:
|
|
latitude_value, longitude_value = geo_coords_clicked
|
|
logger.info(f"Map click converted to Geo: Lat={latitude_value:.5f}, Lon={longitude_value:.5f}")
|
|
|
|
elevation_value = self.elevation_manager.get_elevation(latitude_value, longitude_value)
|
|
|
|
if elevation_value is None: elevation_display_text = "Unavailable"
|
|
elif math.isnan(elevation_value): elevation_display_text = "NoData"
|
|
else: elevation_display_text = f"{elevation_value:.2f} m"
|
|
logger.info(f"Elevation at clicked geo point: {elevation_display_text}")
|
|
else:
|
|
logger.warning(f"Could not convert pixel click ({pixel_x_on_displayed}, {pixel_y_on_displayed}) to geo.")
|
|
elevation_display_text = "Error: Click out of bounds?"
|
|
|
|
try:
|
|
click_data_to_gui = {
|
|
"type": "map_click_data",
|
|
"latitude": latitude_value,
|
|
"longitude": longitude_value,
|
|
"elevation_str": elevation_display_text,
|
|
"elevation_val": elevation_value
|
|
}
|
|
self.gui_com_queue.put(click_data_to_gui)
|
|
logger.debug(f"Sent map_click_data to GUI queue: {click_data_to_gui}")
|
|
except Exception as e_queue_send:
|
|
logger.exception(f"Error putting click data onto GUI queue: {e_queue_send}")
|
|
|
|
# Redraw the map: take the original stitched map, draw relevant markers, then show.
|
|
# MapDisplayWindow.show_map() will re-apply the current_display_scale_factor.
|
|
if self._current_stitched_map_pil and self.map_display_window_controller:
|
|
base_map_for_drawing = self._current_stitched_map_pil.copy()
|
|
|
|
# Redraw point/area markers if applicable (logic for this needs to be robust)
|
|
# For simplicity, assume we only draw the latest click for now on top of base.
|
|
# A better way would be to store what 'mode' the map is in (point/area)
|
|
# and redraw the primary feature of that mode plus the click.
|
|
|
|
map_with_new_marker = self._draw_user_click_marker_on_map(base_map_for_drawing)
|
|
|
|
if map_with_new_marker:
|
|
self.map_display_window_controller.show_map(map_with_new_marker)
|
|
else: # Fallback if marker drawing fails
|
|
self.map_display_window_controller.show_map(base_map_for_drawing)
|
|
|
|
def shutdown(self) -> None:
|
|
"""Cleans up resources, particularly the map display window."""
|
|
logger.info("Shutting down GeoElevationMapViewer and its display window.")
|
|
if self.map_display_window_controller:
|
|
self.map_display_window_controller.destroy_window()
|
|
logger.info("GeoElevationMapViewer shutdown procedure complete.") |