From 7934fc8d2c0a72913196f6a0ad0692dd8c16b381 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Fri, 16 May 2025 12:57:24 +0200 Subject: [PATCH] fix map with grey band --- flightmonitor/map/map_canvas_manager.py | 263 +++++++++++++++--------- 1 file changed, 165 insertions(+), 98 deletions(-) diff --git a/flightmonitor/map/map_canvas_manager.py b/flightmonitor/map/map_canvas_manager.py index 97a3e8f..6f046d2 100644 --- a/flightmonitor/map/map_canvas_manager.py +++ b/flightmonitor/map/map_canvas_manager.py @@ -157,34 +157,44 @@ class MapCanvasManager: self.update_map_view_for_bbox(new_bbox_dict, preserve_current_zoom_if_possible=False) def update_map_view_for_bbox(self, target_bbox_dict: Dict[str, float], preserve_current_zoom_if_possible: bool = False): + # Questa funzione ora calcola il centro e lo zoom OTTIMALI + # per visualizzare target_bbox_dict e poi chiama recenter_and_redraw. if not target_bbox_dict: logger.warning("update_map_view_for_bbox called with no target_bbox_dict.") return - self._target_bbox_input = target_bbox_dict.copy() + self._target_bbox_input = target_bbox_dict.copy() # Memorizza il BBox utente + + # Se questa chiamata deriva da un'impostazione esplicita del BBox (non da pan/zoom interattivo), + # aggiorna anche il BBox di riferimento per i voli. + if not preserve_current_zoom_if_possible: + self._active_api_bbox_for_flights = target_bbox_dict.copy() + lat_min = target_bbox_dict['lat_min'] lon_min = target_bbox_dict['lon_min'] lat_max = target_bbox_dict['lat_max'] lon_max = target_bbox_dict['lon_max'] - center_lat = (lat_min + lat_max) / 2.0 - center_lon = (lon_min + lon_max) / 2.0 - if lon_min > lon_max: + # Il centro della mappa sarà il centro del BBox utente + view_center_lat = (lat_min + lat_max) / 2.0 + view_center_lon = (lon_min + lon_max) / 2.0 + if lon_min > lon_max: # Antimeridian center_lon_adjusted = (lon_min + (lon_max + 360.0)) / 2.0 - center_lon = center_lon_adjusted - 360.0 if center_lon_adjusted >= 180.0 else center_lon_adjusted + view_center_lon = center_lon_adjusted - 360.0 if center_lon_adjusted >= 180.0 else center_lon_adjusted - logger.info(f"Updating map view for target BBox: {self._target_bbox_input}. Calculated center: ({center_lat:.4f}, {center_lon:.4f})") + logger.info(f"Updating map view for target BBox: {self._target_bbox_input}. Target view center: ({view_center_lat:.4f}, {view_center_lon:.4f})") - new_zoom_level_to_use = self._current_zoom if self._current_zoom is not None and preserve_current_zoom_if_possible else DEFAULT_INITIAL_ZOOM + # Lo zoom da usare. Se preserviamo, usiamo il corrente, altrimenti calcoliamo. + zoom_to_use = self._current_zoom if self._current_zoom is not None and preserve_current_zoom_if_possible else DEFAULT_INITIAL_ZOOM if not preserve_current_zoom_if_possible: - calculated_zoom_val = DEFAULT_INITIAL_ZOOM + calculated_zoom_for_target_bbox = DEFAULT_INITIAL_ZOOM if PYPROJ_MODULE_LOCALLY_AVAILABLE and pyproj is not None: try: geod = pyproj.Geod(ellps="WGS84") - _, _, height_m = geod.inv(center_lon, lat_min, center_lon, lat_max) - _, _, width_m = geod.inv(lon_min, center_lat, lon_max, center_lat) + _, _, height_m = geod.inv(view_center_lon, lat_min, view_center_lon, lat_max) + _, _, width_m = geod.inv(lon_min, view_center_lat, lon_max, view_center_lat) height_m = abs(height_m) width_m = abs(width_m) logger.debug(f"Geographic dimensions of target BBox: Width={width_m:.0f}m, Height={height_m:.0f}m") @@ -207,54 +217,56 @@ class MapCanvasManager: if res_needed_for_height != float('inf') and res_needed_for_height > 0: target_resolution_m_px = res_needed_for_height valid_res_found = True - # Modifica qui per usare max correttemente if res_needed_for_width != float('inf') and res_needed_for_width > 0: - if valid_res_found: - target_resolution_m_px = max(target_resolution_m_px, res_needed_for_width) - else: - target_resolution_m_px = res_needed_for_width + current_target_res = target_resolution_m_px if valid_res_found else float('inf') # Inizializza con l'altezza se valida + target_resolution_m_px = max(current_target_res, res_needed_for_width) # Prendi la risoluzione maggiore (zoom minore) valid_res_found = True - if not valid_res_found: target_resolution_m_px = float('inf') + if not valid_res_found and target_resolution_m_px == float('inf'): # Se nessuna dimensione valida + logger.warning("Could not determine valid target resolution from BBox dimensions or canvas size.") + target_resolution_m_px = 0 # Per forzare fallback - logger.debug(f"Canvas: {current_canvas_width}x{current_canvas_height}. Resolutions needed H,W (m/px): {res_needed_for_height:.2f}, {res_needed_for_width:.2f}. Target res: {target_resolution_m_px:.2f} m/px") + logger.debug(f"Canvas: {current_canvas_width}x{current_canvas_height}. Resolutions needed for target BBox (H,W) (m/px): {res_needed_for_height:.2f}, {res_needed_for_width:.2f}. Final Target res: {target_resolution_m_px:.2f} m/px") if target_resolution_m_px > 0 and target_resolution_m_px != float('inf'): EARTH_CIRCUMFERENCE_METERS = 40075016.686 - clamped_center_lat_for_cos = max(-85.05, min(85.05, center_lat)) + clamped_center_lat_for_cos = max(-85.05, min(85.05, view_center_lat)) # Usa il centro del BBox per il calcolo dello zoom cos_val = math.cos(math.radians(clamped_center_lat_for_cos)) if cos_val > 1e-9 and self.tile_manager.tile_size > 0: term_for_log = (EARTH_CIRCUMFERENCE_METERS * cos_val) / \ (self.tile_manager.tile_size * target_resolution_m_px) if term_for_log > 0: precise_zoom = math.log2(term_for_log) - calculated_zoom_val = int(round(precise_zoom)) + calculated_zoom_for_target_bbox = int(round(precise_zoom)) max_zoom_limit = self.map_service.max_zoom if self.map_service else DEFAULT_MAX_ZOOM_FALLBACK - new_zoom_level_to_use = max(MIN_ZOOM_LEVEL, min(calculated_zoom_val, max_zoom_limit)) - logger.info(f"Calculated zoom {new_zoom_level_to_use} to fit BBox (precise float: {precise_zoom:.2f}).") + zoom_to_use = max(MIN_ZOOM_LEVEL, min(calculated_zoom_for_target_bbox, max_zoom_limit)) + logger.info(f"Calculated zoom {zoom_to_use} to fit target BBox (precise float: {precise_zoom:.2f}).") else: - logger.warning(f"Cannot calculate zoom for BBox: term for log2 is non-positive ({term_for_log}). Using zoom {DEFAULT_INITIAL_ZOOM}.") - new_zoom_level_to_use = DEFAULT_INITIAL_ZOOM + logger.warning(f"Cannot calculate zoom for BBox: term for log2 non-positive. Using zoom {DEFAULT_INITIAL_ZOOM}.") + zoom_to_use = DEFAULT_INITIAL_ZOOM else: - logger.warning(f"Cannot calculate zoom for BBox: cosine of latitude or tile_size is problematic. Using zoom {DEFAULT_INITIAL_ZOOM}.") - new_zoom_level_to_use = DEFAULT_INITIAL_ZOOM + logger.warning(f"Cannot calculate zoom for BBox: cosine of latitude or tile_size problematic. Using zoom {DEFAULT_INITIAL_ZOOM}.") + zoom_to_use = DEFAULT_INITIAL_ZOOM else: - logger.warning(f"Cannot calculate zoom for BBox: target resolution is invalid or zero. Using zoom {DEFAULT_INITIAL_ZOOM}.") - new_zoom_level_to_use = DEFAULT_INITIAL_ZOOM + logger.warning(f"Cannot calculate zoom for BBox: target resolution invalid. Using zoom {DEFAULT_INITIAL_ZOOM}.") + zoom_to_use = DEFAULT_INITIAL_ZOOM except Exception as e: logger.error(f"Error calculating zoom for BBox using pyproj: {e}. Using zoom {DEFAULT_INITIAL_ZOOM}.", exc_info=True) - new_zoom_level_to_use = DEFAULT_INITIAL_ZOOM + zoom_to_use = DEFAULT_INITIAL_ZOOM else: - logger.warning("Pyproj module not available in MapCanvasManager, cannot accurately calculate zoom for BBox. Using default zoom.") - new_zoom_level_to_use = DEFAULT_INITIAL_ZOOM + logger.warning("Pyproj module not available. Using default zoom.") + zoom_to_use = DEFAULT_INITIAL_ZOOM - self.recenter_and_redraw(center_lat, center_lon, new_zoom_level_to_use, ensure_bbox_is_covered=self._target_bbox_input) + # recenter_and_redraw userà view_center_lat, view_center_lon e zoom_to_use. + # Non passerà più `ensure_bbox_is_covered` perché il centro e lo zoom + # sono già stati calcolati per mostrare il _target_bbox_input. + self.recenter_and_redraw(view_center_lat, view_center_lon, zoom_to_use) - def recenter_and_redraw(self, center_lat: float, center_lon: float, zoom_level: int, - ensure_bbox_is_covered: Optional[Dict[str,float]] = None): - logger.info(f"Recentering map. Target Center: ({center_lat:.4f}, {center_lon:.4f}), Zoom: {zoom_level}") - if ensure_bbox_is_covered: - logger.debug(f"This redraw will ensure BBox is covered: {ensure_bbox_is_covered}") + def recenter_and_redraw(self, center_lat: float, center_lon: float, zoom_level: int, + ensure_bbox_is_covered_dict: Optional[Dict[str, float]] = None): + logger.info(f"Recentering map. Target View Center: ({center_lat:.4f}, {center_lon:.4f}), Zoom: {zoom_level}") + if ensure_bbox_is_covered_dict: + logger.debug(f"This redraw must ensure BBox is fully visible: {ensure_bbox_is_covered_dict}") self._current_center_lat = center_lat self._current_center_lon = center_lon @@ -262,54 +274,62 @@ class MapCanvasManager: current_canvas_width = self.canvas.winfo_width() current_canvas_height = self.canvas.winfo_height() - if current_canvas_width <=1 : current_canvas_width = self.canvas_width - if current_canvas_height <=1 : current_canvas_height = self.canvas_height + if current_canvas_width <= 1: current_canvas_width = self.canvas_width + if current_canvas_height <= 1: current_canvas_height = self.canvas_height - map_fetch_geo_bounds_for_tiles: Optional[Tuple[float, float, float, float]] + map_fetch_geo_bounds_for_tiles_tuple: Optional[Tuple[float, float, float, float]] - if ensure_bbox_is_covered: - # Logica per calcolare un BBox di tile che copra `ensure_bbox_is_covered` - # E che sia anche centrato su `center_lat`, `center_lon` e riempia il canvas - # Questo è il caso più complesso: vogliamo vedere un BBox specifico E riempire il canvas. - # Lo zoom è già stato calcolato in `update_map_view_for_bbox` per far entrare `ensure_bbox_is_covered`. - # Quindi, i `map_fetch_geo_bounds_for_tiles` dovrebbero essere quelli che, a questo zoom e centro, - # riempiono il canvas. Se questi bounds sono più piccoli di `ensure_bbox_is_covered`, - # allora lo zoom calcolato non era sufficientemente basso. - # Per ora, semplifichiamo: il BBox per le tile sarà quello che riempie il canvas - # al centro/zoom calcolati (che dovrebbero già tenere conto di far entrare `ensure_bbox_is_covered`). - map_fetch_geo_bounds_for_tiles = calculate_geographic_bbox_from_pixel_size_and_zoom( - self._current_center_lat, self._current_center_lon, + if ensure_bbox_is_covered_dict: + # Caso 1: Dobbiamo visualizzare un BBox specifico (_target_bbox_input) + # Lo zoom e il centro sono già stati calcolati da update_map_view_for_bbox + # per far entrare questo BBox nel canvas. + # Ora dobbiamo determinare le tile da scaricare. + # Il BBox delle tile deve coprire almeno ensure_bbox_is_covered_dict. + # E, per evitare barre grigie, dovrebbe anche cercare di riempire il canvas + # mantenendo il centro e lo zoom calcolati. + + user_bb = ensure_bbox_is_covered_dict + + # Calcola il BBox che riempirebbe il canvas a questo centro e zoom + canvas_fill_bbox = calculate_geographic_bbox_from_pixel_size_and_zoom( + self._current_center_lat, self._current_center_lon, # Centro del BBox utente current_canvas_width, current_canvas_height, self._current_zoom, self.tile_manager.tile_size ) - if map_fetch_geo_bounds_for_tiles: - logger.debug(f"Tile fetching for BBox coverage, calculated canvas fill bounds: {map_fetch_geo_bounds_for_tiles} at zoom {self._current_zoom}") + + if not canvas_fill_bbox: + logger.error("Failed to calculate canvas_fill_bbox even when ensure_bbox_is_covered was set. Using user_bbox directly.") + map_fetch_geo_bounds_for_tiles_tuple = ( + user_bb['lon_min'], user_bb['lat_min'], + user_bb['lon_max'], user_bb['lat_max'] + ) else: - logger.error("Failed to calculate canvas filling BBox even when ensure_bbox_is_covered was set.") - # Fallback: usa direttamente ensure_bbox_is_covered per le tile, potrebbe non riempire il canvas - map_fetch_geo_bounds_for_tiles = ( - ensure_bbox_is_covered['lon_min'], ensure_bbox_is_covered['lat_min'], - ensure_bbox_is_covered['lon_max'], ensure_bbox_is_covered['lat_max'] - ) - logger.warning(f"Falling back to fetching tiles strictly for ensure_bbox_is_covered: {map_fetch_geo_bounds_for_tiles}") + # Ora crea un BBox che sia l'unione del BBox utente e del BBox che riempie il canvas. + # Questo assicura che il BBox utente sia dentro E che il canvas sia riempito. + final_west = min(user_bb['lon_min'], canvas_fill_bbox[0]) + final_south = min(user_bb['lat_min'], canvas_fill_bbox[1]) + final_east = max(user_bb['lon_max'], canvas_fill_bbox[2]) + final_north = max(user_bb['lat_max'], canvas_fill_bbox[3]) + map_fetch_geo_bounds_for_tiles_tuple = (final_west, final_south, final_east, final_north) + logger.debug(f"Combined BBox (user & canvas fill) for tile fetching: {map_fetch_geo_bounds_for_tiles_tuple} at zoom {self._current_zoom}") - else: # Pan/Zoom interattivo - map_fetch_geo_bounds_for_tiles = calculate_geographic_bbox_from_pixel_size_and_zoom( + else: # Caso 2: Pan/Zoom interattivo, non c'è un "ensure_bbox_is_covered_dict" + map_fetch_geo_bounds_for_tiles_tuple = calculate_geographic_bbox_from_pixel_size_and_zoom( self._current_center_lat, self._current_center_lon, current_canvas_width, current_canvas_height, self._current_zoom, self.tile_manager.tile_size ) - if map_fetch_geo_bounds_for_tiles: - logger.debug(f"Tile fetching for interactive pan/zoom (canvas-fill): {map_fetch_geo_bounds_for_tiles} at zoom {self._current_zoom}") + if map_fetch_geo_bounds_for_tiles_tuple: + logger.debug(f"Tile fetching for interactive pan/zoom (canvas-fill): {map_fetch_geo_bounds_for_tiles_tuple} at zoom {self._current_zoom}") - if not map_fetch_geo_bounds_for_tiles: + if not map_fetch_geo_bounds_for_tiles_tuple: logger.error("Failed to determine geographic bounds for tile fetching. Cannot draw map.") self._clear_canvas_content() return - tile_xy_ranges = get_tile_ranges_for_bbox(map_fetch_geo_bounds_for_tiles, self._current_zoom) + tile_xy_ranges = get_tile_ranges_for_bbox(map_fetch_geo_bounds_for_tiles_tuple, self._current_zoom) if not tile_xy_ranges: - logger.error(f"Failed to get tile ranges for fetch_bounds {map_fetch_geo_bounds_for_tiles}. Cannot draw map.") + logger.error(f"Failed to get tile ranges for fetch_bounds {map_fetch_geo_bounds_for_tiles_tuple}. Cannot draw map.") self._clear_canvas_content() return logger.debug(f"Tile ranges for current view: X={tile_xy_ranges[0]}, Y={tile_xy_ranges[1]}") @@ -326,13 +346,13 @@ class MapCanvasManager: actual_stitched_geo_bounds = self.tile_manager._get_bounds_for_tile_range(self._current_zoom, tile_xy_ranges) if not actual_stitched_geo_bounds: logger.error("Failed to get actual geographic bounds of stitched tiles. Using calculated fetch_bounds as fallback.") - self._current_map_geo_bounds = map_fetch_geo_bounds_for_tiles + self._current_map_geo_bounds = map_fetch_geo_bounds_for_tiles_tuple else: self._current_map_geo_bounds = actual_stitched_geo_bounds - logger.debug(f"Actual geographic bounds of final stitched map: {self._current_map_geo_bounds}") + logger.debug(f"Actual geographic bounds of final stitched map (self._current_map_geo_bounds): {self._current_map_geo_bounds}") self._map_pil_image = stitched_map_pil - self._redraw_canvas_content() + self._redraw_canvas_content() def _redraw_canvas_content(self): logger.debug(f"_redraw_canvas_content called. Current zoom: {self._current_zoom}, Flights to draw: {len(self._current_flights_to_display)}") @@ -517,28 +537,74 @@ class MapCanvasManager: if new_zoom != self._current_zoom: logger.info(f"Zoom changed from {self._current_zoom} to {new_zoom}") - # Durante lo zoom interattivo, non c'è un "ensure_bbox_is_covered" fisso. - # Il _target_bbox_input (contorno blu) rimane, ma la vista si adatta. - self.recenter_and_redraw(target_center_lat, target_center_lon, new_zoom, ensure_bbox_is_covered=None) + self.recenter_and_redraw(target_center_lat, target_center_lon, new_zoom) else: logger.debug(f"Zoom unchanged (already at min/max: {self._current_zoom})") def _on_mouse_button_press(self, event: tk.Event): if not self.canvas.winfo_exists(): return - self._drag_start_x = event.x - self._drag_start_y = event.y - self._is_dragging = True + # Salva il punto di inizio del drag in coordinate CANVAS + self._drag_start_x_canvas = event.x + self._drag_start_y_canvas = event.y + # Salva anche il centro geografico corrente all'inizio del drag + if self._current_center_lat is not None and self._current_center_lon is not None: + self._drag_start_center_lon = self._current_center_lon + self._drag_start_center_lat = self._current_center_lat + else: # Non dovrebbe succedere se la mappa è visualizzata + self._drag_start_center_lon = None + self._drag_start_center_lat = None + + self._is_dragging = False # Non considerarlo un drag finché non c'è movimento self.canvas.config(cursor="fleur") - logger.debug(f"Mouse button press at ({event.x}, {event.y}) for panning.") + logger.debug(f"Mouse button press at ({event.x}, {event.y}). Ready for potential pan.") def _on_mouse_drag(self, event: tk.Event): - if not self._is_dragging or self._drag_start_x is None or self._drag_start_y is None or \ - self._current_map_geo_bounds is None or not self.canvas.winfo_exists() or \ - self._current_center_lat is None or self._current_center_lon is None or self._current_zoom is None: + if self._drag_start_x_canvas is None or self._drag_start_y_canvas is None or \ + self._drag_start_center_lon is None or self._drag_start_center_lat is None or \ + self._current_map_geo_bounds is None or not self.canvas.winfo_exists() or self._current_zoom is None: return - dx_pixel = event.x - self._drag_start_x - dy_pixel = event.y - self._drag_start_y + self._is_dragging = True # Il mouse si è mosso mentre era premuto + + # --- Logica per visualizzare un'anteprima del pan (OPZIONALE e più complesso) --- + # Si potrebbe spostare l'immagine PhotoImage esistente sul canvas SPOSTANDO l'item_id + # self.canvas.move(self._canvas_image_id, dx_pixel, dy_pixel) + # Questo però non ricarica le tile, mostra solo la stessa immagine spostata. + # Per ora, omettiamo l'anteprima per semplicità e ridisegniamo solo al rilascio. + # Potremmo cambiare il cursore per indicare il drag. + # logger.debug(f"Dragging to ({event.x}, {event.y})") # Troppo verboso + pass + + def _on_mouse_button_release(self, event: tk.Event): + if not self.canvas.winfo_exists(): + self.canvas.config(cursor="") + return + + self.canvas.config(cursor="") # Ripristina cursore + + if not self._is_dragging: # Se non c'è stato drag, è stato un click semplice + logger.debug("Mouse button release without dragging (simple click). Action handled by specific button bindings (e.g., right-click).") + self._drag_start_x_canvas = None # Resetta per sicurezza + self._drag_start_y_canvas = None + self._drag_start_center_lon = None + self._drag_start_center_lat = None + self._is_dragging = False + return + + # Se c'è stato un drag (self._is_dragging è True) + logger.debug(f"Mouse button release after drag to ({event.x}, {event.y}). Finalizing pan.") + + if self._drag_start_x_canvas is None or self._drag_start_y_canvas is None or \ + self._drag_start_center_lon is None or self._drag_start_center_lat is None or \ + self._current_map_geo_bounds is None or self._current_zoom is None : + logger.warning("Cannot finalize pan: drag start state or map context is missing.") + self._is_dragging = False + self._drag_start_x_canvas = None + self._drag_start_y_canvas = None + return + + dx_pixel = event.x - self._drag_start_x_canvas + dy_pixel = event.y - self._drag_start_y_canvas map_width_deg = self._current_map_geo_bounds[2] - self._current_map_geo_bounds[0] map_height_deg = self._current_map_geo_bounds[3] - self._current_map_geo_bounds[1] @@ -550,33 +616,34 @@ class MapCanvasManager: if current_canvas_height <=1 : current_canvas_height = self.canvas_height if current_canvas_width <= 0 or current_canvas_height <= 0: - logger.warning("Cannot pan, canvas dimensions are zero.") + logger.warning("Cannot finalize pan, canvas dimensions are zero.") + self._is_dragging = False return deg_per_pixel_lon = map_width_deg / current_canvas_width deg_per_pixel_lat = map_height_deg / current_canvas_height - delta_lon = -dx_pixel * deg_per_pixel_lon - delta_lat = dy_pixel * deg_per_pixel_lat + delta_lon = -dx_pixel * deg_per_pixel_lon + delta_lat = dy_pixel * deg_per_pixel_lat - new_center_lon = self._current_center_lon + delta_lon - new_center_lat = self._current_center_lat + delta_lat + # Il nuovo centro è calcolato rispetto al centro che avevamo ALL'INIZIO del drag + new_center_lon = self._drag_start_center_lon + delta_lon + new_center_lat = self._drag_start_center_lat + delta_lat new_center_lon = (new_center_lon + 180) % 360 - 180 - new_center_lat = max(-85.05112878, min(85.05112878, new_center_lat)) # Limiti Web Mercator + new_center_lat = max(-85.05112878, min(85.05112878, new_center_lat)) - self.recenter_and_redraw(new_center_lat, new_center_lon, self._current_zoom, ensure_bbox_is_covered=None) + logger.info(f"Pan finalized. Old center: ({self._drag_start_center_lat:.4f}, {self._drag_start_center_lon:.4f}), New center: ({new_center_lat:.4f}, {new_center_lon:.4f})") + + # Ridisegna la mappa con il nuovo centro, mantenendo lo zoom. + # Non c'è un "ensure_bbox_is_covered" specifico per il pan. + self.recenter_and_redraw(new_center_lat, new_center_lon, self._current_zoom, ensure_bbox_is_covered_dict=None) #MODIFICATO nome parametro - self._drag_start_x = event.x - self._drag_start_y = event.y - - def _on_mouse_button_release(self, event: tk.Event): - if not self.canvas.winfo_exists(): return self._is_dragging = False - self._drag_start_x = None - self._drag_start_y = None - self.canvas.config(cursor="") - logger.debug("Mouse button release, panning finished.") + self._drag_start_x_canvas = None + self._drag_start_y_canvas = None + self._drag_start_center_lon = None + self._drag_start_center_lat = None def _on_right_click(self, event: tk.Event): if not self.canvas.winfo_exists(): return