feat(bandopen): announce sporadic-E openings on 6, 4 and 2 m
Observation, not prediction, and it needs no new data source: the cluster event worker already enriches every spot with the great-circle distance and bearing from the operator's grid, which is exactly what a single-hop Es detection rests on. The signature is four or more DISTINCT stations at 500-2400 km inside a 90 degree bearing sector within twelve minutes. Each constraint earns its place: distinct callsigns because one station spotted by six skimmers is six spots and one station; the lower bound because a 6 m contact under 500 km is ordinary tropo and says nothing about the ionosphere; the upper bound because past one hop the bearing test stops meaning anything; and the sector because a real Es cloud illuminates a direction, which is what separates an opening from a merely busy evening. Fed AFTER the Historical guard in the worker. A SH/DX reply replays a hundred past spots in a second - precisely the shape of a burst - and would announce an opening that ended hours ago. Season LABELS, it never gates. Both hemispheres get a summer peak and a lesser winter one, and an opening outside those is announced with "unusual for the season" attached: the rare one is the one an operator must not hear about last. One announcement per band per opening (45 minute quiet period). An opening runs for hours and produces hundreds of spots; one alert is information, forty is noise.
This commit is contained in:
@@ -677,6 +677,56 @@ export namespace awardref {
|
||||
|
||||
}
|
||||
|
||||
export namespace bandopen {
|
||||
|
||||
export class Opening {
|
||||
band: string;
|
||||
calls: number;
|
||||
median_km: number;
|
||||
bearing_min: number;
|
||||
bearing_max: number;
|
||||
in_season: boolean;
|
||||
// Go type: time
|
||||
at: any;
|
||||
examples: string[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Opening(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.band = source["band"];
|
||||
this.calls = source["calls"];
|
||||
this.median_km = source["median_km"];
|
||||
this.bearing_min = source["bearing_min"];
|
||||
this.bearing_max = source["bearing_max"];
|
||||
this.in_season = source["in_season"];
|
||||
this.at = this.convertValues(source["at"], null);
|
||||
this.examples = source["examples"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace cat {
|
||||
|
||||
export class FlexMeter {
|
||||
|
||||
Reference in New Issue
Block a user