Paint the band the way N1MM paints it
Behind the bandmap scale, the CW part of the band is one shade and the phone part another, with a third for a digital block. That is what N1MM's graphical bandmap does: ResizeBandPanels puts a CW, a Digi and an SSB panel behind the scale, sized from its CWBands, DigiBands and SSBBands tables. Anything past a band edge is shaded red, which is the only place the edges themselves show, and it only appears now that the slice can reach past an edge. The defaults are N1MM's: CW ends at 1840, 3600, 7125, 14150 and so on, and the digital pair is empty until somebody fills it in, exactly as N1MM ships. They are one worldwide set rather than one per ITU region, which is also N1MM's answer — the reason recorded for leaving this out, that the segments differ by region, was wrong about what N1MM holds. 60M and everything above 2M get no entry, because N1MM's own numbers there contradict themselves: its 1.25M CW top is below its phone start, and its 70CM CW top is above the band. Config ▸ Sub bands edits them, one row per band in kilohertz, with a Defaults button. Only the rows that differ are written to the settings file, so a plan nobody has touched leaves nothing behind. BandPlan is an object rather than a static table, so the stored changes layer over the defaults and the bandmap picks up a save without a restart. The numbers are a drawing aid, not a licence. Nothing stops a contact being logged either side of a boundary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,13 @@ public sealed class BandmapView : Control
|
||||
private static readonly IBrush TransmitBrush = new SolidColorBrush(Color.FromRgb(0xC0, 0x39, 0x2B));
|
||||
private static readonly IBrush SecondBrush = new SolidColorBrush(Color.FromRgb(0xE6, 0x7E, 0x22));
|
||||
|
||||
private static readonly IBrush CwBrush = new SolidColorBrush(Color.FromRgb(0x29, 0x80, 0xB9), 0.10);
|
||||
private static readonly IBrush PhoneBrush = new SolidColorBrush(Color.FromRgb(0x27, 0xAE, 0x60), 0.10);
|
||||
private static readonly IBrush DigitalBrush = new SolidColorBrush(Color.FromRgb(0x8E, 0x44, 0xAD), 0.13);
|
||||
|
||||
/// Past the band edge. The slice can reach there when the radio does.
|
||||
private static readonly IBrush OutsideBrush = new SolidColorBrush(Color.FromRgb(0xC0, 0x39, 0x2B), 0.13);
|
||||
|
||||
private readonly Typeface typeface = new(new FontFamily("monospace"));
|
||||
private IReadOnlyList<BandmapLabel> placed = [];
|
||||
private BandmapScale scale = new(Bands.Band20M.Low, Bands.Band20M.High, 1);
|
||||
@@ -42,6 +49,10 @@ public sealed class BandmapView : Control
|
||||
|
||||
public Frequency High { get; set; } = Bands.Band20M.High;
|
||||
|
||||
/// The CW and phone parts of the band, painted behind the scale. Null for a
|
||||
/// band with no boundary in the band plan, and then nothing is painted.
|
||||
public BandSegments? Segments { get; set; }
|
||||
|
||||
/// How the callsign is coloured: the same scorer as the entry window.
|
||||
public Func<Spot, IBrush> ColourOf { get; set; } = _ => Brushes.Gray;
|
||||
|
||||
@@ -58,6 +69,7 @@ public sealed class BandmapView : Control
|
||||
scale = new BandmapScale(Low, High, Math.Max(1, Bounds.Height - (2 * Inset)));
|
||||
IBrush ink = Foreground();
|
||||
context.FillRectangle(Brushes.Transparent, new Rect(Bounds.Size));
|
||||
DrawSegments(context);
|
||||
DrawScale(context, ink);
|
||||
DrawVfos(context);
|
||||
placed = [.. BandmapLayout.Place(Spots, scale, LabelHeight)
|
||||
@@ -68,6 +80,39 @@ public sealed class BandmapView : Control
|
||||
}
|
||||
}
|
||||
|
||||
/// N1MM paints the CW, digital and phone parts of the band behind the
|
||||
/// scale, the digital block over the other two.
|
||||
/// Anything past a band edge is painted as out of band, which is the only
|
||||
/// place the edges themselves show.
|
||||
private void DrawSegments(DrawingContext context)
|
||||
{
|
||||
if (Segments is not { } segments)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Fill(context, segments.CwLow, segments.CwHigh, CwBrush);
|
||||
if (segments.HasPhone)
|
||||
{
|
||||
Fill(context, segments.PhoneLow, segments.PhoneHigh, PhoneBrush);
|
||||
}
|
||||
if (segments.HasDigital)
|
||||
{
|
||||
Fill(context, segments.DigitalLow, segments.DigitalHigh, DigitalBrush);
|
||||
}
|
||||
Fill(context, Low, segments.CwLow, OutsideBrush);
|
||||
Fill(context, segments.PhoneHigh, High, OutsideBrush);
|
||||
}
|
||||
|
||||
private void Fill(DrawingContext context, Frequency from, Frequency to, IBrush brush)
|
||||
{
|
||||
double top = Math.Max(0, scale.YFor(from)) + Inset;
|
||||
double bottom = Math.Min(scale.Height, scale.YFor(to)) + Inset;
|
||||
if (bottom > top)
|
||||
{
|
||||
context.FillRectangle(brush, new Rect(0, top, Bounds.Width, bottom - top));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawScale(DrawingContext context, IBrush ink)
|
||||
{
|
||||
Pen line = new(Opaque(ink, 0.5), 1);
|
||||
|
||||
@@ -49,6 +49,7 @@ public sealed partial class BandmapWindow : RefreshableWindow
|
||||
{
|
||||
View.Spots = [];
|
||||
View.Vfos = [];
|
||||
View.Segments = null;
|
||||
StatusText.Text = session.Position is null ? "no contest is open" : "off band";
|
||||
View.InvalidateVisual();
|
||||
return;
|
||||
@@ -60,10 +61,18 @@ public sealed partial class BandmapWindow : RefreshableWindow
|
||||
View.TickStep = StepFor(high.Hertz - low.Hertz);
|
||||
View.Spots = session.Bandmap.On(band);
|
||||
View.Vfos = Vfos();
|
||||
StatusText.Text = $"{View.Spots.Count} stations · {low.Kilohertz:0.#} to {high.Kilohertz:0.#} kHz";
|
||||
View.Segments = session.BandPlan.For(band);
|
||||
StatusText.Text =
|
||||
$"{View.Spots.Count} stations · {low.Kilohertz:0.#} to {high.Kilohertz:0.#} kHz{Split(band)}";
|
||||
View.InvalidateVisual();
|
||||
}
|
||||
|
||||
/// The band plan boundary, so the two shades behind the scale have a name.
|
||||
private string Split(Band band) =>
|
||||
session.BandPlan.For(band) is { HasPhone: true } segments
|
||||
? $" · CW/phone {segments.CwHigh.Kilohertz:0.#}"
|
||||
: "";
|
||||
|
||||
/// Green for where the operator is listening, red for where the radio
|
||||
/// transmits when working split, orange for the other radio of a two-radio
|
||||
/// station.
|
||||
|
||||
@@ -353,6 +353,24 @@ public sealed partial class EntryWindow
|
||||
: $"{Path.GetFileName(path)} holds no calls we could read");
|
||||
}
|
||||
|
||||
/// The bandmap's CW, digital and phone boundaries.
|
||||
private async void OnSubBandSettings(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
SubBandsDialog dialog = new(session.Settings);
|
||||
Settings? updated = await dialog.ShowDialog<Settings?>(this);
|
||||
if (updated is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
session.Save(updated);
|
||||
Status(updated.SubBands.Count switch
|
||||
{
|
||||
0 => "sub bands back to the defaults",
|
||||
1 => "1 band changed from the defaults",
|
||||
int changed => $"{changed} bands changed from the defaults",
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnDownloadCountryFile(object? sender, RoutedEventArgs e) =>
|
||||
await Download(
|
||||
downloader => downloader.DownloadCountryFileAsync(session.Paths.CountryFile),
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
<MenuItem Header="_Keyer and messages…" Click="OnKeyerSettings" />
|
||||
<Separator />
|
||||
<MenuItem Header="Call _History File…" Click="OnCallHistoryFile" />
|
||||
<MenuItem Header="S_ub bands…" Click="OnSubBandSettings" />
|
||||
<Separator />
|
||||
<MenuItem Header="Download Country _File" Click="OnDownloadCountryFile" />
|
||||
<MenuItem Header="Download Check _Partial File" Click="OnDownloadCallDatabase" />
|
||||
|
||||
Reference in New Issue
Block a user