Draw the bandmap the way N1MM draws it
The bandmap was a list of frequencies and callsigns. It is now a frequency scale with the stations written out beside it and a leader line from each callsign back to where the station really is. BandmapLayout does the placing and has no UI reference, so the rule is unit tested: put the label centred on its frequency, and if it would cover the one above, push it below instead. That is N1MM's CalculateOffset, and it is what keeps three stations a hundred hertz apart readable. The scale stops short of the top and bottom edges, or the first and last frequency numbers come out cut in half. VfoMarker draws the receiver as a bar as wide as the mode passes, rather than a line. Only the receiver is fed: the radio does not report a transmit VFO or a second radio yet, so VfoRole has the other two roles ready and nothing draws them. Band-plan colouring of the scale is left out because the segments differ by ITU region and there is no band-plan table to read them from. Checked by running the program against a fake cluster node under Xvfb and looking at the result: twelve spots, three of them stacked with fanned leaders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
6
src/Nonemm.Spotting/BandmapLabel.cs
Normal file
6
src/Nonemm.Spotting/BandmapLabel.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Nonemm.Spotting;
|
||||
|
||||
/// One callsign on the bandmap. `MarkY` is where the station really is on the
|
||||
/// scale and `LabelY` is the top of the text, which is pushed down when the
|
||||
/// station above is too close. The leader line joins the two.
|
||||
public sealed record BandmapLabel(Spot Spot, double MarkY, double LabelY);
|
||||
52
src/Nonemm.Spotting/BandmapLayout.cs
Normal file
52
src/Nonemm.Spotting/BandmapLayout.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Spotting;
|
||||
|
||||
/// Works out where each callsign goes. Two stations a hundred hertz apart would
|
||||
/// have their text on top of each other, so a label that would cover the one
|
||||
/// above is pushed below it and a leader line runs back to the real frequency.
|
||||
/// This is what N1MM does.
|
||||
public static class BandmapLayout
|
||||
{
|
||||
/// N1MM leaves this much between two labels it had to separate.
|
||||
private const double Gap = 0.5;
|
||||
|
||||
public static IReadOnlyList<BandmapLabel> Place(
|
||||
IEnumerable<Spot> spots,
|
||||
BandmapScale scale,
|
||||
double labelHeight)
|
||||
{
|
||||
List<BandmapLabel> placed = [];
|
||||
double? above = null;
|
||||
foreach (Spot spot in spots
|
||||
.Where(s => scale.Shows(s.Frequency))
|
||||
.OrderBy(s => s.Frequency.Hertz))
|
||||
{
|
||||
double markY = scale.YFor(spot.Frequency);
|
||||
double labelY = markY - (labelHeight / 2);
|
||||
if (above is { } previous && previous + labelHeight > labelY)
|
||||
{
|
||||
labelY = previous + labelHeight + Gap;
|
||||
}
|
||||
placed.Add(new BandmapLabel(spot, markY, labelY));
|
||||
above = labelY;
|
||||
}
|
||||
return placed;
|
||||
}
|
||||
|
||||
/// The frequency ticks to draw, every `step` from the first round frequency
|
||||
/// at or below the top of the scale.
|
||||
public static IReadOnlyList<Frequency> Ticks(BandmapScale scale, Frequency step)
|
||||
{
|
||||
List<Frequency> ticks = [];
|
||||
long first = scale.Low.Hertz - (scale.Low.Hertz % step.Hertz);
|
||||
for (long at = first; at <= scale.High.Hertz; at += step.Hertz)
|
||||
{
|
||||
if (at >= scale.Low.Hertz)
|
||||
{
|
||||
ticks.Add(Frequency.FromHertz(at));
|
||||
}
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
}
|
||||
19
src/Nonemm.Spotting/BandmapScale.cs
Normal file
19
src/Nonemm.Spotting/BandmapScale.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Spotting;
|
||||
|
||||
/// The slice of a band the bandmap is showing, and how it maps to pixels.
|
||||
/// Frequency runs down the window, so `Low` is at the top.
|
||||
public sealed record BandmapScale(Frequency Low, Frequency High, double Height)
|
||||
{
|
||||
public long Span => Math.Max(1, High.Hertz - Low.Hertz);
|
||||
|
||||
public double YFor(Frequency frequency) =>
|
||||
(frequency.Hertz - Low.Hertz) * Height / Span;
|
||||
|
||||
public Frequency At(double y) =>
|
||||
Frequency.FromHertz(Low.Hertz + (long)Math.Round(y * Span / Math.Max(1, Height)));
|
||||
|
||||
public bool Shows(Frequency frequency) =>
|
||||
frequency >= Low && frequency <= High;
|
||||
}
|
||||
15
src/Nonemm.Spotting/VfoMarker.cs
Normal file
15
src/Nonemm.Spotting/VfoMarker.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Spotting;
|
||||
|
||||
/// Where a receiver or transmitter is sitting, drawn on the bandmap as a bar
|
||||
/// covering what it hears or occupies rather than a single line.
|
||||
public sealed record VfoMarker(Frequency Low, Frequency High, VfoRole Role)
|
||||
{
|
||||
/// The bar for a receiver on `centre`, as wide as the mode it is in.
|
||||
public static VfoMarker Centred(Frequency centre, Frequency width, VfoRole role) =>
|
||||
new(
|
||||
Frequency.FromHertz(centre.Hertz - (width.Hertz / 2)),
|
||||
Frequency.FromHertz(centre.Hertz + (width.Hertz / 2)),
|
||||
role);
|
||||
}
|
||||
9
src/Nonemm.Spotting/VfoRole.cs
Normal file
9
src/Nonemm.Spotting/VfoRole.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Nonemm.Spotting;
|
||||
|
||||
/// Which of a station's receivers or transmitters a bandmap bar stands for.
|
||||
public enum VfoRole
|
||||
{
|
||||
Receive,
|
||||
Transmit,
|
||||
Second,
|
||||
}
|
||||
Reference in New Issue
Block a user