Tell the boxes from the flight paths, and say how far out things are

Three changes to what is on the picture, and both drawings got all three.

The line from an information box to its aircraft was drawn in the
aircraft's own colour, which made it the same colour as that aircraft's
trail: a straight solid line running out of an aeroplane, in the colour of
the path behind the aeroplane, reads as more path, and on a busy picture
that is a heading nobody flew.  It has its own colour now, and is dashed.
Qt measures a dash pattern in multiples of the pen's width, so each pass of
the glow divides the pattern by its own width; without that the halo's
dashes are three times the core's and the line comes out as beads.

The animation had no such line at all, which only came out when the two
were held up against each other: a label pushed into one of the outward
rings by a crowd had nothing tying it to the aeroplane it was about.  It has
one now, walked along the line's own length rather than along whichever axis
is longer, so that a nearly horizontal leader and a nearly vertical one get
dashes of the same length and neither runs past the aircraft it points at.

A red flag stands where the receiver was told it is standing, from the
coordinates in the settings.  The foot of the pole is the position and the
pennant flies up and to the right, so nothing the flag is made of covers the
place it points at.  It is pure red in every theme: that is the one mark on
the picture whose meaning must not change with the colours, and pure red is
both the brightest red there is and the one furthest from every altitude
colour -- a softer one sat close enough to a low aeroplane on the default
map, and to a mid-altitude one on the red theme, to be taken for one.  It is
drawn only where a position was actually given, since a middle worked out
from whatever flew past is not a place anybody is standing.

And the range rings: faint discs at a quarter, a half and three quarters of
the radius, concentric on the receiver and labelled with the distance.  They
are translucent and they stack, so the ground inside the innermost is lifted
three times and the outer once, which gives a sense of how far away a thing
is without measuring anything.  An indexed picture cannot blend, so
translucent there means moving the ground under the disc a step or two up
its own ramp of shades, which keeps the coastline and the roads visible
through it.  Separate settings for the two, because a picture is studied and
a window is glanced at.

Two bugs found by the tests rather than by looking.  The rings were built
across the whole canvas instead of the part of it the map is drawn on, so
they came out stretched by the height of the title bar -- four per cent,
which is invisible and wrong.  And a radius of zero killed the window: the
projection collapses, every pixel maps to one point, and the graticule asks
for lines across a span of nothing until Qt aborts.  The program never
passes a zero, but a caller could, and a crash is not an answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
The Dust Council 2026-09-06 11:49:48 -07:00
parent 2e20c48971
commit 2da1d1f245
12 changed files with 1099 additions and 16 deletions

View file

@ -1645,3 +1645,201 @@ def test_a_vector_theme_lays_a_halo_round_what_it_draws(app):
finally:
fm.set_theme("night")
assert glowing > plain, f"{glowing} shades is no more than {plain}"
# ---------------------------------------------------------------------------
# The leader line, and the flag on the receiver
# ---------------------------------------------------------------------------
def _leader_pixels(view) -> int:
"""How much of the picture the leader line accounts for.
Counted by drawing the view twice, once with the leader colour set to
the background, and taking the difference. Matching the colour itself
finds almost nothing: the line is smoothed and drawn with an alpha, so
hardly a pixel of it comes out the pure colour.
"""
from bandsaunter.flightmap import BG, LEADER, PALETTE
was = PALETTE[LEADER].copy()
try:
PALETTE[LEADER] = PALETTE[BG]
hidden = _rendered(view)
finally:
PALETTE[LEADER] = was
shown = _rendered(view)
return int((hidden != shown).any(axis=2).sum())
@qt
def test_the_leader_is_not_drawn_in_the_aircrafts_own_colour(app):
"""Drawn in the aircraft's colour it came out the same colour as that
aircraft's trail, and a straight line running out of an aeroplane in
the colour of the path behind it reads as more path."""
from bandsaunter.flightmap import LEADER, PALETTE, RAMP, RAMP_STEPS
from bandsaunter.livemap import SkyView
view = SkyView(a_sky(a_blip()))
view.show_ground = False
assert _leader_pixels(view) > 10, "no leader was drawn at all"
leader = tuple(int(v) for v in PALETTE[LEADER])
ramp = {tuple(int(v) for v in PALETTE[RAMP + i]) for i in range(RAMP_STEPS)}
assert leader not in ramp
@qt
def test_the_leader_is_dashed_rather_than_solid(app):
"""The same line with the dashes taken out paints noticeably more of
itself, which is what a dashed line is."""
from bandsaunter import livemap as lm
from bandsaunter.livemap import SkyView
def drawn(pattern):
was, lm.LEADER_DASH = lm.LEADER_DASH, pattern
try:
view = SkyView(a_sky(a_blip()))
view.show_ground = False
return _leader_pixels(view)
finally:
lm.LEADER_DASH = was
dashed = drawn(lm.LEADER_DASH)
solid = drawn(None)
assert solid > 10, "the solid line drew nothing to compare against"
assert dashed < solid * 0.85, \
f"{dashed} pixels against {solid}: that is not a dashed line"
def test_a_dash_pattern_is_scaled_by_the_width_of_the_pen_drawing_it():
"""Qt measures a dash pattern in multiples of the pen's own width. A
glowing line is the same line drawn two or three times at different
widths, so without this its halo would have dashes three times the
length of its core and it would come out as beads."""
from bandsaunter.livemap import dash_for
for width in (1.0, 3.4, 5.8):
pattern = dash_for((5.0, 4.0), width)
assert [round(step * width, 6) for step in pattern] == [5.0, 4.0]
def test_a_dash_pattern_never_asks_for_a_step_of_nothing():
"""Qt refuses a zero-length dash, and a pen can be asked for at any
width the glow happens to want."""
from bandsaunter.livemap import dash_for
for width in (0.0, -3.0, 1e6):
assert all(step > 0 for step in dash_for((5.0, 4.0), width))
@qt
def test_a_flag_stands_where_the_receiver_was_told_it_is(app):
from bandsaunter.flightmap import HOME_RED
from bandsaunter.livemap import SkyView
def flag_pixels(view):
picture = _rendered(view)
return int(((picture[:, :, 2] == HOME_RED[0])
& (picture[:, :, 1] == HOME_RED[1])
& (picture[:, :, 0] == HOME_RED[2])).sum())
placed = SkyView(a_sky(a_blip(), home=(32.4325, -111.0841)))
placed.show_ground = False
assert flag_pixels(placed) > 20, "no flag where the receiver is"
@qt
def test_no_flag_where_nobody_said_the_receiver_is(app):
"""The middle is otherwise worked out from whatever flew past, which is
not a place anybody is standing."""
from bandsaunter.flightmap import HOME_RED
from bandsaunter.livemap import SkyView
view = SkyView(a_sky(a_blip(), home=None))
view.show_ground = False
picture = _rendered(view)
assert int(((picture[:, :, 2] == HOME_RED[0])
& (picture[:, :, 1] == HOME_RED[1])
& (picture[:, :, 0] == HOME_RED[2])).sum()) == 0
def test_both_pictures_draw_the_leader_and_the_flag_the_same():
"""They are meant to look like the same program, and two copies of a
number are two chances to change only one of them."""
from bandsaunter import flightmap as fm
from bandsaunter import livemap as lm
assert tuple(lm.LEADER_DASH) == tuple(float(x) for x in fm.LEADER_DASH)
# Both read the one palette, so there is no second colour to drift.
assert tuple(fm.PALETTE[fm.LEADER]) == tuple(fm.PALETTE[fm.LEADER])
assert tuple(fm.PALETTE[fm.HOME]) == fm.HOME_RED
# And the one set of measurements for the flag itself.
for name in ("HOME_POLE", "HOME_FLY", "HOME_DROP"):
assert hasattr(fm, name)
# ---------------------------------------------------------------------------
# Range rings on the window
# ---------------------------------------------------------------------------
def _lifted_pixels(view) -> int:
"""How much of the window the rings account for, by drawing it with and
without them. The tint is an alpha over whatever is underneath, so
there is no one colour to count."""
was = view.sky.show_rings
try:
view.sky.show_rings = False
without = _rendered(view)
view.sky.show_rings = True
with_them = _rendered(view)
finally:
view.sky.show_rings = was
return int((without != with_them).any(axis=2).sum())
def test_a_sky_does_not_draw_rings_unless_it_is_asked_to():
assert a_sky().show_rings is False
assert Sky(rings=True).show_rings is True
@qt
def test_the_window_draws_the_range_rings(app):
from bandsaunter.livemap import SkyView
view = SkyView(a_sky(a_blip(), rings=True))
view.show_ground = False
assert _lifted_pixels(view) > 5000, "no rings were drawn"
@qt
def test_no_rings_without_a_position_or_without_a_radius(app):
"""They are measured from the radius and centred on the receiver, so
they mean nothing without both."""
from bandsaunter.livemap import SkyView
nowhere = SkyView(a_sky(a_blip(), rings=True, home=None))
nowhere.show_ground = False
assert _lifted_pixels(nowhere) == 0
flat = SkyView(a_sky(a_blip(), rings=True, radius_nm=0.0))
flat.show_ground = False
assert _lifted_pixels(flat) == 0
def test_the_rings_are_labelled_with_how_far_out_they_are():
"""Both pictures ask the same function for the wording, so the window
and the drawings cannot come to different numbers for the same ring."""
from bandsaunter.flightmap import ring_labels
assert ring_labels(100.0, "knots") == [(25.0, "25 nm"), (50.0, "50 nm"),
(75.0, "75 nm")]
def test_the_ring_labels_are_in_the_unit_the_rest_of_the_window_uses():
"""Miles an hour beside a ring measured in nautical miles would be two
different miles on one picture."""
from bandsaunter.flightmap import ring_labels
assert [text for _nm, text in ring_labels(104.0, "mph")] == \
["30 mi", "60 mi", "90 mi"]
assert [text for _nm, text in ring_labels(100.0, "kph")] == \
["46 km", "93 km", "139 km"]