from __future__ import annotations import collections import math from PIL import Image, ImageDraw, ImageFont from planemapper.constants import COLOUR_TRAIL, TRAIL_DOT_SIZE_MAX, TRAIL_DOT_SIZE_MIN from planemapper.models import Aircraft from planemapper.renderer.colours import altitude_to_colour def _rotate_point(x: float, y: float, angle_deg: float) -> tuple[float, float]: r = math.radians(angle_deg) return (x * math.cos(r) - y * math.sin(r), x * math.sin(r) + y * math.cos(r)) def _draw_arrow( draw: ImageDraw.ImageDraw, cx: int, cy: int, heading: float, colour: tuple[int, int, int], is_mlat: bool, ) -> None: tip = _rotate_point(0, -12, heading) left = _rotate_point(-6, 8, heading) right = _rotate_point(6, 8, heading) pts = [ (cx + tip[0], cy + tip[1]), (cx + left[0], cy + left[1]), (cx + right[0], cy + right[1]), ] if is_mlat: draw.polygon(pts, fill=None, outline=colour) else: draw.polygon(pts, fill=colour) def _draw_label( draw: ImageDraw.ImageDraw, cx: int, cy: int, aircraft: Aircraft, colour: tuple[int, int, int], ) -> None: font = ImageFont.load_default() if aircraft.callsign: text = f"{aircraft.callsign}\n{aircraft.altitude_ft}ft" else: text = f"{aircraft.altitude_ft}ft" draw.text((cx + 12, cy - 8), text, fill=colour, font=font) def _draw_trail( draw: ImageDraw.ImageDraw, trail: collections.deque[tuple[int, int]], ) -> None: n = len(trail) if n == 0: return for i, (tx, ty) in enumerate(trail): if n > 1: size = int(TRAIL_DOT_SIZE_MAX - i * (TRAIL_DOT_SIZE_MAX - TRAIL_DOT_SIZE_MIN) / (n - 1)) else: size = TRAIL_DOT_SIZE_MAX r = max(size // 2, 1) draw.ellipse((tx - r, ty - r, tx + r, ty + r), fill=COLOUR_TRAIL) def draw_aircraft( image: Image.Image, aircraft: Aircraft, pos: tuple[int, int], trail: collections.deque[tuple[int, int]], ) -> None: cx, cy = pos colour = altitude_to_colour(aircraft.altitude_ft) draw = ImageDraw.Draw(image) _draw_trail(draw, trail) _draw_arrow(draw, cx, cy, aircraft.heading or 0.0, colour, aircraft.is_mlat) _draw_label(draw, cx, cy, aircraft, colour)