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:
parent
2e20c48971
commit
2da1d1f245
12 changed files with 1099 additions and 16 deletions
|
|
@ -1560,3 +1560,294 @@ def test_an_airport_cannot_be_mistaken_for_an_aircraft():
|
|||
assert apart > 40, (f"the airport colour is {apart:.0f} units from the "
|
||||
f"nearest altitude colour; under about 25 they read "
|
||||
f"as the same colour")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The flag on the receiver
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_the_background_flies_a_flag_where_the_receiver_was_told_it_is():
|
||||
track = straight()
|
||||
view = fm.fit([track], width=600)
|
||||
middle = ((view.south + view.north) / 2, (view.west + view.east) / 2)
|
||||
plain = fm.background(view, unit="knots")
|
||||
flagged = fm.background(view, unit="knots", home=middle)
|
||||
assert not (plain == fm.HOME).any()
|
||||
assert (flagged == fm.HOME).any(), "no flag was drawn"
|
||||
# The foot of the pole is the position it is pointing at.
|
||||
x, y = view.xy(*middle)
|
||||
assert flagged[y, x] == fm.HOME
|
||||
|
||||
|
||||
def test_no_flag_where_nobody_said_the_receiver_is():
|
||||
"""The middle is otherwise worked out from whatever flew past, which is
|
||||
not a place anybody is standing, and a flag on it would say one is."""
|
||||
track = straight()
|
||||
view = fm.fit([track], width=600)
|
||||
assert not (fm.background(view, unit="knots") == fm.HOME).any()
|
||||
|
||||
|
||||
def test_a_receiver_outside_the_picture_is_not_flagged_at_its_edge():
|
||||
track = straight()
|
||||
view = fm.fit([track], width=600)
|
||||
away = (view.north + 20.0, view.east + 20.0)
|
||||
assert not (fm.background(view, unit="knots", home=away) == fm.HOME).any()
|
||||
|
||||
|
||||
def test_the_animation_flies_the_flag_only_where_it_was_given_a_centre(tmp_path):
|
||||
track = straight()
|
||||
middle = (track.fixes[0].latitude, track.fixes[0].longitude)
|
||||
told = fm.animate([track], tmp_path / "told.png", ground=False,
|
||||
airports=False, centre=middle, radius_nm=200.0)
|
||||
guessed = fm.animate([track], tmp_path / "guessed.png", ground=False,
|
||||
airports=False)
|
||||
assert told is not None and guessed is not None
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
def has_red(path):
|
||||
picture = np.array(Image.open(path).convert("RGB"))
|
||||
return bool(((picture[:, :, 0] == fm.HOME_RED[0])
|
||||
& (picture[:, :, 1] == fm.HOME_RED[1])
|
||||
& (picture[:, :, 2] == fm.HOME_RED[2])).any())
|
||||
|
||||
assert has_red(told.path)
|
||||
assert not has_red(guessed.path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The line from a label to its aircraft
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_a_dashed_line_has_gaps_in_it():
|
||||
img = np.full((40, 80), fm.BG, dtype=np.uint8)
|
||||
fm._dashed(img, 5, 20, 74, 20, fm.LEADER)
|
||||
row = img[20, 5:75]
|
||||
assert (row == fm.LEADER).any(), "nothing was drawn"
|
||||
assert (row == fm.BG).any(), "no gaps: that is a solid line"
|
||||
# On and off in the lengths asked for, near enough to the pixel.
|
||||
runs, last, count = [], row[0], 0
|
||||
for value in row:
|
||||
if value == last:
|
||||
count += 1
|
||||
else:
|
||||
runs.append((last, count))
|
||||
last, count = value, 1
|
||||
runs.append((last, count))
|
||||
on = [n for value, n in runs[1:-1] if value == fm.LEADER]
|
||||
assert on and all(4 <= n <= 6 for n in on), on
|
||||
|
||||
|
||||
def test_a_dash_is_the_same_length_whichever_way_the_line_runs():
|
||||
"""Stepped along the line rather than along whichever axis is longer,
|
||||
so a nearly-horizontal leader and a nearly-vertical one come out the
|
||||
same instead of one of them turning into a dotted line."""
|
||||
import math
|
||||
|
||||
drawn = []
|
||||
for x1, y1 in ((79, 21), (21, 39), (79, 39)):
|
||||
img = np.full((40, 80), fm.BG, dtype=np.uint8)
|
||||
fm._dashed(img, 1, 1, x1, y1, fm.LEADER)
|
||||
length = math.hypot(x1 - 1, y1 - 1)
|
||||
drawn.append((img == fm.LEADER).sum() / length)
|
||||
assert max(drawn) - min(drawn) < 0.25, drawn
|
||||
|
||||
|
||||
def test_a_leader_stops_at_the_aircraft_rather_than_running_past_it():
|
||||
"""Walked along the line's own length, so the last step lands on the
|
||||
far end. Walked along whichever axis is longer instead, a diagonal
|
||||
overshoots by half as much again and the leader carries on past the
|
||||
aeroplane it was drawn to point at."""
|
||||
img = np.full((80, 80), fm.BG, dtype=np.uint8)
|
||||
fm._dashed(img, 10, 10, 50, 50, fm.LEADER)
|
||||
ys, xs = np.where(img == fm.LEADER)
|
||||
assert len(xs), "nothing was drawn"
|
||||
assert xs.max() <= 50 and ys.max() <= 50, \
|
||||
f"ran past the end to ({xs.max()}, {ys.max()})"
|
||||
assert xs.min() >= 10 and ys.min() >= 10
|
||||
# And it does reach it, near enough to the pixel.
|
||||
assert xs.max() >= 46 and ys.max() >= 46
|
||||
|
||||
|
||||
def test_a_leader_that_goes_nowhere_draws_nothing():
|
||||
img = np.full((40, 80), fm.BG, dtype=np.uint8)
|
||||
fm._dashed(img, 20, 20, 20, 20, fm.LEADER)
|
||||
assert not (img == fm.LEADER).any()
|
||||
|
||||
|
||||
def test_the_animation_ties_a_label_to_its_aircraft():
|
||||
"""The window has always had a leader; here there was nothing at all,
|
||||
and a label pushed out into one of the rings by a crowd had nothing
|
||||
tying it to the aeroplane it was about."""
|
||||
track = straight()
|
||||
view = fm.fit([track], width=800, box=(50.0, -3.0, 52.0, 3.0))
|
||||
base = fm.background(view, unit="knots")
|
||||
img = base.copy()
|
||||
fm._label(img, 300, 200, track, track.fixes[0], fm.RAMP, [], "knots",
|
||||
entry=_Entry())
|
||||
assert (img == fm.LEADER).any(), "no leader was drawn"
|
||||
|
||||
|
||||
def test_the_animation_leader_is_not_the_aircrafts_own_colour():
|
||||
"""A solid line running out of an aeroplane in the colour of the path
|
||||
behind it reads as more path; so does a line of any pattern in that
|
||||
colour, and this one is neither."""
|
||||
from bandsaunter.flightmap import LEADER, PALETTE, RAMP, RAMP_STEPS
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_the_leader_fades_with_the_label_it_belongs_to():
|
||||
track = straight()
|
||||
view = fm.fit([track], width=800, box=(50.0, -3.0, 52.0, 3.0))
|
||||
base = fm.background(view, unit="knots")
|
||||
|
||||
def leader_shade(strength):
|
||||
img = base.copy()
|
||||
fm._label(img, 300, 200, track, track.fixes[0], fm.RAMP, [], "knots",
|
||||
entry=_Entry(), strength=strength)
|
||||
drawn = {int(v) for v in np.unique(img[img != base])}
|
||||
return {v for v in drawn if fm.LEADER <= v <= fm.LEADER + 2}
|
||||
|
||||
assert leader_shade(1.0) == {fm.LEADER}
|
||||
faded = leader_shade(0.45)
|
||||
assert faded and fm.LEADER not in faded, faded
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Range rings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ring_view(width=400):
|
||||
track = straight()
|
||||
return fm.fit([track], width=width, box=(50.0, -3.0, 52.0, 3.0))
|
||||
|
||||
|
||||
def test_the_rings_lift_the_ground_more_the_nearer_the_middle_they_are():
|
||||
"""Concentric and translucent, so they stack: three lifts inside the
|
||||
innermost, two in the next, one in the outer, none beyond."""
|
||||
view = _ring_view()
|
||||
home = ((view.south + view.north) / 2, (view.west + view.east) / 2)
|
||||
ground = np.zeros((view.height, view.width), dtype=np.uint8)
|
||||
img = fm.background(view, unit="knots", ground=ground, home=home,
|
||||
rings=60.0)
|
||||
lift = fm.RING_LIFT
|
||||
counts = [int((img == fm.GROUND + lift * n).sum()) for n in range(4)]
|
||||
assert all(counts), f"not every ring was drawn: {counts}"
|
||||
# Each ring is an annulus further out than the last, so it covers more
|
||||
# of the picture than the one inside it.
|
||||
assert counts[3] < counts[2] < counts[1], counts
|
||||
|
||||
|
||||
def test_the_rings_stack_on_a_picture_with_no_map_under_it():
|
||||
"""With no map there is nothing but background to lift, and a pixel
|
||||
the outer disc has lifted has to count as ground for the next one --
|
||||
otherwise every ring lands on bare background and they all come out the
|
||||
same shade."""
|
||||
view = _ring_view()
|
||||
home = ((view.south + view.north) / 2, (view.west + view.east) / 2)
|
||||
img = fm.background(view, unit="knots", home=home, rings=60.0)
|
||||
lift = fm.RING_LIFT
|
||||
shades = [int((img == fm.GROUND + lift * n).sum()) for n in (1, 2, 3)]
|
||||
assert all(shades), f"the rings did not stack: {shades}"
|
||||
assert shades[0] > shades[1] > shades[2], shades
|
||||
|
||||
|
||||
def test_the_rings_stop_at_the_radius_they_were_given():
|
||||
view = _ring_view()
|
||||
home = ((view.south + view.north) / 2, (view.west + view.east) / 2)
|
||||
ground = np.zeros((view.height, view.width), dtype=np.uint8)
|
||||
img = fm.background(view, unit="knots", ground=ground, home=home,
|
||||
rings=60.0)
|
||||
away = fm._distance_field(
|
||||
home,
|
||||
view.north - (view.north - view.south) * (np.arange(view.height) + 0.5)
|
||||
/ view.height,
|
||||
view.west + (view.east - view.west) * (np.arange(view.width) + 0.5)
|
||||
/ view.width)
|
||||
# The view sits inside the canvas, below the title and inside the
|
||||
# margins, so the distance field lines up with that part of it.
|
||||
patch = img[view.top:view.top + view.height,
|
||||
view.left:view.left + view.width]
|
||||
# Ground only: the flag is drawn over the middle of the innermost ring
|
||||
# and is not ground that was lifted.
|
||||
ground = (patch >= fm.GROUND) & (patch < fm.GROUND + fm.GROUND_SHADES)
|
||||
lifted = ground & (patch > fm.GROUND)
|
||||
beyond = away > 60.0 * 0.75 + 1.0
|
||||
assert not (lifted & beyond).any(), \
|
||||
"the ground beyond the outer ring was lifted"
|
||||
assert (lifted & (away < 60.0 * 0.25)).any(), "the innermost ring is missing"
|
||||
|
||||
|
||||
def test_no_rings_without_a_radius_or_without_a_position():
|
||||
view = _ring_view()
|
||||
home = ((view.south + view.north) / 2, (view.west + view.east) / 2)
|
||||
ground = np.zeros((view.height, view.width), dtype=np.uint8)
|
||||
plain = fm.background(view, unit="knots", ground=ground)
|
||||
assert np.array_equal(
|
||||
fm.background(view, unit="knots", ground=ground, home=None,
|
||||
rings=60.0), plain)
|
||||
flagged = fm.background(view, unit="knots", ground=ground, home=home)
|
||||
assert (flagged == fm.HOME).any() # the flag, but no rings
|
||||
assert not (flagged == fm.GROUND + fm.RING_LIFT).any()
|
||||
|
||||
|
||||
def _labels_drawn(**over):
|
||||
"""Every string the drawing was asked to stamp, in order.
|
||||
|
||||
Read by watching the drawing rather than by looking for the letters in
|
||||
the picture afterwards: a picture with a ring on it has large areas of
|
||||
one flat colour, and searching those for a pattern of pixels finds
|
||||
whatever it is asked for.
|
||||
"""
|
||||
view = _ring_view(width=700)
|
||||
home = ((view.south + view.north) / 2, (view.west + view.east) / 2)
|
||||
said = []
|
||||
real = fm.draw_text
|
||||
|
||||
def watch(img, x, y, text, colour):
|
||||
said.append(text)
|
||||
return real(img, x, y, text, colour)
|
||||
|
||||
fm.draw_text = watch
|
||||
try:
|
||||
fm.background(view, home=home, rings=60.0, **over)
|
||||
finally:
|
||||
fm.draw_text = real
|
||||
return said
|
||||
|
||||
|
||||
def test_each_ring_is_labelled_with_how_far_out_it_is():
|
||||
said = _labels_drawn(unit="knots")
|
||||
for nm in (15, 30, 45):
|
||||
assert f"{nm} NM" in said, (nm, said)
|
||||
|
||||
|
||||
def test_the_ring_labels_are_in_the_unit_the_rest_of_the_picture_uses():
|
||||
"""Miles an hour beside a ring measured in nautical miles would be two
|
||||
different miles on one picture."""
|
||||
said = _labels_drawn(unit="mph")
|
||||
assert "17 MI" in said, said # 15 nm, written as statute
|
||||
assert not any(word.endswith(" NM") for word in said), said
|
||||
|
||||
|
||||
def test_the_rings_leave_the_aerodromes_and_the_grid_alone():
|
||||
"""They only lift pixels that are still map or still empty."""
|
||||
view = _ring_view()
|
||||
home = ((view.south + view.north) / 2, (view.west + view.east) / 2)
|
||||
airports = [("EGLL", home[0], home[1] + 0.2)]
|
||||
img = fm.background(view, unit="knots", airports=airports, home=home,
|
||||
rings=300.0)
|
||||
assert (img == fm.AIRPORT).any(), "the aerodrome was washed out"
|
||||
assert (img == fm.GRID).any(), "the grid was washed out"
|
||||
|
||||
|
||||
def test_a_distance_field_measures_from_the_place_it_was_given():
|
||||
lats = np.array([51.0, 52.0])
|
||||
lons = np.array([-1.0, -1.0])
|
||||
away = fm._distance_field((51.0, -1.0), lats, lons)
|
||||
assert away[0, 0] == pytest.approx(0.0, abs=0.01)
|
||||
# A degree of latitude is sixty nautical miles, near enough.
|
||||
assert away[1, 0] == pytest.approx(60.0, abs=0.5)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -259,3 +259,61 @@ def test_the_brightness_setting_still_does_something_on_a_vector_theme():
|
|||
fm.set_theme("phosphor")
|
||||
assert int(fm.dim_ground(levels, 1.0).max()) > \
|
||||
int(fm.dim_ground(levels, 0.3).max())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The leader line, and the flag on the receiver
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_no_theme_lets_a_leader_line_be_mistaken_for_a_flight_path():
|
||||
"""The whole reason it stopped being drawn in the aircraft's colour: a
|
||||
straight line running out of an aeroplane, in the colour of the path
|
||||
behind that aeroplane, reads as more path."""
|
||||
for name in themes.THEMES:
|
||||
fm.set_theme(name)
|
||||
leader = _lab(fm.PALETTE[fm.LEADER])
|
||||
apart = min(float(np.linalg.norm(leader - _lab(fm.PALETTE[fm.RAMP + i])))
|
||||
for i in range(fm.RAMP_STEPS))
|
||||
assert apart > 25, f"{name}: only {apart:.0f} units from a trail"
|
||||
|
||||
|
||||
def test_the_receiver_flag_is_red_whatever_the_theme_is():
|
||||
""""You are here" is the one mark on the picture whose meaning must not
|
||||
change with the colours, so it does not take the theme's."""
|
||||
for name in themes.THEMES:
|
||||
fm.set_theme(name)
|
||||
assert tuple(fm.PALETTE[fm.HOME]) == fm.HOME_RED
|
||||
|
||||
|
||||
def test_the_flag_stands_clear_of_the_aircraft_on_every_theme():
|
||||
"""Including the red one, which is the hard case and the reason the
|
||||
flag is pure red rather than a softer one: a softened red 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."""
|
||||
for name in themes.THEMES:
|
||||
fm.set_theme(name)
|
||||
red = _lab(fm.PALETTE[fm.HOME])
|
||||
apart = min(float(np.linalg.norm(red - _lab(fm.PALETTE[fm.RAMP + i])))
|
||||
for i in range(fm.RAMP_STEPS))
|
||||
assert apart > 20, f"{name}: only {apart:.0f} units from an aircraft"
|
||||
|
||||
|
||||
def test_the_flag_stands_on_the_spot_rather_than_covering_it():
|
||||
"""The foot of the pole is the position. A blob would put the position
|
||||
somewhere inside itself."""
|
||||
img = np.full((40, 40), fm.BG, dtype=np.uint8)
|
||||
fm.draw_home(img, 20, 34)
|
||||
assert img[34, 20] == fm.HOME, "the pole does not stand on the spot"
|
||||
assert img[35, 20] == fm.BG, "something is drawn below the position"
|
||||
assert (img[34, :20] == fm.BG).all(), "the flag reaches left of the pole"
|
||||
# The pennant flies up and to the right, and nothing else does.
|
||||
top = 34 - fm.HOME_POLE
|
||||
assert (img[top, 21:21 + fm.HOME_FLY] == fm.HOME).all()
|
||||
assert (img[34 - 1, 21:] == fm.BG).all(), "the pennant hangs to the foot"
|
||||
|
||||
|
||||
def test_the_flag_off_the_edge_of_the_picture_paints_nothing_absurd():
|
||||
for x, y in ((-50, 20), (20, -50), (200, 20), (20, 200)):
|
||||
img = np.full((40, 40), fm.BG, dtype=np.uint8)
|
||||
fm.draw_home(img, x, y) # must not raise or wrap
|
||||
assert img.shape == (40, 40)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue