Mark the aerodromes in the window, and draw the lot on a vector display
The aerodromes were never on the window at all: only the animation drew them, and the window's map had whatever airport glyphs the tiles happened to carry. They are drawn there now, in the same colour and the same square. The first attempt at that had a bug worth naming, because it would have looked like the feature simply not working. They were fetched inside the same pass of the fetching loop as a piece of map, so they queued behind a hundred and twenty tiles coming off a network -- and once the map was in hand there were no more passes, so they were never fetched again. They are their own question now, asked on the same thread but not behind the tiles, and they arrive whether the tiles do or not. An area that has been asked about and has none in it is remembered as none, rather than asked about again five times a second for the rest of the night. And the thread now starts if either the map or the aerodromes are wanted, so --no-basemap no longer quietly takes the airports with it. Then the themes, which change the window and the animated pictures together because both read their colours out of the same palette. night is what this program has always drawn and is untouched. digital, phosphor, amber and red are the screens the phrase "air defence display" actually calls to mind: a black tube, one phosphor, and thin bright vector lines with a halo round them. Three things follow from having one colour to spend, and they are constraints rather than decoration. Height becomes brightness, since hue is no longer free -- low is dim and high burns, which is the trade those displays made. The map underneath drops to about a quarter of the brightness asked for, because a tinted photograph of a county behind the vectors is the one thing that stops a vector display looking like one. And a country is named in two letters rather than drawn as a flag, a flag being half a dozen colours. The glow is done twice, differently, because the two are different kinds of picture. The window lays each line down two or three times, wider and fainter each pass, with the core last: trails, symbols, leader lines, box borders and the aerodrome squares. The animation cannot blend at all, a GIF being indexed colour, so it dilates what it has drawn and fills the halo with the dimmed copy of the colour underneath -- and the aircraft colours already had dimmed copies, since those are the trail shades, so an aeroplane glows into the colour its own trail is drawn in, which is the colour a phosphor would have spread into. The fixed colours get two rings each in the palette for the purpose. The halo goes over the map, the grid and the background and over nothing else that was drawn, since a halo is what light does to the dark around a line; where two rings meet the nearer wins. It costs about 55 ms a frame at 1400 by 1258 and the default theme skips the pass entirely. The palette is written over in place rather than replaced, because both drawings and every one of their helpers hold a reference to that array and a new one would leave half the program painting in the colours of the theme before. There is a test that every theme keeps the aerodrome colour more than forty units of CIELAB from every altitude colour, stated as the distance rather than as the colour, so that a new theme cannot quietly walk an aircraft back into the airports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
parent
f9b21f7e35
commit
2e20c48971
11 changed files with 1342 additions and 50 deletions
|
|
@ -1356,3 +1356,292 @@ def test_the_category_reaches_the_window_from_the_air():
|
|||
craft = Aircraft(icao="AE07D3", callsign="PRIME04", latitude=32.5,
|
||||
longitude=-111.0, category="heavy")
|
||||
assert blip_for(craft).category == "heavy"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The aerodromes under the window
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TUCSON_AIRPORTS = [("KTUS", 32.116, -110.941), ("KDMA", 32.166, -110.883),
|
||||
("KAVQ", 32.409, -111.218)]
|
||||
|
||||
|
||||
def test_a_sky_does_not_reach_for_airports_unless_it_is_asked_to():
|
||||
"""A question to a network the first time an area is drawn, so the
|
||||
program turns it on and a library user has to say so on purpose."""
|
||||
assert a_sky().show_airports is False
|
||||
assert Sky(airports=True).show_airports is True
|
||||
|
||||
|
||||
def test_the_airports_come_back_when_what_was_fetched_covers_the_view():
|
||||
sky = a_sky()
|
||||
sky.set_airports(TUCSON_AIRPORTS, (31.0, -112.0, 33.0, -110.0))
|
||||
assert sky.airports_covering(31.5, -111.5, 32.5, -110.5) == TUCSON_AIRPORTS
|
||||
|
||||
|
||||
def test_a_view_outside_what_was_fetched_asks_again():
|
||||
"""None, not an empty list: nothing has been asked about this piece of
|
||||
world, which is a different thing from having asked and found none."""
|
||||
sky = a_sky()
|
||||
sky.set_airports(TUCSON_AIRPORTS, (31.0, -112.0, 33.0, -110.0))
|
||||
assert sky.airports_covering(40.0, -112.0, 41.0, -110.0) is None
|
||||
assert a_sky().airports_covering(31.5, -111.5, 32.5, -110.5) is None
|
||||
|
||||
|
||||
def test_an_area_with_no_aerodromes_is_not_asked_about_all_night():
|
||||
"""An empty answer is an answer. Kept as an empty list, so that the
|
||||
difference between "there are none" and "nobody has looked" survives."""
|
||||
sky = a_sky()
|
||||
sky.want_airports((31.0, -112.0, 33.0, -110.0))
|
||||
sky.set_airports([], (31.0, -112.0, 33.0, -110.0))
|
||||
assert sky.airports_covering(31.5, -111.5, 32.5, -110.5) == []
|
||||
assert sky.wanted_airports() is None
|
||||
|
||||
|
||||
def test_the_aerodromes_do_not_wait_behind_the_tiles():
|
||||
"""The bug this is here for. They used to be fetched only in the same
|
||||
pass as a piece of map, so they queued behind a hundred and twenty tiles
|
||||
coming off a network -- and once the map was in hand there were no more
|
||||
passes and they were never fetched at all."""
|
||||
import threading
|
||||
|
||||
from bandsaunter import basemap
|
||||
|
||||
sky = a_sky()
|
||||
sky.show_airports = True
|
||||
sky.want_airports((32.0, -112.0, 33.0, -110.0))
|
||||
real = basemap.airports_in
|
||||
slow = threading.Event()
|
||||
|
||||
def tiles(*a, **kw): # a map that never arrives
|
||||
slow.wait(10.0)
|
||||
return None
|
||||
|
||||
basemap.airports_in = lambda s, w, n, e, **kw: [
|
||||
{"code": c, "latitude": la, "longitude": lo}
|
||||
for c, la, lo in TUCSON_AIRPORTS]
|
||||
try:
|
||||
# A map is asked for too, and the fetching of it hangs.
|
||||
sky.want_ground("k", (32.0, -112.0, 33.0, -110.0), (40, 40))
|
||||
worker = threading.Thread(
|
||||
target=livemap.fetch_ground, args=(sky,),
|
||||
kwargs={"fetch": tiles}, daemon=True)
|
||||
worker.start()
|
||||
for _ in range(60):
|
||||
if sky.airports_covering(32.0, -112.0, 33.0, -110.0):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
got = sky.airports_covering(32.0, -112.0, 33.0, -110.0)
|
||||
slow.set()
|
||||
sky.stopping = True
|
||||
worker.join(timeout=3.0)
|
||||
finally:
|
||||
basemap.airports_in = real
|
||||
slow.set()
|
||||
assert got == TUCSON_AIRPORTS, "they waited for a map that never came"
|
||||
|
||||
|
||||
def test_the_airports_are_fetched_off_the_painting_thread():
|
||||
import threading
|
||||
|
||||
from bandsaunter import basemap
|
||||
|
||||
sky = a_sky()
|
||||
sky.show_airports = True
|
||||
sky.want_airports((32.0, -112.0, 33.0, -110.0))
|
||||
real = basemap.airports_in
|
||||
basemap.airports_in = lambda s, w, n, e, **kw: [
|
||||
{"code": c, "latitude": la, "longitude": lo}
|
||||
for c, la, lo in TUCSON_AIRPORTS]
|
||||
try:
|
||||
worker = threading.Thread(
|
||||
target=livemap.fetch_ground, args=(sky,),
|
||||
kwargs={"fetch": lambda z, x, y, **kw: None}, daemon=True)
|
||||
worker.start()
|
||||
for _ in range(60):
|
||||
if sky.airports_covering(32.0, -112.0, 33.0, -110.0):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
sky.stopping = True
|
||||
worker.join(timeout=2.0)
|
||||
finally:
|
||||
basemap.airports_in = real
|
||||
assert sky.airports_covering(32.0, -112.0, 33.0, -110.0) == TUCSON_AIRPORTS
|
||||
|
||||
|
||||
def test_an_area_that_could_not_be_asked_about_is_not_asked_about_again():
|
||||
import threading
|
||||
|
||||
from bandsaunter import basemap
|
||||
|
||||
sky = a_sky()
|
||||
sky.show_airports = True
|
||||
sky.want_airports((32.0, -112.0, 33.0, -110.0))
|
||||
real = basemap.airports_in
|
||||
tries = []
|
||||
|
||||
def broken(*a, **kw):
|
||||
tries.append(1)
|
||||
raise OSError("no network tonight")
|
||||
|
||||
basemap.airports_in = broken
|
||||
try:
|
||||
worker = threading.Thread(
|
||||
target=livemap.fetch_ground, args=(sky,),
|
||||
kwargs={"fetch": lambda z, x, y, **kw: None}, daemon=True)
|
||||
worker.start()
|
||||
time.sleep(0.6)
|
||||
sky.stopping = True
|
||||
worker.join(timeout=2.0)
|
||||
finally:
|
||||
basemap.airports_in = real
|
||||
assert len(tries) == 1, f"asked {len(tries)} times after failing once"
|
||||
assert sky.airports_covering(32.0, -112.0, 33.0, -110.0) == []
|
||||
|
||||
|
||||
def test_no_airports_are_fetched_when_they_are_turned_off():
|
||||
import threading
|
||||
|
||||
from bandsaunter import basemap
|
||||
|
||||
sky = a_sky() # show_airports is False
|
||||
sky.want_airports((32.0, -112.0, 33.0, -110.0))
|
||||
real = basemap.airports_in
|
||||
|
||||
def refuse(*a, **kw):
|
||||
raise AssertionError("asked for airports with them turned off")
|
||||
|
||||
basemap.airports_in = refuse
|
||||
try:
|
||||
worker = threading.Thread(
|
||||
target=livemap.fetch_ground, args=(sky,),
|
||||
kwargs={"fetch": lambda z, x, y, **kw: None}, daemon=True)
|
||||
worker.start()
|
||||
time.sleep(0.5)
|
||||
sky.stopping = True
|
||||
worker.join(timeout=2.0)
|
||||
finally:
|
||||
basemap.airports_in = real
|
||||
assert sky.airports_covering(32.0, -112.0, 33.0, -110.0) is None
|
||||
|
||||
|
||||
def _airport_pixels(picture) -> int:
|
||||
"""Pixels in the aerodrome colour, which nothing else on the window is."""
|
||||
from bandsaunter.flightmap import AIRPORT, PALETTE
|
||||
|
||||
want = PALETTE[AIRPORT]
|
||||
return int(((picture[:, :, 2] == want[0]) & (picture[:, :, 1] == want[1])
|
||||
& (picture[:, :, 0] == want[2])).sum())
|
||||
|
||||
|
||||
@qt
|
||||
def test_the_window_marks_the_aerodromes_under_it(app):
|
||||
from bandsaunter.livemap import SkyView
|
||||
|
||||
sky = a_sky(a_blip(), airports=True)
|
||||
view = SkyView(sky)
|
||||
view.show_ground = False
|
||||
bare = _rendered(view)
|
||||
assert _airport_pixels(bare) == 0, "something else is already that colour"
|
||||
projection = view.projection()
|
||||
sky.set_airports(TUCSON_AIRPORTS,
|
||||
(projection.south - 1, projection.west - 1,
|
||||
projection.north + 1, projection.east + 1))
|
||||
marked = _rendered(view)
|
||||
assert _airport_pixels(marked) > 60, "no aerodrome was drawn"
|
||||
|
||||
|
||||
@qt
|
||||
def test_the_window_draws_no_aerodromes_when_they_are_turned_off(app):
|
||||
from bandsaunter.livemap import SkyView
|
||||
|
||||
sky = a_sky(a_blip()) # show_airports is False
|
||||
view = SkyView(sky)
|
||||
view.show_ground = False
|
||||
projection = view.projection()
|
||||
sky.set_airports(TUCSON_AIRPORTS,
|
||||
(projection.south - 1, projection.west - 1,
|
||||
projection.north + 1, projection.east + 1))
|
||||
assert _airport_pixels(_rendered(view)) == 0
|
||||
|
||||
|
||||
@qt
|
||||
def test_an_aerodrome_outside_the_view_is_nowhere_on_the_picture(app):
|
||||
"""What is fetched covers rather more world than is shown, so on a
|
||||
zoomed-in view most of the county's airports are outside it.
|
||||
|
||||
Projecting one gives coordinates off the widget and Qt clips them, so
|
||||
this holds whether or not they are skipped first; it is here to catch
|
||||
anyone who later clamps a position into the view instead, which would
|
||||
pile every airport in the county along the border of the picture.
|
||||
"""
|
||||
from bandsaunter.livemap import SkyView
|
||||
|
||||
sky = a_sky(a_blip(), airports=True)
|
||||
view = SkyView(sky)
|
||||
view.show_ground = False
|
||||
projection = view.projection()
|
||||
box = (projection.south - 5, projection.west - 5,
|
||||
projection.north + 5, projection.east + 5)
|
||||
sky.set_airports([("KJFK", projection.north + 3, projection.east + 3)], box)
|
||||
assert _airport_pixels(_rendered(view)) == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The window follows the theme
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@qt
|
||||
def test_the_window_draws_in_whatever_theme_is_set(app):
|
||||
"""Both drawings read the same palette, so setting a theme changes the
|
||||
window as well as the animation and there is nothing to keep in step."""
|
||||
from bandsaunter import flightmap as fm
|
||||
from bandsaunter.livemap import SkyView
|
||||
|
||||
def colours(theme):
|
||||
fm.set_theme(theme)
|
||||
sky = a_sky(a_blip(), airports=True)
|
||||
view = SkyView(sky)
|
||||
view.show_ground = False
|
||||
projection = view.projection()
|
||||
sky.set_airports([("KTUS", projection.south + 0.1,
|
||||
projection.west + 0.1)],
|
||||
(projection.south - 1, projection.west - 1,
|
||||
projection.north + 1, projection.east + 1))
|
||||
picture = _rendered(view)
|
||||
return {tuple(int(v) for v in rgb) for rgb in
|
||||
picture[:, :, [2, 1, 0]].reshape(-1, 3)}
|
||||
|
||||
try:
|
||||
night = colours("night")
|
||||
green = colours("phosphor")
|
||||
finally:
|
||||
fm.set_theme("night")
|
||||
assert (255, 64, 200) in night, "the night aerodrome colour is missing"
|
||||
assert (255, 64, 200) not in green, "a magenta aerodrome on a green screen"
|
||||
assert (255, 184, 72) in green, "the phosphor aerodrome colour is missing"
|
||||
|
||||
|
||||
@qt
|
||||
def test_a_vector_theme_lays_a_halo_round_what_it_draws(app):
|
||||
"""The window does its glow by laying the same line down two or three
|
||||
times, wider and fainter each pass. So a themed window paints more
|
||||
distinct colours than a plain one drawing the same aircraft."""
|
||||
from bandsaunter import flightmap as fm
|
||||
from bandsaunter.livemap import SkyView
|
||||
|
||||
def shades(theme):
|
||||
fm.set_theme(theme)
|
||||
sky = a_sky(a_blip())
|
||||
view = SkyView(sky)
|
||||
view.show_ground = False
|
||||
picture = _rendered(view)
|
||||
return len({tuple(int(v) for v in rgb) for rgb in
|
||||
picture[:, :, [2, 1, 0]].reshape(-1, 3)})
|
||||
|
||||
try:
|
||||
plain = shades("night")
|
||||
glowing = shades("phosphor")
|
||||
finally:
|
||||
fm.set_theme("night")
|
||||
assert glowing > plain, f"{glowing} shades is no more than {plain}"
|
||||
|
|
|
|||
261
tests/test_themes.py
Normal file
261
tests/test_themes.py
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
"""The colour themes, and the glow that goes with the vector ones.
|
||||
|
||||
A theme changes both drawings at once, because both read their colours out
|
||||
of the same palette: the animation looks indices up in it directly and the
|
||||
window asks it for a QColor. So the tests here are mostly about that
|
||||
palette -- that it is rewritten rather than replaced, that every theme keeps
|
||||
the aerodromes distinguishable from the aircraft, and that the halo goes
|
||||
where light would go and nowhere else.
|
||||
"""
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bandsaunter import flightmap as fm
|
||||
from bandsaunter import themes
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def back_to_night():
|
||||
"""Every test leaves the program drawing the way it found it."""
|
||||
yield
|
||||
fm.set_theme(themes.DEFAULT_THEME)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Choosing one
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_every_theme_says_what_it_is():
|
||||
for name, theme in themes.THEMES.items():
|
||||
assert theme.name == name
|
||||
assert theme.summary and not theme.summary.endswith(".")
|
||||
assert len(theme.stops) >= 2
|
||||
assert theme.stops[0][0] == 0.0
|
||||
|
||||
|
||||
def test_a_theme_can_be_asked_for_by_name_or_by_what_it_looks_like():
|
||||
assert themes.theme_named("phosphor").name == "phosphor"
|
||||
assert themes.theme_named("green").name == "phosphor"
|
||||
assert themes.theme_named("wargames").name == "digital"
|
||||
assert themes.theme_named("BLUE").name == "digital"
|
||||
assert themes.theme_named(" amber ").name == "amber"
|
||||
|
||||
|
||||
def test_a_name_nobody_recognises_is_the_default_rather_than_a_refusal():
|
||||
"""A theme is how the picture looks. Refusing to draw an evening's
|
||||
flying because of a misspelt colour would be the wrong trade."""
|
||||
assert themes.theme_named("puce").name == themes.DEFAULT_THEME
|
||||
assert themes.theme_named("").name == themes.DEFAULT_THEME
|
||||
assert themes.theme_named(None).name == themes.DEFAULT_THEME
|
||||
|
||||
|
||||
def test_no_two_themes_share_a_name_or_an_alias():
|
||||
seen = set()
|
||||
for theme in themes.THEMES.values():
|
||||
for word in (theme.name,) + tuple(theme.aliases):
|
||||
assert word not in seen, word
|
||||
seen.add(word)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# What setting one does to the palette
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_the_palette_is_written_over_rather_than_replaced():
|
||||
"""Both drawings and every one of their helpers hold a reference to
|
||||
this array. A new one would leave half the program painting in the
|
||||
colours of the theme before."""
|
||||
before = fm.PALETTE
|
||||
fm.set_theme("phosphor")
|
||||
assert fm.PALETTE is before
|
||||
assert tuple(fm.PALETTE[fm.BG]) != (14, 16, 22)
|
||||
|
||||
|
||||
def test_setting_a_theme_says_which_one_it_settled_on():
|
||||
assert fm.set_theme("norad").name == "digital"
|
||||
assert fm.set_theme("puce").name == themes.DEFAULT_THEME
|
||||
|
||||
|
||||
def test_the_default_theme_draws_exactly_what_it_always_drew():
|
||||
"""The colours this program has always used, unchanged: an existing
|
||||
recording redrawn today has to come out the same picture."""
|
||||
fm.set_theme("phosphor")
|
||||
fm.set_theme("night")
|
||||
assert tuple(fm.PALETTE[fm.BG]) == (14, 16, 22)
|
||||
assert tuple(fm.PALETTE[fm.INK]) == (196, 204, 218)
|
||||
assert tuple(fm.PALETTE[fm.RAMP]) == (252, 96, 72)
|
||||
assert tuple(fm.PALETTE[fm.RAMP + fm.RAMP_STEPS - 1]) == (158, 142, 255)
|
||||
|
||||
|
||||
def _lab(rgb):
|
||||
c = np.asarray(rgb, float) / 255.0
|
||||
c = np.where(c > 0.04045, ((c + 0.055) / 1.055) ** 2.4, c / 12.92)
|
||||
m = np.array([[0.4124, 0.3576, 0.1805], [0.2126, 0.7152, 0.0722],
|
||||
[0.0193, 0.1192, 0.9505]])
|
||||
xyz = (c @ m.T) / np.array([0.9505, 1.0, 1.089])
|
||||
f = np.where(xyz > 0.008856, np.cbrt(xyz), 7.787 * xyz + 16 / 116)
|
||||
return np.array([116 * f[1] - 16, 500 * (f[0] - f[1]), 200 * (f[1] - f[2])])
|
||||
|
||||
|
||||
def test_no_theme_lets_an_aerodrome_be_mistaken_for_an_aircraft():
|
||||
"""The whole reason the aerodromes stopped being amber. Stated as the
|
||||
distance rather than as the colour, so that a new theme cannot quietly
|
||||
walk an aircraft back into the airports."""
|
||||
for name in themes.THEMES:
|
||||
fm.set_theme(name)
|
||||
airport = _lab(fm.PALETTE[fm.AIRPORT])
|
||||
apart = min(float(np.linalg.norm(airport - _lab(fm.PALETTE[fm.RAMP + i])))
|
||||
for i in range(fm.RAMP_STEPS))
|
||||
assert apart > 40, f"{name}: only {apart:.0f} units apart"
|
||||
|
||||
|
||||
def test_a_phosphor_theme_reads_height_as_brightness():
|
||||
"""One colour to spend, so it cannot be spent on hue. Low is dim and
|
||||
high burns, which is the constraint those screens actually had."""
|
||||
for name in ("digital", "phosphor", "amber", "red"):
|
||||
theme = fm.set_theme(name)
|
||||
assert theme.height_is_brightness
|
||||
weights = [int(fm.PALETTE[fm.RAMP + i].astype(int).sum())
|
||||
for i in range(fm.RAMP_STEPS)]
|
||||
assert weights == sorted(weights), f"{name} is not monotonic"
|
||||
assert weights[-1] > weights[0] * 3
|
||||
|
||||
|
||||
def test_the_default_theme_reads_height_as_hue_instead():
|
||||
fm.set_theme("night")
|
||||
assert not fm.THEME.height_is_brightness
|
||||
|
||||
|
||||
def test_every_theme_fills_the_palette_without_running_off_the_end():
|
||||
for name in themes.THEMES:
|
||||
fm.set_theme(name)
|
||||
assert fm.PALETTE.shape == (256, 3)
|
||||
assert fm.GLOW + 6 <= 256
|
||||
# Nothing drawn with is left as the black the unused tail is.
|
||||
for index in (fm.INK, fm.AIRPORT, fm.RAMP, fm.GROUND + 31):
|
||||
assert fm.PALETTE[index].any(), (name, index)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The glow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _picture(width=80, height=60):
|
||||
return np.full((height, width), fm.BG, dtype=np.uint8)
|
||||
|
||||
|
||||
def test_the_default_theme_has_no_glow_at_all():
|
||||
fm.set_theme("night")
|
||||
img = _picture()
|
||||
img[30, 10:70] = fm.RAMP + 20
|
||||
assert np.array_equal(fm.bloom(img), img)
|
||||
|
||||
|
||||
def test_a_line_on_a_vector_theme_gets_a_halo_either_side_of_it():
|
||||
fm.set_theme("phosphor")
|
||||
img = _picture()
|
||||
img[30, 10:70] = fm.RAMP + 20
|
||||
out = fm.bloom(img)
|
||||
assert (out[30, 10:70] == fm.RAMP + 20).all(), "the core was painted over"
|
||||
assert (out[29, 10:70] == fm.TRAIL + 20).all(), "no halo above the line"
|
||||
assert (out[31, 10:70] == fm.TRAIL + 20).all(), "no halo below it"
|
||||
assert (out[28, 10:70] == fm.OLD + 20).all(), "no second, fainter ring"
|
||||
assert (out[27, 10:70] == fm.BG).all(), "the halo reaches too far"
|
||||
|
||||
|
||||
def test_the_halo_is_the_colour_of_the_thing_that_cast_it():
|
||||
"""A green aeroplane glows green and a red one red: the halo is the
|
||||
aircraft's own dimmed colour, which is what a phosphor would spread."""
|
||||
fm.set_theme("digital")
|
||||
for step in (0, 12, 31):
|
||||
img = _picture()
|
||||
img[30, 10:70] = fm.RAMP + step
|
||||
out = fm.bloom(img)
|
||||
assert (out[29, 10:70] == fm.TRAIL + step).all()
|
||||
|
||||
|
||||
def test_a_halo_never_paints_over_something_else_that_was_drawn():
|
||||
"""A halo is what light does to the dark around a line. Painting it
|
||||
over another line would be light doing something light does not do."""
|
||||
fm.set_theme("phosphor")
|
||||
img = _picture()
|
||||
img[30, 10:70] = fm.RAMP + 20 # an aeroplane
|
||||
img[29, 10:70] = fm.AIRPORT # an aerodrome right beside it
|
||||
out = fm.bloom(img)
|
||||
assert (out[29, 10:70] == fm.AIRPORT).all()
|
||||
|
||||
|
||||
def test_the_halo_does_not_wrap_round_the_edge_of_the_picture():
|
||||
"""Rolled and then cut: without the cut, a line down the left edge
|
||||
would glow on the right edge of the picture."""
|
||||
fm.set_theme("amber")
|
||||
img = _picture()
|
||||
img[:, 0] = fm.RAMP + 20
|
||||
out = fm.bloom(img)
|
||||
assert (out[:, 1] == fm.TRAIL + 20).all()
|
||||
assert (out[:, -1] == fm.BG).all()
|
||||
assert (out[:, -2] == fm.BG).all()
|
||||
|
||||
tall = _picture()
|
||||
tall[0, :] = fm.AIRPORT
|
||||
assert (fm.bloom(tall)[-1, :] == fm.BG).all()
|
||||
|
||||
|
||||
def test_the_halo_goes_over_the_map_and_the_grid_but_not_the_aircraft():
|
||||
fm.set_theme("digital")
|
||||
img = np.full((60, 80), fm.GROUND + 10, dtype=np.uint8)
|
||||
img[30, 40] = fm.INK
|
||||
out = fm.bloom(img)
|
||||
assert out[30, 41] != fm.GROUND + 10, "no halo over the map"
|
||||
assert out[30, 40] == fm.INK
|
||||
|
||||
|
||||
def test_the_nearer_ring_wins_where_two_meet():
|
||||
"""Which is what happens on the tube as well."""
|
||||
fm.set_theme("phosphor")
|
||||
img = _picture()
|
||||
img[30, 40] = fm.RAMP + 20
|
||||
out = fm.bloom(img)
|
||||
assert out[31, 40] == fm.TRAIL + 20 # near
|
||||
assert out[32, 40] == fm.OLD + 20 # far
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# What else a vector theme changes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_a_phosphor_theme_names_the_country_instead_of_drawing_its_flag():
|
||||
"""A flag is half a dozen colours and a phosphor screen has one. Two
|
||||
letters are what a display of the period would have done anyway."""
|
||||
from bandsaunter.flags import FLAG_H, FLAG_W
|
||||
|
||||
fm.set_theme("night")
|
||||
flagged = np.full((40, 60), fm.BG, dtype=np.uint8)
|
||||
fm.draw_flag(flagged, 5, 5, "US")
|
||||
patch = flagged[5:5 + FLAG_H, 5:5 + FLAG_W]
|
||||
assert ((patch >= fm.FLAG) & (patch < fm.FLAG + 12)).any()
|
||||
|
||||
fm.set_theme("phosphor")
|
||||
lettered = np.full((40, 60), fm.BG, dtype=np.uint8)
|
||||
fm.draw_flag(lettered, 5, 5, "US")
|
||||
patch = lettered[5:5 + FLAG_H, 5:5 + FLAG_W]
|
||||
assert not ((patch >= fm.FLAG) & (patch < fm.FLAG + 12)).any()
|
||||
assert (lettered == fm.DIM).any(), "the letters were not drawn either"
|
||||
|
||||
|
||||
def test_a_vector_theme_pushes_the_map_underneath_well_back():
|
||||
"""A tinted photograph of a county behind the vectors is the one thing
|
||||
that stops a vector display looking like one."""
|
||||
levels = np.full((8, 8), fm.GROUND_SHADES - 1, dtype=np.uint8)
|
||||
fm.set_theme("night")
|
||||
plain = int(fm.dim_ground(levels, 0.7).max())
|
||||
fm.set_theme("digital")
|
||||
quiet = int(fm.dim_ground(levels, 0.7).max())
|
||||
assert quiet < plain * 0.6, f"{quiet} is not much darker than {plain}"
|
||||
|
||||
|
||||
def test_the_brightness_setting_still_does_something_on_a_vector_theme():
|
||||
levels = np.full((8, 8), fm.GROUND_SHADES - 1, dtype=np.uint8)
|
||||
fm.set_theme("phosphor")
|
||||
assert int(fm.dim_ground(levels, 1.0).max()) > \
|
||||
int(fm.dim_ground(levels, 0.3).max())
|
||||
Loading…
Add table
Add a link
Reference in a new issue