Commit pirate JS files
This commit is contained in:
926
js/WsprSearchUiChartsController.js
Normal file
926
js/WsprSearchUiChartsController.js
Normal file
@@ -0,0 +1,926 @@
|
||||
/*
|
||||
Copyright (c) 2023-forever Douglas Malnati. All rights reserved.
|
||||
|
||||
See the /faq/tos page for details.
|
||||
|
||||
(If this generated header is stamped on a file which is a 3rd party file or under a different license or copyright, then ignore this copyright statement and use that file's terms.)
|
||||
*/
|
||||
|
||||
import * as utl from '/js/Utl.js';
|
||||
|
||||
import { Base } from './Base.js';
|
||||
import { TabularData } from '../../../../js/TabularData.js';
|
||||
import {
|
||||
ChartTimeSeries,
|
||||
ChartTimeSeriesBar,
|
||||
ChartHistogramBar,
|
||||
ChartTimeSeriesTwoEqualSeriesOneLine,
|
||||
ChartTimeSeriesTwoEqualSeriesOneLinePlus,
|
||||
ChartScatterSeriesPicker,
|
||||
} from './Chart.js';
|
||||
import { WSPREncoded } from '/js/WSPREncoded.js';
|
||||
import { GreatCircle } from '/js/GreatCircle.js';
|
||||
|
||||
|
||||
export class WsprSearchUiChartsController
|
||||
extends Base
|
||||
{
|
||||
constructor(cfg)
|
||||
{
|
||||
super();
|
||||
|
||||
this.cfg = cfg;
|
||||
this.wsprSearch = this.cfg.wsprSearch || null;
|
||||
|
||||
this.ok = this.cfg.container;
|
||||
|
||||
if (this.ok)
|
||||
{
|
||||
this.ui = this.MakeUI();
|
||||
this.activeChartsUi = this.MakeChartsGrid();
|
||||
this.ui.appendChild(this.activeChartsUi);
|
||||
this.cfg.container.appendChild(this.ui);
|
||||
|
||||
ChartTimeSeries.PreLoadExternalResources();
|
||||
}
|
||||
|
||||
this.plotQueueToken = 0;
|
||||
this.renderChartsUi = this.activeChartsUi;
|
||||
this.pendingChartsUi = null;
|
||||
}
|
||||
|
||||
SetDebug(tf)
|
||||
{
|
||||
super.SetDebug(tf);
|
||||
|
||||
this.t.SetCcGlobal(tf);
|
||||
}
|
||||
|
||||
OnEvent(evt)
|
||||
{
|
||||
if (this.ok)
|
||||
{
|
||||
switch (evt.type) {
|
||||
case "DATA_TABLE_RAW_READY": this.OnDataTableRawReady(evt); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnDataTableRawReady(evt)
|
||||
{
|
||||
this.t.Reset();
|
||||
this.t.Event(`WsprSearchUiChartsController::OnDataTableReady Start`);
|
||||
|
||||
if (this.pendingChartsUi)
|
||||
{
|
||||
this.pendingChartsUi.remove();
|
||||
this.pendingChartsUi = null;
|
||||
}
|
||||
|
||||
this.ui.style.minHeight = `${Math.max(this.ui.offsetHeight, this.activeChartsUi?.offsetHeight || 0, 300)}px`;
|
||||
this.renderChartsUi = this.MakeChartsGrid();
|
||||
this.renderChartsUi.style.position = "absolute";
|
||||
this.renderChartsUi.style.top = "0";
|
||||
this.renderChartsUi.style.left = "-20000px";
|
||||
this.renderChartsUi.style.visibility = "hidden";
|
||||
this.renderChartsUi.style.pointerEvents = "none";
|
||||
this.renderChartsUi.style.contain = "layout style paint";
|
||||
this.pendingChartsUi = this.renderChartsUi;
|
||||
document.body.appendChild(this.renderChartsUi);
|
||||
|
||||
// duplicate and enrich
|
||||
let td = evt.tabularDataReadOnly;
|
||||
this.plottedColSet = new Set();
|
||||
let plotJobList = [];
|
||||
let enqueue = (fn) => {
|
||||
plotJobList.push(fn);
|
||||
};
|
||||
|
||||
// add standard charts
|
||||
if (td.Idx("AltM") && td.Idx("AltFt"))
|
||||
{
|
||||
let defaultAltMaxM = 21340;
|
||||
let defaultAltMaxFt = utl.MtoFt_Round(defaultAltMaxM);
|
||||
let actualAltMaxM = this.GetFiniteColumnMax(td, "AltM");
|
||||
let actualAltMaxFt = this.GetFiniteColumnMax(td, "AltFt");
|
||||
|
||||
enqueue(() => this.PlotTwoSeriesOneLine(td, ["AltM", "AltFt"], [
|
||||
{ min: 0, max: Math.max(defaultAltMaxM, actualAltMaxM ?? 0) },
|
||||
{ min: 0, max: Math.max(defaultAltMaxFt, actualAltMaxFt ?? 0) },
|
||||
]));
|
||||
}
|
||||
|
||||
if (td.Idx("MPH") && td.Idx("KPH") &&
|
||||
td.Idx("GpsMPH") && td.Idx("GpsKPH"))
|
||||
{
|
||||
enqueue(() => this.PlotTwoEqualSeriesPlus(td, ["KPH", "MPH", "GpsKPH", "GpsMPH"], 0, 290, 0, 180));
|
||||
}
|
||||
|
||||
if (td.Idx("TempC") && td.Idx("TempF"))
|
||||
{
|
||||
enqueue(() => this.PlotTwoSeriesOneLine(td, ["TempC", "TempF"]));
|
||||
}
|
||||
|
||||
if (td.Idx("Voltage"))
|
||||
{
|
||||
enqueue(() => this.Plot(td, "Voltage"));
|
||||
}
|
||||
|
||||
if (td.Idx("UptimeMinutes"))
|
||||
{
|
||||
enqueue(() => this.Plot(td, "UptimeMinutes", 0, 1440));
|
||||
}
|
||||
|
||||
if (td.Idx("GpsLockType"))
|
||||
{
|
||||
enqueue(() => this.Plot(td, "GpsLockType", 0, 2));
|
||||
}
|
||||
|
||||
if (td.Idx("GpsTryLockSeconds"))
|
||||
{
|
||||
enqueue(() => this.Plot(td, "GpsTryLockSeconds", 0, null));
|
||||
}
|
||||
|
||||
if (td.Idx("GpsSatsInViewCount"))
|
||||
{
|
||||
enqueue(() => this.Plot(td, "GpsSatsInViewCount", 0, 50));
|
||||
}
|
||||
|
||||
if (td.Idx("SolAngle"))
|
||||
{
|
||||
enqueue(() => this.Plot(td, "SolAngle"));
|
||||
}
|
||||
|
||||
if (td.Idx("AltChgMpm") && td.Idx("AltChgFpm"))
|
||||
{
|
||||
enqueue(() => this.PlotTwoSeriesOneLine(td, ["AltChgMpm", "AltChgFpm"]));
|
||||
}
|
||||
|
||||
// plot dynamic columns
|
||||
let headerList = td.GetHeaderList();
|
||||
|
||||
// plot UserDefined first
|
||||
for (let slot = 0; slot < 5; ++slot)
|
||||
{
|
||||
let slotHeaderList = headerList.filter(str => str.startsWith(`slot${slot}.ud`));
|
||||
|
||||
for (let slotHeader of slotHeaderList)
|
||||
{
|
||||
if (slotHeader != `slot${slot}.ud.EncMsg`)
|
||||
{
|
||||
// let metadata drive this instead of auto-ranging?
|
||||
enqueue(() => this.Plot(td, slotHeader, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// plot VendorDefined after
|
||||
for (let slot = 0; slot < 5; ++slot)
|
||||
{
|
||||
let slotHeaderList = headerList.filter(str => str.startsWith(`slot${slot}.vd`));
|
||||
|
||||
for (let slotHeader of slotHeaderList)
|
||||
{
|
||||
if (slotHeader != `slot${slot}.vd.EncMsg`)
|
||||
{
|
||||
// let metadata drive this instead of auto-ranging?
|
||||
enqueue(() => this.Plot(td, slotHeader, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add summary charts
|
||||
if (td.Idx("RxStationCount"))
|
||||
{
|
||||
enqueue(() => this.PlotBar(td, "RxStationCount", 0, null));
|
||||
}
|
||||
|
||||
if (td.Idx("DateTimeLocal"))
|
||||
{
|
||||
enqueue(() => this.PlotSpotCountByDate(td));
|
||||
}
|
||||
|
||||
if (td.Idx("WinFreqDrift"))
|
||||
{
|
||||
enqueue(() => this.PlotBar(td, "WinFreqDrift", null, null));
|
||||
}
|
||||
|
||||
enqueue(() => this.PlotRxStationFingerprintFreqSpanHistogram());
|
||||
|
||||
if (td.Idx("Lat"))
|
||||
{
|
||||
enqueue(() => this.PlotRxStationDistanceHistogram(td));
|
||||
}
|
||||
|
||||
enqueue(() => this.PlotScatterPicker(td, Array.from(this.plottedColSet).sort(), ["GpsMPH", "MPH"], ["AltFt"]));
|
||||
|
||||
this.t.Event(`WsprSearchUiChartsController::OnDataTableReady End`);
|
||||
|
||||
this.SchedulePlotJobs(plotJobList, () => {
|
||||
if (!this.pendingChartsUi)
|
||||
{
|
||||
this.Emit({
|
||||
type: "CHARTS_RENDER_COMPLETE",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.activeChartsUi)
|
||||
{
|
||||
this.activeChartsUi.remove();
|
||||
}
|
||||
|
||||
this.ui.appendChild(this.pendingChartsUi);
|
||||
this.pendingChartsUi.style.position = "";
|
||||
this.pendingChartsUi.style.top = "";
|
||||
this.pendingChartsUi.style.left = "";
|
||||
this.pendingChartsUi.style.visibility = "";
|
||||
this.pendingChartsUi.style.pointerEvents = "";
|
||||
this.pendingChartsUi.style.contain = "";
|
||||
|
||||
this.activeChartsUi = this.pendingChartsUi;
|
||||
this.pendingChartsUi = null;
|
||||
this.renderChartsUi = this.activeChartsUi;
|
||||
this.ui.style.minHeight = "";
|
||||
|
||||
this.Emit({
|
||||
type: "CHARTS_RENDER_COMPLETE",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
SchedulePlotJobs(plotJobList, onComplete)
|
||||
{
|
||||
let token = ++this.plotQueueToken;
|
||||
let idx = 0;
|
||||
let JOBS_PER_SLICE_MAX = 4;
|
||||
let TIME_BUDGET_MS = 12;
|
||||
|
||||
let runSlice = (deadline) => {
|
||||
if (token != this.plotQueueToken)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let jobsRun = 0;
|
||||
let sliceStart = performance.now();
|
||||
while (idx < plotJobList.length && jobsRun < JOBS_PER_SLICE_MAX)
|
||||
{
|
||||
let outOfIdleTime = false;
|
||||
if (deadline && typeof deadline.timeRemaining == "function")
|
||||
{
|
||||
outOfIdleTime = deadline.timeRemaining() <= 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
outOfIdleTime = (performance.now() - sliceStart) >= TIME_BUDGET_MS;
|
||||
}
|
||||
|
||||
if (jobsRun > 0 && outOfIdleTime)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
plotJobList[idx++]();
|
||||
++jobsRun;
|
||||
}
|
||||
|
||||
if (idx >= plotJobList.length)
|
||||
{
|
||||
if (onComplete)
|
||||
{
|
||||
onComplete();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.#ScheduleNextPlotSlice(runSlice);
|
||||
};
|
||||
|
||||
this.#ScheduleNextPlotSlice(runSlice);
|
||||
}
|
||||
|
||||
#ScheduleNextPlotSlice(runSlice)
|
||||
{
|
||||
if (window.requestIdleCallback)
|
||||
{
|
||||
window.requestIdleCallback(runSlice, { timeout: 100 });
|
||||
}
|
||||
else
|
||||
{
|
||||
window.setTimeout(() => {
|
||||
window.requestAnimationFrame(() => runSlice());
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
MakeUI()
|
||||
{
|
||||
let ui = document.createElement("div");
|
||||
|
||||
ui.style.boxSizing = "border-box";
|
||||
ui.style.width = "1210px";
|
||||
ui.style.position = "relative";
|
||||
|
||||
return ui;
|
||||
}
|
||||
|
||||
MakeChartsGrid()
|
||||
{
|
||||
let ui = document.createElement("div");
|
||||
|
||||
ui.style.boxSizing = "border-box";
|
||||
ui.style.width = "1210px";
|
||||
ui.style.display = "grid";
|
||||
ui.style.gridTemplateColumns = "1fr 1fr"; // two columns, equal spacing
|
||||
ui.style.gap = '0.5vw';
|
||||
|
||||
return ui;
|
||||
}
|
||||
|
||||
// default to trying to use metadata, let parameter min/max override
|
||||
Plot(td, colName, min, max)
|
||||
{
|
||||
this.AddPlottedColumns([colName]);
|
||||
|
||||
let chart = new ChartTimeSeries();
|
||||
chart.SetDebug(this.debug);
|
||||
this.renderChartsUi.appendChild(chart.GetUI());
|
||||
|
||||
|
||||
let minUse = undefined;
|
||||
let maxUse = undefined;
|
||||
|
||||
// look up metadata (if any) to use initially
|
||||
let metaData = td.GetColMetaData(colName);
|
||||
if (metaData)
|
||||
{
|
||||
minUse = metaData.rangeMin;
|
||||
maxUse = metaData.rangeMax;
|
||||
}
|
||||
|
||||
// let parameters override.
|
||||
// null is not the same as undefined.
|
||||
// passing null is the same as letting the chart auto-range.
|
||||
if (min !== undefined) { minUse = min; }
|
||||
if (max !== undefined) { maxUse = max; }
|
||||
|
||||
chart.PlotData({
|
||||
td: td,
|
||||
|
||||
xAxisDetail: {
|
||||
column: "DateTimeLocal",
|
||||
},
|
||||
|
||||
yAxisMode: "one",
|
||||
|
||||
yAxisDetailList: [
|
||||
{
|
||||
column: colName,
|
||||
min: minUse,
|
||||
max: maxUse,
|
||||
},
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
PlotMulti(td, colNameList)
|
||||
{
|
||||
this.AddPlottedColumns(colNameList);
|
||||
|
||||
let chart = new ChartTimeSeries();
|
||||
chart.SetDebug(this.debug);
|
||||
this.renderChartsUi.appendChild(chart.GetUI());
|
||||
|
||||
let yAxisDetailList = [];
|
||||
|
||||
for (const colName of colNameList)
|
||||
{
|
||||
let metaData = td.GetColMetaData(colName);
|
||||
|
||||
yAxisDetailList.push({
|
||||
column: colName,
|
||||
min: metaData.rangeMin,
|
||||
max: metaData.rangeMax,
|
||||
});
|
||||
}
|
||||
|
||||
chart.PlotData({
|
||||
td: td,
|
||||
|
||||
xAxisDetail: {
|
||||
column: "DateTimeLocal",
|
||||
},
|
||||
|
||||
yAxisDetailList,
|
||||
});
|
||||
};
|
||||
|
||||
PlotTwoSeriesOneLine(td, colNameList, rangeOverrideList = [])
|
||||
{
|
||||
this.AddPlottedColumns(colNameList);
|
||||
|
||||
let chart = new ChartTimeSeriesTwoEqualSeriesOneLine();
|
||||
chart.SetDebug(this.debug);
|
||||
this.renderChartsUi.appendChild(chart.GetUI());
|
||||
|
||||
let yAxisDetailList = [];
|
||||
|
||||
for (let idx = 0; idx < colNameList.length; ++idx)
|
||||
{
|
||||
let colName = colNameList[idx];
|
||||
let metaData = td.GetColMetaData(colName);
|
||||
let rangeOverride = rangeOverrideList[idx] ?? {};
|
||||
|
||||
yAxisDetailList.push({
|
||||
column: colName,
|
||||
min: rangeOverride.min ?? metaData.rangeMin,
|
||||
max: rangeOverride.max ?? metaData.rangeMax,
|
||||
});
|
||||
}
|
||||
|
||||
chart.PlotData({
|
||||
td: td,
|
||||
|
||||
xAxisDetail: {
|
||||
column: "DateTimeLocal",
|
||||
},
|
||||
|
||||
yAxisDetailList,
|
||||
});
|
||||
};
|
||||
|
||||
PlotTwoEqualSeriesPlus(td, colNameList, minExtra0, maxExtra0, minExtra1, maxExtra1)
|
||||
{
|
||||
this.AddPlottedColumns(colNameList);
|
||||
|
||||
let chart = new ChartTimeSeriesTwoEqualSeriesOneLinePlus();
|
||||
chart.SetDebug(this.debug);
|
||||
this.renderChartsUi.appendChild(chart.GetUI());
|
||||
|
||||
let yAxisDetailList = [];
|
||||
|
||||
for (const colName of colNameList)
|
||||
{
|
||||
let metaData = td.GetColMetaData(colName);
|
||||
|
||||
yAxisDetailList.push({
|
||||
column: colName,
|
||||
min: metaData.rangeMin,
|
||||
max: metaData.rangeMax,
|
||||
});
|
||||
}
|
||||
|
||||
// force the min/max of the 2 additional series
|
||||
yAxisDetailList[2].min = minExtra0;
|
||||
yAxisDetailList[2].max = maxExtra0;
|
||||
|
||||
yAxisDetailList[3].min = minExtra1;
|
||||
yAxisDetailList[3].max = maxExtra1;
|
||||
|
||||
chart.PlotData({
|
||||
td: td,
|
||||
|
||||
xAxisDetail: {
|
||||
column: "DateTimeLocal",
|
||||
},
|
||||
|
||||
yAxisDetailList,
|
||||
});
|
||||
};
|
||||
|
||||
PlotScatterPicker(td, colNameList, preferredXSeriesList, preferredYSeriesList)
|
||||
{
|
||||
if (!colNameList || colNameList.length < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let chart = new ChartScatterSeriesPicker();
|
||||
chart.SetDebug(this.debug);
|
||||
this.renderChartsUi.appendChild(chart.GetUI());
|
||||
|
||||
chart.PlotData({
|
||||
td,
|
||||
colNameList,
|
||||
preferredXSeriesList,
|
||||
preferredYSeriesList,
|
||||
});
|
||||
};
|
||||
|
||||
GetFiniteColumnMax(td, colName)
|
||||
{
|
||||
if (td.Idx(colName) == undefined)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
let max = null;
|
||||
td.ForEach(row => {
|
||||
let val = Number(td.Get(row, colName));
|
||||
if (Number.isFinite(val))
|
||||
{
|
||||
max = max == null ? val : Math.max(max, val);
|
||||
}
|
||||
});
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
AddPlottedColumns(colNameList)
|
||||
{
|
||||
for (const colName of colNameList)
|
||||
{
|
||||
this.plottedColSet.add(colName);
|
||||
}
|
||||
}
|
||||
|
||||
PlotSpotCountByDate(td)
|
||||
{
|
||||
let tdRxCount = this.MakeSpotCountByDateTd(td);
|
||||
|
||||
if (tdRxCount.Length() > 0)
|
||||
{
|
||||
this.PlotBar(tdRxCount, "SpotCountByDate", 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
MakeSpotCountByDateTd(td)
|
||||
{
|
||||
let countByDate = new Map();
|
||||
|
||||
let dateTimeList = td.ExtractDataOnly(["DateTimeLocal"]);
|
||||
for (const row of dateTimeList)
|
||||
{
|
||||
let dt = row[0];
|
||||
if (dt == undefined || dt == null || dt === "")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let dateOnly = String(dt).substring(0, 10);
|
||||
if (dateOnly.length != 10)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let cur = countByDate.get(dateOnly) || 0;
|
||||
countByDate.set(dateOnly, cur + 1);
|
||||
}
|
||||
|
||||
let dataTable = [["DateTimeLocal", "SpotCountByDate"]];
|
||||
for (const [dateOnly, count] of countByDate.entries())
|
||||
{
|
||||
dataTable.push([dateOnly, count]);
|
||||
}
|
||||
|
||||
return new TabularData(dataTable);
|
||||
}
|
||||
|
||||
PlotBar(td, colName, min, max)
|
||||
{
|
||||
this.AddPlottedColumns([colName]);
|
||||
|
||||
let chart = new ChartTimeSeriesBar();
|
||||
chart.SetDebug(this.debug);
|
||||
this.renderChartsUi.appendChild(chart.GetUI());
|
||||
|
||||
let minUse = undefined;
|
||||
let maxUse = undefined;
|
||||
|
||||
let metaData = td.GetColMetaData(colName);
|
||||
if (metaData)
|
||||
{
|
||||
minUse = metaData.rangeMin;
|
||||
maxUse = metaData.rangeMax;
|
||||
}
|
||||
|
||||
if (min !== undefined) { minUse = min; }
|
||||
if (max !== undefined) { maxUse = max; }
|
||||
|
||||
chart.PlotData({
|
||||
td: td,
|
||||
|
||||
xAxisDetail: {
|
||||
column: "DateTimeLocal",
|
||||
},
|
||||
|
||||
yAxisMode: "one",
|
||||
|
||||
yAxisDetailList: [
|
||||
{
|
||||
column: colName,
|
||||
min: minUse,
|
||||
max: maxUse,
|
||||
},
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
PlotRxStationDistanceHistogram(td)
|
||||
{
|
||||
let hist = this.MakeRxStationDistanceHistogram(td, 25);
|
||||
if (!hist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let chart = new ChartHistogramBar();
|
||||
chart.SetDebug(this.debug);
|
||||
this.renderChartsUi.appendChild(chart.GetUI());
|
||||
|
||||
chart.PlotData(hist);
|
||||
}
|
||||
|
||||
PlotRxStationFingerprintFreqSpanHistogram()
|
||||
{
|
||||
let hist = this.MakeRxStationFingerprintFreqSpanHistogram();
|
||||
if (!hist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let chart = new ChartHistogramBar();
|
||||
chart.SetDebug(this.debug);
|
||||
this.renderChartsUi.appendChild(chart.GetUI());
|
||||
|
||||
chart.PlotData(hist);
|
||||
}
|
||||
|
||||
MakeRxStationDistanceHistogram(td, bucketCount)
|
||||
{
|
||||
const MAX_DISTANCE_KM = 20037.5; // half Earth's circumference
|
||||
|
||||
if (!bucketCount || bucketCount < 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
let bucketSize = MAX_DISTANCE_KM / bucketCount;
|
||||
let bucketCountList = new Array(bucketCount).fill(0);
|
||||
|
||||
td.ForEach(row => {
|
||||
let txLat = null;
|
||||
let txLng = null;
|
||||
let lat = td.Idx("Lat") != undefined ? td.Get(row, "Lat") : null;
|
||||
let lng = td.Idx("Lng") != undefined ? td.Get(row, "Lng") : null;
|
||||
|
||||
if (lat != null && lng != null)
|
||||
{
|
||||
txLat = Number(lat);
|
||||
txLng = Number(lng);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(txLat) || !Number.isFinite(txLng))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Build superset of receiving stations for this row/window.
|
||||
let stationCallToGrid = new Map();
|
||||
let slotMsgList = td.GetRowMetaData(row)?.slotMsgList || [];
|
||||
|
||||
for (const msg of slotMsgList)
|
||||
{
|
||||
if (!msg || !Array.isArray(msg.rxRecordList))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const rxRecord of msg.rxRecordList)
|
||||
{
|
||||
let rxCall = rxRecord?.rxCallsign;
|
||||
let rxGrid = rxRecord?.rxGrid;
|
||||
if (!rxCall || !rxGrid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!stationCallToGrid.has(rxCall))
|
||||
{
|
||||
stationCallToGrid.set(rxCall, rxGrid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate one distance per station in superset.
|
||||
for (const rxGrid of stationCallToGrid.values())
|
||||
{
|
||||
let rxLat = null;
|
||||
let rxLng = null;
|
||||
try
|
||||
{
|
||||
[rxLat, rxLng] = WSPREncoded.DecodeMaidenheadToDeg(rxGrid);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(rxLat) || !Number.isFinite(rxLng))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let distKm = GreatCircle.distance(txLat, txLng, rxLat, rxLng, "KM");
|
||||
if (!Number.isFinite(distKm))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let distUse = Math.max(0, Math.min(MAX_DISTANCE_KM, distKm));
|
||||
let bucketIdx = Math.floor(distUse / bucketSize);
|
||||
bucketIdx = Math.max(0, Math.min(bucketCount - 1, bucketIdx));
|
||||
|
||||
bucketCountList[bucketIdx] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
let bucketLabelList = [];
|
||||
for (let i = 0; i < bucketCount; ++i)
|
||||
{
|
||||
let high = (i + 1) * bucketSize;
|
||||
if (i == bucketCount - 1)
|
||||
{
|
||||
high = MAX_DISTANCE_KM;
|
||||
}
|
||||
|
||||
let highKm = Math.round(high);
|
||||
let highMi = Math.round(high * 0.621371);
|
||||
bucketLabelList.push(`${utl.Commas(highKm)}km / ${utl.Commas(highMi)}mi`);
|
||||
}
|
||||
|
||||
return {
|
||||
bucketLabelList,
|
||||
bucketCountList,
|
||||
grid: {
|
||||
top: 30,
|
||||
left: 52,
|
||||
right: 18,
|
||||
bottom: 82,
|
||||
},
|
||||
xAxisNameGap: 58,
|
||||
xAxisLabelRotate: 45,
|
||||
xAxisLabelMargin: 8,
|
||||
yAxisNameGap: 14,
|
||||
yAxisName: " Histogram RX Distance",
|
||||
};
|
||||
}
|
||||
|
||||
MakeRxStationFingerprintFreqSpanHistogram()
|
||||
{
|
||||
if (!this.wsprSearch?.time__windowData)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
let rxCall__freqStats = new Map();
|
||||
|
||||
let addRxRecord = (rxRecord) => {
|
||||
let rxCall = rxRecord?.rxCallsign;
|
||||
let freq = Number(rxRecord?.frequency);
|
||||
if (!rxCall || !Number.isFinite(freq))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rxCall__freqStats.has(rxCall))
|
||||
{
|
||||
rxCall__freqStats.set(rxCall, {
|
||||
min: freq,
|
||||
max: freq,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let stats = rxCall__freqStats.get(rxCall);
|
||||
if (freq < stats.min) { stats.min = freq; }
|
||||
if (freq > stats.max) { stats.max = freq; }
|
||||
};
|
||||
|
||||
for (const [time, windowData] of this.wsprSearch.time__windowData)
|
||||
{
|
||||
let referenceAudit = windowData?.fingerprintingData?.referenceAudit;
|
||||
if (!referenceAudit?.slotAuditList)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let slot = 0; slot < referenceAudit.slotAuditList.length; ++slot)
|
||||
{
|
||||
if (slot == referenceAudit.referenceSlot)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let slotAudit = referenceAudit.slotAuditList[slot];
|
||||
if (!slotAudit?.msgAuditList?.length || !slotAudit?.msgMatchList?.length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let msgMatchSet = new Set(slotAudit.msgMatchList);
|
||||
for (const msgAudit of slotAudit.msgAuditList)
|
||||
{
|
||||
if (!msgMatchSet.has(msgAudit.msg))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const rxCallMatch of msgAudit.rxCallMatchList || [])
|
||||
{
|
||||
for (const rxRecord of rxCallMatch.rxRecordListA || [])
|
||||
{
|
||||
addRxRecord(rxRecord);
|
||||
}
|
||||
for (const rxRecord of rxCallMatch.rxRecordListB || [])
|
||||
{
|
||||
addRxRecord(rxRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rxCall__freqStats.size == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
let spanList = [];
|
||||
let maxSpan = 0;
|
||||
for (const [rxCall, stats] of rxCall__freqStats)
|
||||
{
|
||||
let span = Math.abs(stats.max - stats.min);
|
||||
span = Math.round(span);
|
||||
spanList.push(span);
|
||||
if (span > maxSpan)
|
||||
{
|
||||
maxSpan = span;
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_BUCKET_START = 150;
|
||||
|
||||
let bucketSpecList = [];
|
||||
for (let span = 0; span <= 30; ++span)
|
||||
{
|
||||
bucketSpecList.push({
|
||||
min: span,
|
||||
max: span,
|
||||
label: `${span}`,
|
||||
});
|
||||
}
|
||||
for (let start = 31; start <= MAX_BUCKET_START - 1; start += 5)
|
||||
{
|
||||
let end = Math.min(start + 4, MAX_BUCKET_START - 1);
|
||||
bucketSpecList.push({
|
||||
min: start,
|
||||
max: end,
|
||||
label: `${start}`,
|
||||
});
|
||||
}
|
||||
bucketSpecList.push({
|
||||
min: MAX_BUCKET_START - 1,
|
||||
max: MAX_BUCKET_START - 1,
|
||||
label: `${MAX_BUCKET_START - 1}`,
|
||||
});
|
||||
bucketSpecList.push({
|
||||
min: MAX_BUCKET_START,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
label: `${MAX_BUCKET_START}+`,
|
||||
});
|
||||
|
||||
let bucketLabelList = bucketSpecList.map(spec => spec.label);
|
||||
let bucketCountList = new Array(bucketSpecList.length).fill(0);
|
||||
for (const span of spanList)
|
||||
{
|
||||
let idxBucket = bucketSpecList.findIndex(spec => span >= spec.min && span <= spec.max);
|
||||
if (idxBucket != -1)
|
||||
{
|
||||
bucketCountList[idxBucket] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bucketLabelList,
|
||||
bucketCountList,
|
||||
xAxisName: "Hz",
|
||||
grid: {
|
||||
top: 30,
|
||||
left: 34,
|
||||
right: 14,
|
||||
bottom: 40,
|
||||
},
|
||||
xAxisNameGap: 28,
|
||||
xAxisLabelRotate: 30,
|
||||
xAxisLabelMargin: 10,
|
||||
yAxisNameGap: 10,
|
||||
yAxisName: " Histogram RX Freq Diff",
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user