f530185e31
Implements draw_aircraft() with heading arrow (filled/outlined for MLAT), callsign+altitude label, and a 5-dot fade trail. Adds 5 tests covering all ACs; all 94 tests pass, ruff lint and format clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
83 lines
2.2 KiB
Python
83 lines
2.2 KiB
Python
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, colour, aircraft.is_mlat)
|
|
_draw_label(draw, cx, cy, aircraft, colour)
|