Two differences from N1MM in the spot path, both found by reading Packet.cs. Randomising moved every CW spot. PacketSpot.Randomize moves a spot only when it is not a dupe, not a self spot, not on a CQ frequency, is on CW and is not working split, and Packet skips it altogether for a station passed to another band. Moving a dupe or a split station makes it harder to find, not easier, so this now leaves the same ones alone. Whether a station has been worked comes from the log, so AppSession asks it before moving a spot. Split needed knowing where a station is listening, which is in the comment. Qsx reads it by N1MM's rules: QSX, UP, DOWN, DN, U or D, the word standing alone or after a space, a bare number after QSX meaning kilohertz inside the band and anything else an offset from the spot, an offset over a hundred kilohertz refused unless it says QSX, DN70 read as a grid square rather than five down, and a listening frequency outside the band thrown away. Spots reached the bandmap one at a time, each one redrawing every window that watches it. N1MM queues them and flushes the queue once a second, which is what this does now: the filters and the randomiser run over the batch, and the bandmap takes it in one call and raises one change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
84 lines
2.8 KiB
C#
84 lines
2.8 KiB
C#
using System.Globalization;
|
|
using Nonemm.Core;
|
|
|
|
namespace Nonemm.Spotting;
|
|
|
|
/// Reads the `DX de` lines a cluster node sends.
|
|
public static class SpotLine
|
|
{
|
|
private const string Marker = "DX de ";
|
|
|
|
/// Null for any other traffic from the node, which the packet window shows
|
|
/// as it arrived.
|
|
public static Spot? Parse(string line, DateTime nowUtc)
|
|
{
|
|
int start = line.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
|
|
if (start < 0)
|
|
{
|
|
return null;
|
|
}
|
|
string body = line[(start + Marker.Length)..];
|
|
int colon = body.IndexOf(':');
|
|
string[] parts = (colon < 0 ? body : body[(colon + 1)..])
|
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length < 2)
|
|
{
|
|
return null;
|
|
}
|
|
// without a colon the spotter is the first word and the frequency follows
|
|
string spotter = colon >= 0 ? body[..colon].Trim() : parts[0];
|
|
string[] fields = colon >= 0 ? parts : parts[1..];
|
|
if (fields.Length < 2
|
|
|| !double.TryParse(fields[0], NumberStyles.Float, CultureInfo.InvariantCulture, out double kilohertz))
|
|
{
|
|
return null;
|
|
}
|
|
string[] tail = fields[2..];
|
|
int stamp = LastTimeStamp(tail);
|
|
Frequency where = Frequency.FromKilohertz(kilohertz);
|
|
string comment = string.Join(' ', stamp < 0 ? tail : tail[..stamp]).Trim();
|
|
return new Spot(
|
|
Callsign.Parse(fields[1]),
|
|
where,
|
|
stamp < 0 ? nowUtc : TimeOf(tail[stamp], nowUtc),
|
|
SpotSource.Cluster,
|
|
spotter,
|
|
comment)
|
|
{
|
|
Qsx = Qsx.Parse(comment, where),
|
|
};
|
|
}
|
|
|
|
/// The node writes the spot's time as `1234Z`. It is usually the last word,
|
|
/// but DXSpider puts the spotter's grid or country after it.
|
|
private static int LastTimeStamp(IReadOnlyList<string> words)
|
|
{
|
|
for (int at = words.Count - 1; at >= 0; at--)
|
|
{
|
|
if (IsTimeStamp(words[at]))
|
|
{
|
|
return at;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
private static bool IsTimeStamp(string word) =>
|
|
word.Length == 5
|
|
&& char.ToUpperInvariant(word[4]) == 'Z'
|
|
&& word[..4].All(char.IsAsciiDigit);
|
|
|
|
private static DateTime TimeOf(string stamp, DateTime nowUtc)
|
|
{
|
|
int hour = int.Parse(stamp[..2], CultureInfo.InvariantCulture);
|
|
int minute = int.Parse(stamp[2..4], CultureInfo.InvariantCulture);
|
|
if (hour > 23 || minute > 59)
|
|
{
|
|
return nowUtc;
|
|
}
|
|
DateTime at = new(nowUtc.Year, nowUtc.Month, nowUtc.Day, hour, minute, 0, DateTimeKind.Utc);
|
|
// a spot timed later than now came in just before midnight
|
|
return at > nowUtc.AddMinutes(5) ? at.AddDays(-1) : at;
|
|
}
|
|
}
|