added meridians and parallels to the map
This commit is contained in:
parent
e74e27bb4e
commit
87cc1850c3
@ -8,6 +8,7 @@ from collections import deque
|
|||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
import copy
|
import copy
|
||||||
|
import os # Aggiunto per _load_label_font
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageTk, ImageDraw, ImageFont
|
from PIL import Image, ImageTk, ImageDraw, ImageFont
|
||||||
@ -120,6 +121,9 @@ class MapCanvasManager:
|
|||||||
return None, None, "Failed to stitch map"
|
return None, None, "Failed to stitch map"
|
||||||
|
|
||||||
stitched_bounds = self.tile_manager._get_bounds_for_tile_range(zoom, tile_ranges)
|
stitched_bounds = self.tile_manager._get_bounds_for_tile_range(zoom, tile_ranges)
|
||||||
|
if not stitched_bounds: # Aggiunto controllo di robustezza
|
||||||
|
return None, None, "Failed to get bounds for stitched map"
|
||||||
|
|
||||||
center_px = geo_to_pixel_on_unscaled_map(center_lat, center_lon, stitched_bounds, stitched_map.size)
|
center_px = geo_to_pixel_on_unscaled_map(center_lat, center_lon, stitched_bounds, stitched_map.size)
|
||||||
if not center_px:
|
if not center_px:
|
||||||
center_px = (stitched_map.width / 2, stitched_map.height / 2)
|
center_px = (stitched_map.width / 2, stitched_map.height / 2)
|
||||||
@ -134,24 +138,39 @@ class MapCanvasManager:
|
|||||||
if not final_bounds:
|
if not final_bounds:
|
||||||
return None, None, "Failed to calculate final map bounds"
|
return None, None, "Failed to calculate final map bounds"
|
||||||
|
|
||||||
image_to_draw = final_image.convert("RGBA")
|
# --- MODIFICA CHIAVE: Logica di disegno con overlay trasparente ---
|
||||||
draw = ImageDraw.Draw(image_to_draw)
|
# 1. Crea un overlay trasparente delle stesse dimensioni della mappa
|
||||||
|
overlay_rgba = Image.new("RGBA", final_image.size, (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(overlay_rgba)
|
||||||
|
|
||||||
|
# 2. Esegui tutti i disegni sull'overlay trasparente
|
||||||
|
if map_constants.GRATICULE_ENABLED and final_bounds:
|
||||||
|
graticule_font = map_drawing._load_label_font(0, is_graticule=True)
|
||||||
|
map_drawing.draw_graticules(draw, final_bounds, overlay_rgba.size, graticule_font)
|
||||||
|
|
||||||
if draw_target_bbox and target_bbox and map_utils._is_valid_bbox_dict(target_bbox):
|
if draw_target_bbox and target_bbox and map_utils._is_valid_bbox_dict(target_bbox):
|
||||||
bbox_wesn = (target_bbox["lon_min"], target_bbox["lat_min"], target_bbox["lon_max"], target_bbox["lat_max"])
|
bbox_wesn = (target_bbox["lon_min"], target_bbox["lat_min"], target_bbox["lon_max"], target_bbox["lat_max"])
|
||||||
map_drawing.draw_area_bounding_box(image_to_draw, bbox_wesn, final_bounds, image_to_draw.size, "blue", 2)
|
map_drawing.draw_area_bounding_box(overlay_rgba, bbox_wesn, final_bounds, overlay_rgba.size, "blue", 2)
|
||||||
|
|
||||||
if flights:
|
if flights:
|
||||||
font = map_drawing._load_label_font(12 + (zoom - 10))
|
label_font_size = 10 + (zoom - 10)
|
||||||
|
flight_label_font = map_drawing._load_label_font(label_font_size)
|
||||||
for flight in flights:
|
for flight in flights:
|
||||||
if flight.latitude is not None and flight.longitude is not None:
|
if flight.latitude is not None and flight.longitude is not None:
|
||||||
pixel_coords = map_drawing.geo_to_pixel_on_unscaled_map(flight.latitude, flight.longitude, final_bounds, image_to_draw.size)
|
pixel_coords = map_drawing.geo_to_pixel_on_unscaled_map(flight.latitude, flight.longitude, final_bounds, overlay_rgba.size)
|
||||||
if pixel_coords:
|
if pixel_coords:
|
||||||
map_drawing._draw_single_flight(
|
map_drawing._draw_single_flight(
|
||||||
draw, pixel_coords, flight, font, tracks.get(flight.icao24), final_bounds, image_to_draw.size
|
draw, pixel_coords, flight, flight_label_font, tracks.get(flight.icao24), final_bounds, overlay_rgba.size
|
||||||
)
|
)
|
||||||
|
|
||||||
return ImageTk.PhotoImage(image_to_draw), final_bounds, None
|
# 3. Fai il 'composite' dell'overlay sulla mappa di base
|
||||||
|
# Prima converto l'immagine base in RGBA per poter fare il composite
|
||||||
|
base_map_rgba = final_image.convert("RGBA")
|
||||||
|
composite_image = Image.alpha_composite(base_map_rgba, overlay_rgba)
|
||||||
|
|
||||||
|
# 4. Crea il PhotoImage dall'immagine composita finale
|
||||||
|
return ImageTk.PhotoImage(composite_image), final_bounds, None
|
||||||
|
# --- FINE MODIFICA ---
|
||||||
|
|
||||||
def _start_gui_result_processing(self):
|
def _start_gui_result_processing(self):
|
||||||
if self._gui_after_id_result_processor and self.canvas.winfo_exists():
|
if self._gui_after_id_result_processor and self.canvas.winfo_exists():
|
||||||
@ -214,6 +233,9 @@ class MapCanvasManager:
|
|||||||
self.canvas.bind("<B1-Motion>", self._on_mouse_drag)
|
self.canvas.bind("<B1-Motion>", self._on_mouse_drag)
|
||||||
self.canvas.bind("<ButtonRelease-1>", self._on_left_button_release)
|
self.canvas.bind("<ButtonRelease-1>", self._on_left_button_release)
|
||||||
self.canvas.bind("<ButtonPress-3>", self._on_right_click)
|
self.canvas.bind("<ButtonPress-3>", self._on_right_click)
|
||||||
|
self.canvas.bind("<MouseWheel>", self._on_mouse_wheel)
|
||||||
|
self.canvas.bind("<Button-4>", self._on_mouse_wheel_linux)
|
||||||
|
self.canvas.bind("<Button-5>", self._on_mouse_wheel_linux)
|
||||||
self._drag_start_x_canvas, self._drag_start_y_canvas = None, None
|
self._drag_start_x_canvas, self._drag_start_y_canvas = None, None
|
||||||
|
|
||||||
def _on_canvas_resize(self, event: tk.Event):
|
def _on_canvas_resize(self, event: tk.Event):
|
||||||
@ -260,9 +282,18 @@ class MapCanvasManager:
|
|||||||
self.app_controller.on_map_left_click(lat, lon, event.x, event.y, event.x_root, event.y_root)
|
self.app_controller.on_map_left_click(lat, lon, event.x, event.y, event.x_root, event.y_root)
|
||||||
self._drag_start_x_canvas = None
|
self._drag_start_x_canvas = None
|
||||||
|
|
||||||
|
def _on_mouse_wheel(self, event: tk.Event):
|
||||||
|
if self._current_zoom_gui is None: return
|
||||||
|
if event.delta > 0: self.zoom_in_at_center()
|
||||||
|
else: self.zoom_out_at_center()
|
||||||
|
|
||||||
|
def _on_mouse_wheel_linux(self, event: tk.Event):
|
||||||
|
if self._current_zoom_gui is None: return
|
||||||
|
if event.num == 4: self.zoom_in_at_center()
|
||||||
|
elif event.num == 5: self.zoom_out_at_center()
|
||||||
|
|
||||||
def get_clicked_flight_icao(self, canvas_x: int, canvas_y: int) -> Optional[str]:
|
def get_clicked_flight_icao(self, canvas_x: int, canvas_y: int) -> Optional[str]:
|
||||||
if not self._current_map_geo_bounds_gui or not self._map_photo_image:
|
if not self._current_map_geo_bounds_gui or not self._map_photo_image: return None
|
||||||
return None
|
|
||||||
min_dist_sq, clicked_icao = float('inf'), None
|
min_dist_sq, clicked_icao = float('inf'), None
|
||||||
radius_sq = 15**2
|
radius_sq = 15**2
|
||||||
map_size = (self._map_photo_image.width(), self._map_photo_image.height())
|
map_size = (self._map_photo_image.width(), self._map_photo_image.height())
|
||||||
@ -313,7 +344,7 @@ class MapCanvasManager:
|
|||||||
if not preserve_zoom or not zoom:
|
if not preserve_zoom or not zoom:
|
||||||
size_km = map_utils.calculate_geographic_bbox_size_km((bbox["lon_min"], bbox["lat_min"], bbox["lon_max"], bbox["lat_max"]))
|
size_km = map_utils.calculate_geographic_bbox_size_km((bbox["lon_min"], bbox["lat_min"], bbox["lon_max"], bbox["lat_max"]))
|
||||||
if size_km:
|
if size_km:
|
||||||
w, h = self.canvas_width * 0.9, self.canvas_height * 0.9
|
w, h = self.canvas_width * (1 - CANVAS_MARGIN_FACTOR_FOR_BBOX_FIT), self.canvas_height * (1 - CANVAS_MARGIN_FACTOR_FOR_BBOX_FIT)
|
||||||
zoom_w = map_utils.calculate_zoom_level_for_geographic_size(center_lat, size_km[0]*1000, int(w), self.tile_manager.tile_size)
|
zoom_w = map_utils.calculate_zoom_level_for_geographic_size(center_lat, size_km[0]*1000, int(w), self.tile_manager.tile_size)
|
||||||
zoom_h = map_utils.calculate_zoom_level_for_geographic_size(center_lat, size_km[1]*1000, int(h), self.tile_manager.tile_size)
|
zoom_h = map_utils.calculate_zoom_level_for_geographic_size(center_lat, size_km[1]*1000, int(h), self.tile_manager.tile_size)
|
||||||
zoom = min(zoom_w, zoom_h) if zoom_w and zoom_h else map_constants.DEFAULT_INITIAL_ZOOM
|
zoom = min(zoom_w, zoom_h) if zoom_w and zoom_h else map_constants.DEFAULT_INITIAL_ZOOM
|
||||||
@ -321,15 +352,12 @@ class MapCanvasManager:
|
|||||||
|
|
||||||
def _clear_canvas_display_elements(self):
|
def _clear_canvas_display_elements(self):
|
||||||
if self.canvas.winfo_exists():
|
if self.canvas.winfo_exists():
|
||||||
if self._canvas_image_id:
|
if self._canvas_image_id: self.canvas.delete(self._canvas_image_id)
|
||||||
self.canvas.delete(self._canvas_image_id)
|
if self._placeholder_text_id: self.canvas.delete(self._placeholder_text_id)
|
||||||
if self._placeholder_text_id:
|
|
||||||
self.canvas.delete(self._placeholder_text_id)
|
|
||||||
self._canvas_image_id, self._placeholder_text_id, self._map_photo_image = None, None, None
|
self._canvas_image_id, self._placeholder_text_id, self._map_photo_image = None, None, None
|
||||||
|
|
||||||
def _display_placeholder_text(self, text: str):
|
def _display_placeholder_text(self, text: str):
|
||||||
if not self.canvas.winfo_exists():
|
if not self.canvas.winfo_exists(): return
|
||||||
return
|
|
||||||
self._clear_canvas_display_elements()
|
self._clear_canvas_display_elements()
|
||||||
self.canvas.configure(bg=getattr(map_constants, "DEFAULT_PLACEHOLDER_COLOR_RGB_TK", "gray85"))
|
self.canvas.configure(bg=getattr(map_constants, "DEFAULT_PLACEHOLDER_COLOR_RGB_TK", "gray85"))
|
||||||
w, h = self.canvas.winfo_width() or self.canvas_width, self.canvas.winfo_height() or self.canvas_height
|
w, h = self.canvas.winfo_width() or self.canvas_width, self.canvas.winfo_height() or self.canvas_height
|
||||||
@ -372,8 +400,7 @@ class MapCanvasManager:
|
|||||||
self.map_render_manager.shutdown_worker()
|
self.map_render_manager.shutdown_worker()
|
||||||
|
|
||||||
def recenter_map_at_coords(self, lat: float, lon: float):
|
def recenter_map_at_coords(self, lat: float, lon: float):
|
||||||
if self._current_zoom_gui is not None:
|
if self._current_zoom_gui is not None: self._request_map_render(lat, lon, self._current_zoom_gui)
|
||||||
self._request_map_render(lat, lon, self._current_zoom_gui)
|
|
||||||
|
|
||||||
def set_bbox_around_coords(self, lat: float, lon: float, size_km: float):
|
def set_bbox_around_coords(self, lat: float, lon: float, size_km: float):
|
||||||
bbox_tuple = map_utils.get_bounding_box_from_center_size(lat, lon, size_km)
|
bbox_tuple = map_utils.get_bounding_box_from_center_size(lat, lon, size_km)
|
||||||
@ -389,11 +416,9 @@ class MapCanvasManager:
|
|||||||
self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui - 1)
|
self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui - 1)
|
||||||
|
|
||||||
def pan_map_fixed_step(self, direction: str):
|
def pan_map_fixed_step(self, direction: str):
|
||||||
if not all([self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui, PYPROJ_MODULE_LOCALLY_AVAILABLE, map_utils.pyproj]):
|
if not all([self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui, PYPROJ_MODULE_LOCALLY_AVAILABLE, map_utils.pyproj]): return
|
||||||
return
|
|
||||||
res = map_utils.calculate_meters_per_pixel(self._current_center_lat_gui, self._current_zoom_gui, self.tile_manager.tile_size)
|
res = map_utils.calculate_meters_per_pixel(self._current_center_lat_gui, self._current_zoom_gui, self.tile_manager.tile_size)
|
||||||
if not res:
|
if not res: return
|
||||||
return
|
|
||||||
dx, dy = 0, 0
|
dx, dy = 0, 0
|
||||||
if direction == "left": dx = -self.canvas_width * PAN_STEP_FRACTION
|
if direction == "left": dx = -self.canvas_width * PAN_STEP_FRACTION
|
||||||
elif direction == "right": dx = self.canvas_width * PAN_STEP_FRACTION
|
elif direction == "right": dx = self.canvas_width * PAN_STEP_FRACTION
|
||||||
|
|||||||
@ -7,139 +7,52 @@ from typing import Optional, Tuple
|
|||||||
|
|
||||||
# --- Map Drawing Colors and Styles ---
|
# --- Map Drawing Colors and Styles ---
|
||||||
|
|
||||||
# Colors for drawing boundaries on the map.
|
|
||||||
DEM_BOUNDARY_COLOR: str = "red"
|
|
||||||
DEM_BOUNDARY_THICKNESS_PX: int = 3
|
|
||||||
|
|
||||||
AREA_BOUNDARY_COLOR: str = "blue"
|
AREA_BOUNDARY_COLOR: str = "blue"
|
||||||
AREA_BOUNDARY_THICKNESS_PX: int = 2
|
AREA_BOUNDARY_THICKNESS_PX: int = 2
|
||||||
|
|
||||||
# Colors for text labels drawn on tiles (e.g., DEM tile names).
|
|
||||||
TILE_TEXT_COLOR: str = "white"
|
TILE_TEXT_COLOR: str = "white"
|
||||||
# Use RGBA for transparency in background if supported by PIL/ImageDraw
|
TILE_TEXT_BG_COLOR: str = "rgba(0, 0, 0, 150)"
|
||||||
TILE_TEXT_BG_COLOR: str = "rgba(0, 0, 0, 150)" # Semi-transparent black background
|
|
||||||
|
# --- Graticule (Grid) Constants ---
|
||||||
|
GRATICULE_ENABLED: bool = True
|
||||||
|
GRATICULE_LINE_COLOR: str = "rgba(100, 100, 100, 128)"
|
||||||
|
GRATICULE_LINE_WIDTH: int = 1
|
||||||
|
GRATICULE_LABEL_FONT_SIZE: int = 10
|
||||||
|
GRATICULE_MIN_PIXEL_SPACING: int = 200
|
||||||
|
|
||||||
# --- Map Labeling Constants ---
|
# --- Map Labeling Constants ---
|
||||||
|
|
||||||
# Base font size for map tile labels and the corresponding zoom level.
|
|
||||||
# Font size will be scaled based on the current map zoom relative to this base zoom.
|
|
||||||
DEM_TILE_LABEL_BASE_FONT_SIZE: int = 12
|
|
||||||
DEM_TILE_LABEL_BASE_ZOOM: int = 10
|
|
||||||
|
|
||||||
# Optional: Default font path for drawing text.
|
|
||||||
# If None, ImageFont.load_default() will be used as the primary font source.
|
|
||||||
# If a path is provided, MapCanvasManager/map_drawing might attempt to load
|
|
||||||
# a TrueType font from this path, falling back to default if it fails.
|
|
||||||
# Example: DEFAULT_LABEL_FONT_PATH = "/path/to/your/font.ttf"
|
|
||||||
DEFAULT_LABEL_FONT_PATH: Optional[str] = None
|
DEFAULT_LABEL_FONT_PATH: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# --- Map Behavior Constants ---
|
# --- Map Behavior Constants ---
|
||||||
|
|
||||||
# Default zoom level when the map is initialized or reset if no BBox is set.
|
|
||||||
DEFAULT_INITIAL_ZOOM: int = 7
|
DEFAULT_INITIAL_ZOOM: int = 7
|
||||||
|
|
||||||
# Minimum and maximum allowed zoom levels for interactive pan/zoom.
|
|
||||||
MIN_ZOOM_LEVEL: int = 0
|
MIN_ZOOM_LEVEL: int = 0
|
||||||
# Note: Max zoom is often determined by the map tile service,
|
|
||||||
# this provides a fallback/hard limit if service info isn't available.
|
|
||||||
DEFAULT_MAX_ZOOM_FALLBACK: int = 19
|
DEFAULT_MAX_ZOOM_FALLBACK: int = 19
|
||||||
|
|
||||||
|
|
||||||
# --- Placeholders and Fallbacks ---
|
# --- Placeholders and Fallbacks ---
|
||||||
|
DEFAULT_PLACEHOLDER_COLOR_RGB: Tuple[int, int, int] = (220, 220, 220)
|
||||||
# Default color for placeholder tile images when a tile cannot be loaded.
|
DEFAULT_PLACEHOLDER_COLOR_RGB_TK: str = "gray85"
|
||||||
DEFAULT_PLACEHOLDER_COLOR_RGB: Tuple[int, int, int] = (220, 220, 220) # Light grey
|
|
||||||
|
|
||||||
# MODIFIED: Added constant for Tkinter canvas background color for placeholders.
|
|
||||||
# WHY: Needed by MapCanvasManager to set the canvas background when displaying placeholder text.
|
|
||||||
# HOW: Added the new constant definition.
|
|
||||||
DEFAULT_PLACEHOLDER_COLOR_RGB_TK: str = (
|
|
||||||
"gray85" # Tkinter color string for canvas background
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Map Information Panel Formatting ---
|
# --- Map Information Panel Formatting ---
|
||||||
|
|
||||||
# Number of decimal places to display for coordinates in the info panel.
|
|
||||||
COORDINATE_DECIMAL_PLACES: int = 5
|
COORDINATE_DECIMAL_PLACES: int = 5
|
||||||
MAP_SIZE_KM_DECIMAL_PLACES: int = 1 # Number of decimal places for map size in km.
|
MAP_SIZE_KM_DECIMAL_PLACES: int = 1
|
||||||
|
|
||||||
|
# --- Simboli DMS ---
|
||||||
# Define standard degree, minute, second symbols for DMS formatting
|
|
||||||
DMS_DEGREE_SYMBOL: str = "°"
|
DMS_DEGREE_SYMBOL: str = "°"
|
||||||
DMS_MINUTE_SYMBOL: str = "'"
|
DMS_MINUTE_SYMBOL: str = "'"
|
||||||
DMS_SECOND_SYMBOL: str = "''"
|
DMS_SECOND_SYMBOL: str = "''"
|
||||||
|
|
||||||
# MODIFIED: Paletta colori
|
# Palette colori per le tracce
|
||||||
# WHY: Paletta ampliata
|
|
||||||
# HOW: Ampliata la lista dei colori
|
|
||||||
TRACK_COLOR_PALETTE = [
|
TRACK_COLOR_PALETTE = [
|
||||||
"#FF0000",
|
"#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF",
|
||||||
"#00FF00",
|
"#800000", "#008000", "#000080", "#808000", "#800080", "#008080",
|
||||||
"#0000FF",
|
"#C0C0C0", "#400000", "#004000", "#000040", "#404000", "#400040",
|
||||||
"#FFFF00",
|
"#004040", "#202020", "#FF8000", "#80FF00", "#00FF80", "#8000FF",
|
||||||
"#FF00FF",
|
"#FF0080", "#0080FF", "#808080", "#A0522D", "#D2691E", "#DAA520",
|
||||||
"#00FFFF",
|
"#BDB76B", "#8FBC8F", "#FA8072", "#E9967A", "#F08080", "#CD5C5C",
|
||||||
"#800000",
|
"#DC143C", "#B22222", "#FF69B4", "#FF1493", "#C71585", "#DB7093",
|
||||||
"#008000",
|
"#E6E6FA", "#D8BFD8", "#DDA0DD", "#EE82EE", "#9932CC", "#8A2BE2",
|
||||||
"#000080",
|
"#BA55D3", "#9370DB", "#7B68EE", "#6A5ACD", "#483D8B", "#708090",
|
||||||
"#808000",
|
"#778899", "#B0C4DE", "#ADD8E6", "#87CEEB", "#87CEFA", "#00BFFF",
|
||||||
"#800080",
|
"#1E90FF", "#6495ED", "#4682B4", "#0000CD", "#00008B",
|
||||||
"#008080",
|
|
||||||
"#C0C0C0",
|
|
||||||
"#400000",
|
|
||||||
"#004000",
|
|
||||||
"#000040",
|
|
||||||
"#404000",
|
|
||||||
"#400040",
|
|
||||||
"#004040",
|
|
||||||
"#202020",
|
|
||||||
"#FF8000",
|
|
||||||
"#80FF00",
|
|
||||||
"#00FF80",
|
|
||||||
"#8000FF",
|
|
||||||
"#FF0080",
|
|
||||||
"#0080FF",
|
|
||||||
"#808080",
|
|
||||||
"#A0522D",
|
|
||||||
"#D2691E",
|
|
||||||
"#DAA520",
|
|
||||||
"#BDB76B",
|
|
||||||
"#8FBC8F",
|
|
||||||
"#FA8072",
|
|
||||||
"#E9967A",
|
|
||||||
"#F08080",
|
|
||||||
"#CD5C5C",
|
|
||||||
"#DC143C",
|
|
||||||
"#B22222",
|
|
||||||
"#FF69B4",
|
|
||||||
"#FF1493",
|
|
||||||
"#C71585",
|
|
||||||
"#DB7093",
|
|
||||||
"#E6E6FA",
|
|
||||||
"#D8BFD8",
|
|
||||||
"#DDA0DD",
|
|
||||||
"#EE82EE",
|
|
||||||
"#9932CC",
|
|
||||||
"#8A2BE2",
|
|
||||||
"#BA55D3",
|
|
||||||
"#9370DB",
|
|
||||||
"#7B68EE",
|
|
||||||
"#6A5ACD",
|
|
||||||
"#483D8B",
|
|
||||||
"#708090",
|
|
||||||
"#778899",
|
|
||||||
"#B0C4DE",
|
|
||||||
"#ADD8E6",
|
|
||||||
"#87CEEB",
|
|
||||||
"#87CEFA",
|
|
||||||
"#00BFFF",
|
|
||||||
"#1E90FF",
|
|
||||||
"#6495ED",
|
|
||||||
"#4682B4",
|
|
||||||
"#0000CD",
|
|
||||||
"#00008B",
|
|
||||||
]
|
]
|
||||||
"""
|
|
||||||
Palette di 64 colori da utilizzare per le tracce degli aerei
|
|
||||||
"""
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import logging
|
|||||||
import math
|
import math
|
||||||
from typing import Optional, Tuple, List, Dict, Any
|
from typing import Optional, Tuple, List, Dict, Any
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
import os
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageDraw, ImageFont, ImageColor
|
from PIL import Image, ImageDraw, ImageFont, ImageColor
|
||||||
@ -15,29 +16,26 @@ except ImportError:
|
|||||||
logging.error("MapDrawing: Pillow not found. Drawing disabled.")
|
logging.error("MapDrawing: Pillow not found. Drawing disabled.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .map_utils import geo_to_pixel_on_unscaled_map
|
from .map_utils import geo_to_pixel_on_unscaled_map, calculate_graticule_interval
|
||||||
from ..data.common_models import CanonicalFlightState
|
from ..data.common_models import CanonicalFlightState
|
||||||
from . import map_constants
|
from . import map_constants
|
||||||
from ..data import config as app_config
|
from ..data import config as app_config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
TRACK_LINE_WIDTH = getattr(app_config, "DEFAULT_TRACK_LINE_WIDTH", 2)
|
TRACK_LINE_WIDTH = 2
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.error(f"MapDrawing failed to import dependencies: {e}. Using fallbacks.")
|
logger.error(f"MapDrawing failed to import dependencies: {e}. Using fallbacks.")
|
||||||
# Fallback classes and constants if imports fail
|
|
||||||
class CanonicalFlightState: pass
|
class CanonicalFlightState: pass
|
||||||
class MockMapConstants:
|
class MockMapConstants:
|
||||||
AREA_BOUNDARY_COLOR = "blue"
|
AREA_BOUNDARY_COLOR, TILE_TEXT_BG_COLOR, TILE_TEXT_COLOR = "blue", "rgba(0, 0, 0, 150)", "white"
|
||||||
AREA_BOUNDARY_THICKNESS_PX = 2
|
AREA_BOUNDARY_THICKNESS_PX, DEFAULT_LABEL_FONT_PATH, TRACK_COLOR_PALETTE = 2, None, ["#FF0000"]
|
||||||
TILE_TEXT_BG_COLOR = "rgba(0, 0, 0, 150)"
|
GRATICULE_ENABLED, GRATICULE_LINE_COLOR, GRATICULE_LINE_WIDTH, GRATICULE_LABEL_FONT_SIZE = True, "gray", 1, 10
|
||||||
TILE_TEXT_COLOR = "white"
|
|
||||||
DEFAULT_LABEL_FONT_PATH = None
|
|
||||||
TRACK_COLOR_PALETTE = ["#FF0000", "#00FF00", "#0000FF"]
|
|
||||||
map_constants = MockMapConstants()
|
map_constants = MockMapConstants()
|
||||||
TRACK_LINE_WIDTH = 2
|
TRACK_LINE_WIDTH = 2
|
||||||
def geo_to_pixel_on_unscaled_map(*args, **kwargs): return None
|
def geo_to_pixel_on_unscaled_map(*args, **kwargs): return None
|
||||||
|
def calculate_graticule_interval(span, pixels): return 1.0
|
||||||
|
|
||||||
|
|
||||||
def draw_area_bounding_box(
|
def draw_area_bounding_box(
|
||||||
@ -65,57 +63,94 @@ def draw_area_bounding_box(
|
|||||||
|
|
||||||
if len(pixel_corners) == 4:
|
if len(pixel_corners) == 4:
|
||||||
draw = ImageDraw.Draw(pil_image_to_draw_on)
|
draw = ImageDraw.Draw(pil_image_to_draw_on)
|
||||||
# Close the polygon by adding the first point at the end
|
|
||||||
draw.line(pixel_corners + [pixel_corners[0]], fill=color, width=thickness)
|
draw.line(pixel_corners + [pixel_corners[0]], fill=color, width=thickness)
|
||||||
|
|
||||||
return pil_image_to_draw_on
|
return pil_image_to_draw_on
|
||||||
|
|
||||||
def _load_label_font(scaled_font_size: int) -> Optional[Any]:
|
def draw_graticules(
|
||||||
|
draw: ImageDraw.ImageDraw,
|
||||||
|
map_geo_bounds: Tuple[float, float, float, float],
|
||||||
|
image_size: Tuple[int, int],
|
||||||
|
font: Optional[ImageFont.FreeTypeFont]
|
||||||
|
):
|
||||||
|
if not (PIL_LIB_AVAILABLE_DRAWING and draw and map_geo_bounds):
|
||||||
|
return
|
||||||
|
|
||||||
|
west, south, east, north = map_geo_bounds
|
||||||
|
lon_span, lat_span = east - west, north - south
|
||||||
|
img_width, img_height = image_size
|
||||||
|
|
||||||
|
# MODIFICATO: Passaggio dei parametri corretti alla funzione
|
||||||
|
lon_interval = calculate_graticule_interval(lon_span, img_width)
|
||||||
|
lat_interval = calculate_graticule_interval(lat_span, img_height)
|
||||||
|
|
||||||
|
line_color, line_width = map_constants.GRATICULE_LINE_COLOR, map_constants.GRATICULE_LINE_WIDTH
|
||||||
|
epsilon = 1e-9
|
||||||
|
|
||||||
|
start_lon = math.floor(west / lon_interval) * lon_interval
|
||||||
|
if start_lon < west - epsilon: start_lon += lon_interval
|
||||||
|
|
||||||
|
lon = start_lon
|
||||||
|
while lon < east + epsilon:
|
||||||
|
p1_pixel = geo_to_pixel_on_unscaled_map(north, lon, map_geo_bounds, image_size)
|
||||||
|
p2_pixel = geo_to_pixel_on_unscaled_map(south, lon, map_geo_bounds, image_size)
|
||||||
|
|
||||||
|
if p1_pixel and p2_pixel:
|
||||||
|
draw.line([p1_pixel, p2_pixel], fill=line_color, width=line_width)
|
||||||
|
label = f"{abs(round(lon, 4)):g}°{'E' if lon >= 0 else 'W'}"
|
||||||
|
label_pos = (p1_pixel[0] + 5, 5)
|
||||||
|
if font: draw.text(label_pos, label, font=font, fill=line_color, anchor="lt")
|
||||||
|
|
||||||
|
lon += lon_interval
|
||||||
|
|
||||||
|
start_lat = math.floor(south / lat_interval) * lat_interval
|
||||||
|
if start_lat < south - epsilon: start_lat += lat_interval
|
||||||
|
|
||||||
|
lat = start_lat
|
||||||
|
while lat < north + epsilon:
|
||||||
|
p1_pixel = geo_to_pixel_on_unscaled_map(lat, west, map_geo_bounds, image_size)
|
||||||
|
p2_pixel = geo_to_pixel_on_unscaled_map(lat, east, map_geo_bounds, image_size)
|
||||||
|
|
||||||
|
if p1_pixel and p2_pixel:
|
||||||
|
draw.line([p1_pixel, p2_pixel], fill=line_color, width=line_width)
|
||||||
|
label = f"{abs(round(lat, 4)):g}°{'N' if lat >= 0 else 'S'}"
|
||||||
|
label_pos = (5, p1_pixel[1] - 5)
|
||||||
|
if font: draw.text(label_pos, label, font=font, fill=line_color, anchor="lb")
|
||||||
|
|
||||||
|
lat += lat_interval
|
||||||
|
|
||||||
|
|
||||||
|
def _load_label_font(scaled_font_size: int, is_graticule: bool = False) -> Optional[Any]:
|
||||||
if not (PIL_LIB_AVAILABLE_DRAWING and ImageFont):
|
if not (PIL_LIB_AVAILABLE_DRAWING and ImageFont):
|
||||||
return None
|
return None
|
||||||
scaled_font_size = max(1, scaled_font_size)
|
|
||||||
|
font_size = map_constants.GRATICULE_LABEL_FONT_SIZE if is_graticule else max(1, scaled_font_size)
|
||||||
font_path = getattr(map_constants, "DEFAULT_LABEL_FONT_PATH", None)
|
font_path = getattr(map_constants, "DEFAULT_LABEL_FONT_PATH", None)
|
||||||
if font_path:
|
|
||||||
try:
|
|
||||||
return ImageFont.truetype(font_path, scaled_font_size)
|
|
||||||
except (IOError, OSError):
|
|
||||||
logger.warning(f"Font file not found at {font_path}. Falling back.")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error loading TrueType font {font_path}: {e}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return ImageFont.load_default()
|
if font_path and os.path.exists(font_path): return ImageFont.truetype(font_path, font_size)
|
||||||
except Exception as e:
|
else: return ImageFont.load_default(size=font_size)
|
||||||
logger.error(f"Could not load default PIL font: {e}")
|
except (IOError, OSError): logger.warning(f"Font file not found at {font_path}. Falling back.")
|
||||||
return None
|
except AttributeError: return ImageFont.load_default()
|
||||||
|
except Exception as e: logger.error(f"Error loading font: {e}")
|
||||||
|
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
def _calculate_faded_color(base_color_hex: str, index_from_end: int, total_visible_points: int, min_intensity: float = 0.1) -> Optional[Tuple[int, int, int, int]]:
|
def _calculate_faded_color(base_color_hex: str, index_from_end: int, total_visible_points: int, min_intensity: float = 0.1) -> Optional[Tuple[int, int, int, int]]:
|
||||||
if not (0 <= index_from_end < total_visible_points and ImageColor):
|
if not (0 <= index_from_end < total_visible_points and ImageColor): return None
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
r, g, b = ImageColor.getrgb(base_color_hex)
|
r, g, b = ImageColor.getrgb(base_color_hex)
|
||||||
if total_visible_points <= 1:
|
intensity = min_intensity + (1.0 - min_intensity) * ((total_visible_points - 1 - index_from_end) / (total_visible_points - 1)) if total_visible_points > 1 else 1.0
|
||||||
intensity = 1.0
|
|
||||||
else:
|
|
||||||
ratio = (total_visible_points - 1 - index_from_end) / (total_visible_points - 1)
|
|
||||||
intensity = min_intensity + (1.0 - min_intensity) * ratio
|
|
||||||
return (r, g, b, int(max(0, min(255, intensity * 255))))
|
return (r, g, b, int(max(0, min(255, intensity * 255))))
|
||||||
except Exception:
|
except Exception: return (128, 128, 128, 255)
|
||||||
return (128, 128, 128, 255)
|
|
||||||
|
|
||||||
|
|
||||||
def _draw_single_flight(
|
def _draw_single_flight(
|
||||||
draw: Any,
|
draw: Any, pixel_coords: Tuple[int, int], flight_state: CanonicalFlightState, label_font: Optional[Any],
|
||||||
pixel_coords: Tuple[int, int],
|
track_deque: Optional[deque], current_map_geo_bounds: Optional[Tuple[float, float, float, float]],
|
||||||
flight_state: CanonicalFlightState,
|
|
||||||
label_font: Optional[Any],
|
|
||||||
track_deque: Optional[deque],
|
|
||||||
current_map_geo_bounds: Optional[Tuple[float, float, float, float]],
|
|
||||||
current_stitched_map_pixel_shape: Optional[Tuple[int, int]],
|
current_stitched_map_pixel_shape: Optional[Tuple[int, int]],
|
||||||
):
|
):
|
||||||
if not (PIL_LIB_AVAILABLE_DRAWING and draw and pixel_coords and flight_state):
|
if not (PIL_LIB_AVAILABLE_DRAWING and draw and pixel_coords and flight_state): return
|
||||||
return
|
|
||||||
|
|
||||||
px, py = pixel_coords
|
px, py = pixel_coords
|
||||||
base_length, side_length = 8, 12
|
base_length, side_length = 8, 12
|
||||||
@ -125,25 +160,11 @@ def _draw_single_flight(
|
|||||||
base_color = map_constants.TRACK_COLOR_PALETTE[color_index]
|
base_color = map_constants.TRACK_COLOR_PALETTE[color_index]
|
||||||
|
|
||||||
if track_deque and len(track_deque) > 1:
|
if track_deque and len(track_deque) > 1:
|
||||||
# Usiamo list() per creare una copia su cui iterare in sicurezza
|
pixel_points = [p for p in [geo_to_pixel_on_unscaled_map(pt[0], pt[1], current_map_geo_bounds, current_stitched_map_pixel_shape) for pt in list(track_deque)] if p]
|
||||||
points_to_draw = list(track_deque)
|
|
||||||
pixel_points = [geo_to_pixel_on_unscaled_map(p[0], p[1], current_map_geo_bounds, current_stitched_map_pixel_shape) for p in points_to_draw]
|
|
||||||
pixel_points = [p for p in pixel_points if p is not None] # Filtra i punti non validi
|
|
||||||
|
|
||||||
# Log di debug per la traccia
|
|
||||||
if flight_state.icao24 == '300557': # Mantieni un ICAO di test
|
|
||||||
logger.info(f"--- Drawing track for {flight_state.icao24} with {len(points_to_draw)} points ---")
|
|
||||||
for i, point in enumerate(points_to_draw):
|
|
||||||
logger.info(f" Point {i}: ts={int(point[2])}, lat={point[0]:.4f}, lon={point[1]:.4f}")
|
|
||||||
logger.info("------------------------------------")
|
|
||||||
|
|
||||||
if len(pixel_points) > 1:
|
if len(pixel_points) > 1:
|
||||||
for i in range(len(pixel_points) - 1):
|
for i in range(len(pixel_points) - 1):
|
||||||
start_point = pixel_points[i]
|
|
||||||
end_point = pixel_points[i+1]
|
|
||||||
faded_color = _calculate_faded_color(base_color, len(pixel_points) - 2 - i, len(pixel_points))
|
faded_color = _calculate_faded_color(base_color, len(pixel_points) - 2 - i, len(pixel_points))
|
||||||
if faded_color:
|
if faded_color: draw.line([pixel_points[i], pixel_points[i+1]], fill=faded_color, width=TRACK_LINE_WIDTH)
|
||||||
draw.line([start_point, end_point], fill=faded_color, width=TRACK_LINE_WIDTH)
|
|
||||||
|
|
||||||
angle_rad = math.radians(90 - (flight_state.true_track_deg or 0.0))
|
angle_rad = math.radians(90 - (flight_state.true_track_deg or 0.0))
|
||||||
tip = (px + side_length * math.cos(angle_rad), py - side_length * math.sin(angle_rad))
|
tip = (px + side_length * math.cos(angle_rad), py - side_length * math.sin(angle_rad))
|
||||||
@ -162,15 +183,13 @@ def _draw_single_flight(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error drawing label for '{label_text}': {e}", exc_info=False)
|
logger.error(f"Error drawing label for '{label_text}': {e}", exc_info=False)
|
||||||
|
|
||||||
|
|
||||||
def _draw_text_on_placeholder(draw: Any, image_size: Tuple[int, int], text: str):
|
def _draw_text_on_placeholder(draw: Any, image_size: Tuple[int, int], text: str):
|
||||||
if not (PIL_LIB_AVAILABLE_DRAWING and draw): return
|
if not (PIL_LIB_AVAILABLE_DRAWING and draw): return
|
||||||
img_w, img_h = image_size
|
img_w, img_h = image_size
|
||||||
font = _load_label_font(12)
|
font = _load_label_font(12, is_graticule=True)
|
||||||
if not font:
|
if not font:
|
||||||
draw.text((10, 10), text, fill="black")
|
draw.text((10, 10), text, fill="black")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bbox = draw.textbbox((0, 0), text, font=font, anchor="lt", spacing=4)
|
bbox = draw.textbbox((0, 0), text, font=font, anchor="lt", spacing=4)
|
||||||
text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||||
|
|||||||
@ -8,7 +8,7 @@ try:
|
|||||||
import pyproj
|
import pyproj
|
||||||
PYPROJ_MODULE_LOCALLY_AVAILABLE = True
|
PYPROJ_MODULE_LOCALLY_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pyproj = None # type: ignore
|
pyproj = None
|
||||||
PYPROJ_MODULE_LOCALLY_AVAILABLE = False
|
PYPROJ_MODULE_LOCALLY_AVAILABLE = False
|
||||||
logging.warning("MapUtils: 'pyproj' not found. Geographic calculations will be impaired.")
|
logging.warning("MapUtils: 'pyproj' not found. Geographic calculations will be impaired.")
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ try:
|
|||||||
import mercantile
|
import mercantile
|
||||||
MERCANTILE_MODULE_LOCALLY_AVAILABLE = True
|
MERCANTILE_MODULE_LOCALLY_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
mercantile = None # type: ignore
|
mercantile = None
|
||||||
MERCANTILE_MODULE_LOCALLY_AVAILABLE = False
|
MERCANTILE_MODULE_LOCALLY_AVAILABLE = False
|
||||||
logging.warning("MapUtils: 'mercantile' not found. Tile operations will fail.")
|
logging.warning("MapUtils: 'mercantile' not found. Tile operations will fail.")
|
||||||
|
|
||||||
@ -34,11 +34,9 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
logger.error("MapUtils: map_constants not found. Using mock constants.")
|
logger.error("MapUtils: map_constants not found. Using mock constants.")
|
||||||
class MockMapConstants:
|
class MockMapConstants:
|
||||||
DMS_DEGREE_SYMBOL = "°"
|
DMS_DEGREE_SYMBOL, DMS_MINUTE_SYMBOL, DMS_SECOND_SYMBOL = "°", "'", "''"
|
||||||
DMS_MINUTE_SYMBOL = "'"
|
MIN_ZOOM_LEVEL, DEFAULT_MAX_ZOOM_FALLBACK = 0, 20
|
||||||
DMS_SECOND_SYMBOL = "''"
|
GRATICULE_MIN_PIXEL_SPACING = 80
|
||||||
MIN_ZOOM_LEVEL = 0
|
|
||||||
DEFAULT_MAX_ZOOM_FALLBACK = 20
|
|
||||||
map_constants = MockMapConstants()
|
map_constants = MockMapConstants()
|
||||||
|
|
||||||
class MapCalculationError(Exception):
|
class MapCalculationError(Exception):
|
||||||
@ -46,28 +44,53 @@ class MapCalculationError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _is_valid_bbox_dict(bbox_dict: Dict[str, Any]) -> bool:
|
def _is_valid_bbox_dict(bbox_dict: Dict[str, Any]) -> bool:
|
||||||
if not isinstance(bbox_dict, dict):
|
if not isinstance(bbox_dict, dict): return False
|
||||||
return False
|
|
||||||
required_keys = ["lat_min", "lon_min", "lat_max", "lon_max"]
|
required_keys = ["lat_min", "lon_min", "lat_max", "lon_max"]
|
||||||
if not all(key in bbox_dict for key in required_keys):
|
if not all(key in bbox_dict for key in required_keys): return False
|
||||||
return False
|
|
||||||
try:
|
try:
|
||||||
lat_min, lon_min = float(bbox_dict["lat_min"]), float(bbox_dict["lon_min"])
|
lat_min, lon_min = float(bbox_dict["lat_min"]), float(bbox_dict["lon_min"])
|
||||||
lat_max, lon_max = float(bbox_dict["lat_max"]), float(bbox_dict["lon_max"])
|
lat_max, lon_max = float(bbox_dict["lat_max"]), float(bbox_dict["lon_max"])
|
||||||
if not (-90.0 <= lat_min <= 90.0 and -90.0 <= lat_max <= 90.0 and
|
if not (-90.0 <= lat_min <= 90.0 and -90.0 <= lat_max <= 90.0 and
|
||||||
-180.0 <= lon_min <= 180.0 and -180.0 <= lon_max <= 180.0):
|
-180.0 <= lon_min <= 180.0 and -180.0 <= lon_max <= 180.0):
|
||||||
return False
|
return False
|
||||||
if lat_min >= lat_max or lon_min >= lon_max:
|
if lat_min >= lat_max or lon_min >= lon_max: return False
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError): return False
|
||||||
return False
|
|
||||||
|
def calculate_graticule_interval(
|
||||||
|
span_degrees: float,
|
||||||
|
image_pixel_span: int,
|
||||||
|
min_pixel_spacing: Optional[int] = None
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculates a reasonable interval for graticule lines based on the map's
|
||||||
|
current scale, ensuring a minimum visual separation in pixels.
|
||||||
|
"""
|
||||||
|
if min_pixel_spacing is None:
|
||||||
|
min_pixel_spacing = map_constants.GRATICULE_MIN_PIXEL_SPACING
|
||||||
|
|
||||||
|
if span_degrees <= 0 or image_pixel_span <= 0:
|
||||||
|
return 5.0
|
||||||
|
|
||||||
|
degrees_per_pixel = span_degrees / image_pixel_span
|
||||||
|
min_degree_interval = degrees_per_pixel * min_pixel_spacing
|
||||||
|
|
||||||
|
possible_intervals = [
|
||||||
|
90, 45, 30, 20, 15, 10, 5, 2, 1,
|
||||||
|
0.5, 0.25, 0.1, 0.05, 0.02, 0.01,
|
||||||
|
0.005, 0.002, 0.001, 0.0005, 0.0001
|
||||||
|
]
|
||||||
|
|
||||||
|
for interval in reversed(possible_intervals):
|
||||||
|
if interval >= min_degree_interval:
|
||||||
|
return interval
|
||||||
|
|
||||||
|
return possible_intervals[0]
|
||||||
|
|
||||||
def get_bounding_box_from_center_size(
|
def get_bounding_box_from_center_size(
|
||||||
center_latitude_deg: float, center_longitude_deg: float, area_size_km: float
|
center_latitude_deg: float, center_longitude_deg: float, area_size_km: float
|
||||||
) -> Optional[Tuple[float, float, float, float]]:
|
) -> Optional[Tuple[float, float, float, float]]:
|
||||||
if not PYPROJ_MODULE_LOCALLY_AVAILABLE or not pyproj:
|
if not PYPROJ_MODULE_LOCALLY_AVAILABLE or not pyproj: return None
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
geod = pyproj.Geod(ellps="WGS84")
|
geod = pyproj.Geod(ellps="WGS84")
|
||||||
half_side_m = (area_size_km / 2.0) * 1000.0
|
half_side_m = (area_size_km / 2.0) * 1000.0
|
||||||
@ -83,15 +106,12 @@ def get_bounding_box_from_center_size(
|
|||||||
def get_tile_ranges_for_bbox(
|
def get_tile_ranges_for_bbox(
|
||||||
bounding_box_deg: Tuple[float, float, float, float], zoom_level: int
|
bounding_box_deg: Tuple[float, float, float, float], zoom_level: int
|
||||||
) -> Optional[Tuple[Tuple[int, int], Tuple[int, int]]]:
|
) -> Optional[Tuple[Tuple[int, int], Tuple[int, int]]]:
|
||||||
if not MERCANTILE_MODULE_LOCALLY_AVAILABLE or not mercantile:
|
if not MERCANTILE_MODULE_LOCALLY_AVAILABLE or not mercantile: return None
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
west, south, east, north = bounding_box_deg
|
west, south, east, north = bounding_box_deg
|
||||||
tiles = list(mercantile.tiles(west, south, east, north, zooms=[zoom_level]))
|
tiles = list(mercantile.tiles(west, south, east, north, zooms=[zoom_level]))
|
||||||
if not tiles:
|
if not tiles: return None
|
||||||
return None
|
x_coords, y_coords = [t.x for t in tiles], [t.y for t in tiles]
|
||||||
x_coords = [t.x for t in tiles]
|
|
||||||
y_coords = [t.y for t in tiles]
|
|
||||||
return ((min(x_coords), max(x_coords)), (min(y_coords), max(y_coords)))
|
return ((min(x_coords), max(x_coords)), (min(y_coords), max(y_coords)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error calculating tile ranges: {e}")
|
logger.exception(f"Error calculating tile ranges: {e}")
|
||||||
@ -112,8 +132,7 @@ def calculate_meters_per_pixel(
|
|||||||
def calculate_geographic_bbox_size_km(
|
def calculate_geographic_bbox_size_km(
|
||||||
bounding_box_deg: Tuple[float, float, float, float]
|
bounding_box_deg: Tuple[float, float, float, float]
|
||||||
) -> Optional[Tuple[float, float]]:
|
) -> Optional[Tuple[float, float]]:
|
||||||
if not PYPROJ_MODULE_LOCALLY_AVAILABLE or not pyproj:
|
if not PYPROJ_MODULE_LOCALLY_AVAILABLE or not pyproj: return None
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
west, south, east, north = bounding_box_deg
|
west, south, east, north = bounding_box_deg
|
||||||
geod = pyproj.Geod(ellps="WGS84")
|
geod = pyproj.Geod(ellps="WGS84")
|
||||||
@ -128,6 +147,7 @@ def calculate_zoom_level_for_geographic_size(
|
|||||||
latitude_degrees: float, geographic_span_meters: float, target_pixel_span: int, tile_pixel_size: int = 256
|
latitude_degrees: float, geographic_span_meters: float, target_pixel_span: int, tile_pixel_size: int = 256
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
try:
|
try:
|
||||||
|
if target_pixel_span <= 0: return None
|
||||||
required_res = geographic_span_meters / target_pixel_span
|
required_res = geographic_span_meters / target_pixel_span
|
||||||
EARTH_CIRCUMFERENCE_METERS = 40075016.686
|
EARTH_CIRCUMFERENCE_METERS = 40075016.686
|
||||||
clamped_lat = max(-85.05112878, min(85.05112878, latitude_degrees))
|
clamped_lat = max(-85.05112878, min(85.05112878, latitude_degrees))
|
||||||
@ -199,10 +219,9 @@ def geo_to_pixel_on_unscaled_map(
|
|||||||
|
|
||||||
return (int(round(rel_x * img_w)), int(round(rel_y * img_h)))
|
return (int(round(rel_x * img_w)), int(round(rel_y * img_h)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error converting geo to pixel: {e}", exc_info=True)
|
logger.error(f"Error converting geo to pixel: {e}", exc_info=False)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Rinominata da _pixel_to_geo a pixel_to_geo
|
|
||||||
def pixel_to_geo(
|
def pixel_to_geo(
|
||||||
canvas_x: int,
|
canvas_x: int,
|
||||||
canvas_y: int,
|
canvas_y: int,
|
||||||
@ -221,9 +240,7 @@ def pixel_to_geo(
|
|||||||
map_ul_merc_x, map_ul_merc_y = mercantile.xy(map_west, map_north, truncate=False)
|
map_ul_merc_x, map_ul_merc_y = mercantile.xy(map_west, map_north, truncate=False)
|
||||||
map_lr_merc_x, map_lr_merc_y = mercantile.xy(map_east, map_south, truncate=False)
|
map_lr_merc_x, map_lr_merc_y = mercantile.xy(map_east, map_south, truncate=False)
|
||||||
|
|
||||||
total_merc_w = abs(map_lr_merc_x - map_ul_merc_x)
|
total_merc_w, total_merc_h = abs(map_lr_merc_x - map_ul_merc_x), abs(map_ul_merc_y - map_lr_merc_y)
|
||||||
total_merc_h = abs(map_ul_merc_y - map_lr_merc_y)
|
|
||||||
|
|
||||||
if total_merc_w < 1e-9 or total_merc_h < 1e-9: return None, None
|
if total_merc_w < 1e-9 or total_merc_h < 1e-9: return None, None
|
||||||
|
|
||||||
rel_x, rel_y = canvas_x / img_w, canvas_y / img_h
|
rel_x, rel_y = canvas_x / img_w, canvas_y / img_h
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user