Say where the station being worked is

N1MM writes a line under its entry window with the beam heading, the heading the
long way round, the distance and the sun times at the other end. This writes the
same line.

SunTimes is the published sunrise equation, with the usual -0.833 degrees for
the sun's radius and refraction at the horizon, and it returns nothing on the
days inside the polar circles when the sun does not rise or set. It agrees with
published times for London at both solstices, Sydney in June and the equator at
the equinox, to within three minutes.

StationPath picks the position: the grid square when the contest exchanges one
and it has been copied, then the call history file, then the country file, which
puts the station in the middle of its country. That is close enough to point a
beam with. Our own end comes from the station grid square, or from the country
file for our own call.

One thing to know when reading the times: they are that station's own sunrise
and sunset in UTC, so for Japan the sun rises late the evening before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 08:42:07 +00:00
parent 2e81191ea0
commit a06bbde3f9
7 changed files with 290 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
using Nonemm.Contests;
using Nonemm.Contests.Rules;
using Nonemm.Core;
using Nonemm.Core.Calls;
using Nonemm.Core.Country;
using Nonemm.Storage;
namespace Nonemm.Session.Tests;
public class StationPathTests
{
private static readonly DateTime Now = new(2026, 8, 28, 12, 0, 0, DateTimeKind.Utc);
/// Two entities far enough apart to check a heading against a map.
private static readonly CountryFile Countries = CountryFile.Parse(
"""
Slovak Republic: 15: 28: EU: 48.67: -19.50: -1.0: OM:
OM;
Japan: 25: 45: AS: 36.40: -138.38: -9.0: JA:
JA;
""");
private static RadioPosition Session(string myGrid = "", CallHistory? history = null)
{
FakeLogStore store = new();
ContestInstance instance = store.AddContest(new ContestInstance
{
ContestNumber = 0,
ContestName = "CQWW",
SentExchange = "15",
});
return new RadioPosition(new ContestSession(
store,
new CqWorldWide(ModeCategory.Cw),
instance,
new StationInfo { Callsign = "OM5M", CqZone = 15, GridSquare = myGrid },
Countries,
history));
}
[Fact]
public void WithNothingInTheCallsignBoxThereIsNoPath() =>
Assert.Null(StationPath.For(Session(), Now));
[Fact]
public void TheCountryFilePlacesAStationWhoseGridIsNotKnown()
{
RadioPosition session = Session();
session.Entry.Call = "JA1XYZ";
StationPath path = Assert.IsType<StationPath>(StationPath.For(session, Now));
// Slovakia to Japan is a little north of east, about nine thousand km
Assert.InRange(path.Heading, 40, 60);
Assert.InRange(path.DistanceKm, 8_500, 9_500);
Assert.Equal((path.Heading + 180) % 360, path.LongPath);
}
[Fact]
public void AGridFromTheCallHistoryBeatsTheCountryFile()
{
RadioPosition known = Session(
myGrid: "JN88",
history: CallHistory.Parse("!!Order!!,Call,LOC1\nJA1XYZ,PM95"));
known.Entry.Call = "JA1XYZ";
RadioPosition unknown = Session(myGrid: "JN88");
unknown.Entry.Call = "JA1XYZ";
StationPath fromGrid = Assert.IsType<StationPath>(StationPath.For(known, Now));
StationPath fromCountry = Assert.IsType<StationPath>(StationPath.For(unknown, Now));
Assert.NotEqual(Math.Round(fromGrid.DistanceKm), Math.Round(fromCountry.DistanceKm));
Assert.InRange(fromGrid.DistanceKm, 9_000, 9_600);
}
[Fact]
public void TheSunTimesAreTheOtherStationsOwn()
{
RadioPosition session = Session();
session.Entry.Call = "JA1XYZ";
StationPath path = Assert.IsType<StationPath>(StationPath.For(session, Now));
// Japan is nine hours ahead, so the sun rises there late the evening
// before, in UTC, and sets in the morning
Assert.Equal(20, Assert.IsType<DateTime>(path.Sunrise).Hour);
Assert.Equal(9, Assert.IsType<DateTime>(path.Sunset).Hour);
}
}