1883 lines
62 KiB
JavaScript
1883 lines
62 KiB
JavaScript
/*
|
|
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 { CSSDynamic } from './CSSDynamic.js';
|
|
import { TabularData } from '../../../../js/TabularData.js';
|
|
import { WSPREncoded } from '/js/WSPREncoded.js';
|
|
import { WsprSearchUiDataTableRowProController } from './WsprSearchUiDataTableRowProController.js';
|
|
import { WsprSearchUiDataTableVisibility } from './WsprSearchUiDataTableVisibility.js';
|
|
import { WsprSearchUiDataTableColumnOrder } from './WsprSearchUiDataTableColumnOrder.js';
|
|
|
|
|
|
export class WsprSearchUiDataTableController
|
|
extends Base
|
|
{
|
|
static DETAILS_COOKIE_NAME = "wsprSearchDataTableControlsOpen";
|
|
static UI_UPGRADE_VERSION_KEY = "wsprSearchDataTableUiUpgradeVersion";
|
|
static UI_UPGRADE_VERSION = 2;
|
|
|
|
constructor(cfg)
|
|
{
|
|
super();
|
|
|
|
this.cfg = cfg;
|
|
this.wsprSearch = this.cfg.wsprSearch || null;
|
|
|
|
this.ok = this.cfg.container;
|
|
|
|
this.msgDefinitionUserDefinedList = new Array(5).fill("");
|
|
this.msgDefinitionVendorDefinedList = new Array(5).fill("");
|
|
|
|
// initialize display/data state before controls are created
|
|
// so persisted control callbacks do not get overwritten later.
|
|
this.table = document.createElement('table');
|
|
this.band = "20m";
|
|
this.channel = "";
|
|
this.channelPadded = "000";
|
|
this.callsign = "unset";
|
|
this.gte = "";
|
|
this.lte = "";
|
|
this.td = new TabularData();
|
|
this.tdSource = null;
|
|
this.showProColumn = false;
|
|
this.renderTableToken = 0;
|
|
this.pendingRenderAfterCharts = null;
|
|
this.pendingRenderAfterChartsFallbackId = null;
|
|
this.pendingJumpToDataTs = null;
|
|
|
|
if (this.ok)
|
|
{
|
|
this.ui = this.MakeUI();
|
|
this.cfg.container.appendChild(this.ui);
|
|
}
|
|
}
|
|
|
|
SetDebug(tf)
|
|
{
|
|
super.SetDebug(tf);
|
|
|
|
this.t.SetCcGlobal(tf);
|
|
}
|
|
|
|
OnEvent(evt)
|
|
{
|
|
if (this.ok)
|
|
{
|
|
switch (evt.type) {
|
|
case "SEARCH_REQUESTED": this.#OnSearchRequest(evt); break;
|
|
case "DATA_TABLE_RAW_READY": this.OnDataTableRawReady(evt); break;
|
|
case "CHARTS_RENDER_COMPLETE": this.#OnChartsRenderComplete(); break;
|
|
case "JUMP_TO_DATA": this.#OnJumpToData(evt); break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#OnSearchRequest(evt)
|
|
{
|
|
this.band = evt.band;
|
|
this.channel = evt.channel;
|
|
this.channelPadded = ("000" + evt.channel).slice(-3);
|
|
this.callsign = evt.callsign;
|
|
this.gte = evt.gte;
|
|
this.lte = evt.lteRaw;
|
|
|
|
this.#UpdateDownloadAndCopyLinks();
|
|
}
|
|
|
|
OnDataTableRawReady(evt)
|
|
{
|
|
this.t.Reset();
|
|
let t1 = this.t.Event(`WsprSearchUiDataTableController::OnDataTableRawReady Start`);
|
|
|
|
// clear existing child nodes
|
|
this.cfg.container.innerHTML = "";
|
|
|
|
this.tdSource = evt.tabularDataReadOnly;
|
|
|
|
this.cfg.container.appendChild(this.ui);
|
|
|
|
let token = ++this.renderTableToken;
|
|
let run = () => {
|
|
if (token != this.renderTableToken)
|
|
{
|
|
return;
|
|
}
|
|
|
|
this.#RenderDataTable();
|
|
|
|
let t2 = this.t.Event(`WsprSearchUiDataTableController::OnDataTableRawReady End`);
|
|
|
|
let stats = this.wsprSearch?.GetStats?.();
|
|
if (stats?.processing)
|
|
{
|
|
stats.processing.uiRenderMs = Math.round(t2 - t1);
|
|
}
|
|
};
|
|
|
|
this.pendingRenderAfterCharts = {
|
|
token,
|
|
run,
|
|
};
|
|
|
|
if (this.pendingRenderAfterChartsFallbackId != null)
|
|
{
|
|
window.clearTimeout(this.pendingRenderAfterChartsFallbackId);
|
|
}
|
|
this.pendingRenderAfterChartsFallbackId = window.setTimeout(() => {
|
|
this.#OnChartsRenderComplete();
|
|
}, 2000);
|
|
}
|
|
|
|
#OnChartsRenderComplete()
|
|
{
|
|
if (!this.pendingRenderAfterCharts)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (this.pendingRenderAfterChartsFallbackId != null)
|
|
{
|
|
window.clearTimeout(this.pendingRenderAfterChartsFallbackId);
|
|
this.pendingRenderAfterChartsFallbackId = null;
|
|
}
|
|
|
|
let { token, run } = this.pendingRenderAfterCharts;
|
|
this.pendingRenderAfterCharts = null;
|
|
|
|
let scheduleRun = () => {
|
|
if (token != this.renderTableToken)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (window.requestIdleCallback)
|
|
{
|
|
window.requestIdleCallback(() => {
|
|
window.requestAnimationFrame(run);
|
|
}, { timeout: 500 });
|
|
}
|
|
else
|
|
{
|
|
window.setTimeout(() => {
|
|
window.requestAnimationFrame(run);
|
|
}, 0);
|
|
}
|
|
};
|
|
|
|
window.requestAnimationFrame(scheduleRun);
|
|
}
|
|
|
|
#OnJumpToData(evt)
|
|
{
|
|
this.pendingJumpToDataTs = evt.ts ?? null;
|
|
this.#TryHandlePendingJumpToData();
|
|
}
|
|
|
|
SetMsgDefinitionUserDefinedList(msgDefinitionUserDefinedList)
|
|
{
|
|
this.msgDefinitionUserDefinedList = msgDefinitionUserDefinedList;
|
|
}
|
|
|
|
SetMsgDefinitionVendorDefinedList(msgDefinitionVendorDefinedList)
|
|
{
|
|
this.msgDefinitionVendorDefinedList = msgDefinitionVendorDefinedList;
|
|
}
|
|
|
|
ModifyTableContentsForDisplay(td)
|
|
{
|
|
this.FormatNumbers(td);
|
|
this.Linkify(td);
|
|
this.LinkifyUserOrVendorDefined(td, "ud");
|
|
this.LinkifyUserOrVendorDefined(td, "vd");
|
|
this.StyleOverlapData(td);
|
|
this.AddProColumnForDisplay(td);
|
|
this.PrioritizeColumnOrder(td);
|
|
}
|
|
|
|
AddProColumnForDisplay(td)
|
|
{
|
|
td.PrependGeneratedColumns([
|
|
"Pro"
|
|
], row => {
|
|
return [`<span class="proInsightEmoji" title="Open Pro Insight" style="cursor: pointer; user-select: none;">🧠</span>`];
|
|
});
|
|
}
|
|
|
|
FormatNumbers(td)
|
|
{
|
|
let colList = td.GetHeaderList();
|
|
let colInfoList = [];
|
|
|
|
for (const col of colList)
|
|
{
|
|
let idx = td.Idx(col);
|
|
if (idx == undefined)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let useDecimals = false;
|
|
for (let rowIdx = 1; rowIdx < td.dataTable.length; ++rowIdx)
|
|
{
|
|
let val = td.dataTable[rowIdx][idx];
|
|
|
|
if (typeof val == "number")
|
|
{
|
|
if (val.toString().indexOf('.') != -1)
|
|
{
|
|
useDecimals = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let decimalPlaceLen = 3;
|
|
if (col == "Voltage" || col == "BtVoltage" || col == "EbtVoltage")
|
|
{
|
|
decimalPlaceLen = 2;
|
|
}
|
|
else if (col == "Lat" || col == "Lng" ||
|
|
col == "RegLat" || col == "RegLng" ||
|
|
col == "BtLat" || col == "BtLng" ||
|
|
col == "EbtLat" || col == "EbtLng" ||
|
|
col == "HiResLat" || col == "HiResLng")
|
|
{
|
|
decimalPlaceLen = 6;
|
|
}
|
|
else if (col == "TxFreqMhz")
|
|
{
|
|
decimalPlaceLen = 6;
|
|
}
|
|
|
|
colInfoList.push({
|
|
col,
|
|
idx,
|
|
useDecimals,
|
|
decimalPlaceLen,
|
|
});
|
|
}
|
|
|
|
for (let rowIdx = 1; rowIdx < td.dataTable.length; ++rowIdx)
|
|
{
|
|
let row = td.dataTable[rowIdx];
|
|
for (const colInfo of colInfoList)
|
|
{
|
|
let val = row[colInfo.idx];
|
|
|
|
if (typeof val != "number")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (colInfo.useDecimals)
|
|
{
|
|
let precision = td.GetRowMetaData(rowIdx - 1)?.overlap?.style?.precisionByCol?.[colInfo.col];
|
|
if (precision == undefined)
|
|
{
|
|
precision = colInfo.decimalPlaceLen;
|
|
}
|
|
|
|
val = val.toFixed(precision);
|
|
}
|
|
|
|
row[colInfo.idx] = utl.Commas(val);
|
|
}
|
|
}
|
|
}
|
|
|
|
StyleOverlapData(td)
|
|
{
|
|
let colList = td.GetHeaderList();
|
|
|
|
for (let col of colList)
|
|
{
|
|
td.GenerateModifiedColumn([
|
|
col
|
|
], (row, idx) => {
|
|
let val = td.Get(row, col);
|
|
|
|
if (val == null || val === "")
|
|
{
|
|
return [val];
|
|
}
|
|
|
|
let overlapStyle = td.GetRowMetaData(idx)?.overlap?.style;
|
|
let dimmed = overlapStyle?.dimmedCols?.has?.(col) ?? false;
|
|
let italic = overlapStyle?.italicCols?.has?.(col) ?? false;
|
|
|
|
if (!dimmed && !italic)
|
|
{
|
|
return [val];
|
|
}
|
|
|
|
let classList = [];
|
|
if (dimmed) { classList.push("overlapDimmed"); }
|
|
if (italic) { classList.push("overlapItalic"); }
|
|
|
|
return [`<span class="${classList.join(" ")}">${val}</span>`];
|
|
});
|
|
}
|
|
}
|
|
|
|
Linkify(td)
|
|
{
|
|
let linkifyLatLngPair = (latCol, lngCol) => {
|
|
if (td.Idx(latCol) == undefined || td.Idx(lngCol) == undefined)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let rawPairByRowIdx = [];
|
|
td.ForEach((row, idx) => {
|
|
rawPairByRowIdx[idx] = {
|
|
latVal: td.Get(row, latCol),
|
|
lngVal: td.Get(row, lngCol),
|
|
};
|
|
});
|
|
|
|
let makeLinkedVal = (row, rowIdx, colToRender) => {
|
|
let latVal = rawPairByRowIdx[rowIdx]?.latVal;
|
|
let lngVal = rawPairByRowIdx[rowIdx]?.lngVal;
|
|
|
|
if (latVal == null || latVal === "" || lngVal == null || lngVal === "")
|
|
{
|
|
return [td.Get(row, colToRender)];
|
|
}
|
|
|
|
let latNum = Number(String(latVal).replaceAll(",", ""));
|
|
let lngNum = Number(String(lngVal).replaceAll(",", ""));
|
|
if (!Number.isFinite(latNum) || !Number.isFinite(lngNum))
|
|
{
|
|
return [td.Get(row, colToRender)];
|
|
}
|
|
|
|
let gmUrl = WSPREncoded.MakeGoogleMapsLink(latNum, lngNum);
|
|
return [`<a href="${gmUrl}" target="_blank">${td.Get(row, colToRender)}</a>`];
|
|
};
|
|
|
|
td.GenerateModifiedColumn([latCol], (row, idx) => makeLinkedVal(row, idx, latCol));
|
|
td.GenerateModifiedColumn([lngCol], (row, idx) => makeLinkedVal(row, idx, lngCol));
|
|
};
|
|
|
|
if (td.Idx("Grid"))
|
|
{
|
|
// linkify grid
|
|
td.GenerateModifiedColumn([
|
|
"Grid"
|
|
], row => {
|
|
let grid = td.Get(row, "Grid");
|
|
|
|
let retVal = [grid];
|
|
|
|
if (grid)
|
|
{
|
|
let [lat, lng] = WSPREncoded.DecodeMaidenheadToDeg(grid);
|
|
|
|
let gmUrl = WSPREncoded.MakeGoogleMapsLink(lat, lng);
|
|
let gridLink = `<a href="${gmUrl}" target="_blank">${grid}</a>`;
|
|
|
|
retVal = [gridLink];
|
|
}
|
|
|
|
return retVal;
|
|
});
|
|
}
|
|
|
|
if (td.Idx("RegGrid"))
|
|
{
|
|
// linkify grid4
|
|
td.GenerateModifiedColumn([
|
|
"RegGrid"
|
|
], row => {
|
|
let grid4 = td.Get(row, "RegGrid");
|
|
|
|
let retVal = [grid4];
|
|
|
|
if (grid4)
|
|
{
|
|
let [lat, lng] = WSPREncoded.DecodeMaidenheadToDeg(grid4);
|
|
|
|
let gmUrl = WSPREncoded.MakeGoogleMapsLink(lat, lng);
|
|
let grid4Link = `<a href="${gmUrl}" target="_blank">${grid4}</a>`;
|
|
|
|
retVal = [grid4Link];
|
|
}
|
|
|
|
return retVal;
|
|
});
|
|
}
|
|
|
|
if (td.Idx("BtGrid6"))
|
|
{
|
|
td.GenerateModifiedColumn([
|
|
"BtGrid6"
|
|
], row => {
|
|
let grid6 = td.Get(row, "BtGrid6");
|
|
|
|
let retVal = [grid6];
|
|
|
|
if (grid6)
|
|
{
|
|
let [lat, lng] = WSPREncoded.DecodeMaidenheadToDeg(grid6);
|
|
|
|
let gmUrl = WSPREncoded.MakeGoogleMapsLink(lat, lng);
|
|
let grid6Link = `<a href="${gmUrl}" target="_blank">${grid6}</a>`;
|
|
|
|
retVal = [grid6Link];
|
|
}
|
|
|
|
return retVal;
|
|
});
|
|
}
|
|
|
|
[
|
|
["Lat", "Lng"],
|
|
["RegLat", "RegLng"],
|
|
["BtLat", "BtLng"],
|
|
["EbtLat", "EbtLng"],
|
|
["HiResLat", "HiResLng"],
|
|
].forEach(([latCol, lngCol]) => linkifyLatLngPair(latCol, lngCol));
|
|
}
|
|
|
|
LinkifyUserOrVendorDefined(td, type)
|
|
{
|
|
for (let slot = 0; slot < 5; ++slot)
|
|
{
|
|
let colName = `slot${slot}.${type}.EncMsg`;
|
|
let msgDef = type == "ud" ?
|
|
this.msgDefinitionUserDefinedList[slot] :
|
|
this.msgDefinitionVendorDefinedList[slot];
|
|
|
|
if (td.Idx(colName))
|
|
{
|
|
td.GenerateModifiedColumn([
|
|
colName
|
|
], row => {
|
|
let val = td.Get(row, colName);
|
|
|
|
let retVal = [val];
|
|
|
|
if (val)
|
|
{
|
|
let link = ``;
|
|
link += `/pro/codec/`;
|
|
link += `?codec=${encodeURIComponent(msgDef)}`;
|
|
link += `&decode=${val}`;
|
|
link += `&encode=`;
|
|
|
|
let a = `<a href='${link}' target='_blank'>${val}</a>`;
|
|
|
|
retVal = [a];
|
|
}
|
|
|
|
return retVal;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
PrioritizeColumnOrder(td)
|
|
{
|
|
WsprSearchUiDataTableColumnOrder.Apply(td);
|
|
}
|
|
|
|
ModifyWebpageFormatting(td, table)
|
|
{
|
|
this.ModifyWebpageFormattingBasic(td, table);
|
|
this.ModifyWebpageFormattingExtendedUserAndVendorDefined(table);
|
|
}
|
|
|
|
ModifyWebpageFormattingBasic(td, table)
|
|
{
|
|
let cd = new CSSDynamic();
|
|
|
|
// column header colors
|
|
for (let ccName of ["Pro"])
|
|
{
|
|
cd.SetCssClassProperties(`${ccName}_hdr`, {
|
|
backgroundColor: "lemonchiffon",
|
|
});
|
|
}
|
|
|
|
for (let ccName of ["RegCall", "RegGrid", "RegPower", "RegLat", "RegLng"])
|
|
{
|
|
cd.SetCssClassProperties(`${ccName}_hdr`, {
|
|
backgroundColor: "lightgreen",
|
|
});
|
|
}
|
|
|
|
for (let ccName of ["BtGpsValid", "BtGrid56", "BtGrid6", "BtLat", "BtLng", "BtKPH", "BtMPH"])
|
|
{
|
|
cd.SetCssClassProperties(`${ccName}_hdr`, {
|
|
backgroundColor: "lightpink",
|
|
});
|
|
}
|
|
|
|
for (let ccName of ["BtTempC", "BtTempF", "BtVoltage", "BtAltM", "BtAltFt"])
|
|
{
|
|
cd.SetCssClassProperties(`${ccName}_hdr`, {
|
|
backgroundColor: "lightpink",
|
|
});
|
|
}
|
|
|
|
// Heartbeat
|
|
for (let ccName of ["TxFreqHzIdx", "TxFreqMhz", "UptimeMinutes", "GpsLockType", "GpsTryLockSeconds", "GpsSatsInViewCount"])
|
|
{
|
|
cd.SetCssClassProperties(`${ccName}_hdr`, {
|
|
backgroundColor: "lavenderblush",
|
|
});
|
|
}
|
|
|
|
for (let ccName of ["Lat", "Lng", "TempF", "TempC", "Voltage", "AltFt", "AltM", "KPH", "MPH", "AltChgFpm", "GpsMPH", "DistMi", "AltChgMpm", "GpsKPH", "DistKm", "SolAngle", "RxStationCount", "WinFreqDrift"])
|
|
{
|
|
cd.SetCssClassProperties(`${ccName}_hdr`, {
|
|
backgroundColor: "khaki",
|
|
});
|
|
}
|
|
|
|
for (let ccName of ["EbtTempF", "EbtTempC", "EbtVoltage", "EbtGpsValid", "EbtLatitudeIdx", "EbtLongitudeIdx", "EbtLat", "EbtLng", "EbtAltFt", "EbtAltM"])
|
|
{
|
|
cd.SetCssClassProperties(`${ccName}_hdr`, {
|
|
backgroundColor: "moccasin",
|
|
});
|
|
}
|
|
|
|
for (let ccName of ["HiResReference", "HiResLatitudeIdx", "HiResLongitudeIdx", "HiResLat", "HiResLng"])
|
|
{
|
|
cd.SetCssClassProperties(`${ccName}_hdr`, {
|
|
backgroundColor: "lavender",
|
|
});
|
|
}
|
|
|
|
cd.SetCssClassProperties(`overlapDimmed`, {
|
|
opacity: "0.55",
|
|
color: "#666",
|
|
});
|
|
|
|
cd.SetCssClassProperties(`overlapItalic`, {
|
|
fontStyle: "italic",
|
|
});
|
|
|
|
|
|
// give table a border
|
|
table.classList.add(`DataTable`);
|
|
|
|
cd.SetCssClassProperties(`DataTable`, {
|
|
border: "1px solid black",
|
|
borderSpacing: 0,
|
|
// using this breaks the nice sticky header
|
|
// borderCollapse: "collapse",
|
|
borderCollapse: "separate",
|
|
});
|
|
|
|
// make sticky header
|
|
let trHeader = table.tHead.rows[0];
|
|
trHeader.classList.add(`DataTableHeader`);
|
|
|
|
cd.SetCssClassProperties(`DataTableHeader`, {
|
|
border: "1px solid black",
|
|
top: "0px",
|
|
position: "sticky",
|
|
background: "white",
|
|
});
|
|
|
|
// make data rows highlight when you mouse over them
|
|
cd.SetCssClassProperties(`DataTable tr:hover`, {
|
|
backgroundColor: "rgb(215, 237, 255)",
|
|
});
|
|
|
|
// look for every column header class name that applies to the column.
|
|
// ie column classes that end with _col.
|
|
let colClassList = [];
|
|
|
|
const colGroupList = table.querySelectorAll('colgroup'); // should just be the one
|
|
|
|
for (let colGroup of colGroupList)
|
|
{
|
|
for (let childNode of colGroup.childNodes)
|
|
{
|
|
for (let className of childNode.classList)
|
|
{
|
|
let suffix = className.slice(-4);
|
|
|
|
if (suffix == "_col")
|
|
{
|
|
colClassList.push(className);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// give minor styling to all cells in the table, by column property.
|
|
// this allows more nuanced control by other css properties to affect
|
|
// cells beyond this.
|
|
for (let colClass of colClassList)
|
|
{
|
|
cd.SetCssClassProperties(colClass, {
|
|
textAlign: "center",
|
|
padding: "2px",
|
|
});
|
|
}
|
|
|
|
|
|
// do column groupings
|
|
let columnGroupLeftRightList = [
|
|
["Pro", "Pro" ],
|
|
["DateTimeUtc", "DateTimeUtc" ],
|
|
["DateTimeLocal", "DateTimeLocal"],
|
|
["RegCall", "RegLng" ],
|
|
["BtGpsValid", "BtMPH" ],
|
|
["EbtGpsValid", "EbtAltM" ],
|
|
["HiResReference","HiResLng" ],
|
|
["Lat", "WinFreqDrift" ],
|
|
["AltM", "DistKm" ],
|
|
["AltFt", "DistMi" ],
|
|
];
|
|
|
|
// do column groupings for dynamic columns
|
|
let GetSlotColumnBorderList = (startsWith) => {
|
|
let columnBorderList = []
|
|
|
|
// find headers related to this column set
|
|
let slotHeaderList = td.GetHeaderList().filter(str => str.startsWith(startsWith));
|
|
|
|
// style the encoded message so it has a border on each side
|
|
let colEncMsgRaw = `${startsWith}.EncMsg`;
|
|
let colEncMsg = utl.StrToCssClassName(colEncMsgRaw);
|
|
columnBorderList.push([colEncMsg, colEncMsg]);
|
|
|
|
// remove the encoded message column to find remaining
|
|
slotHeaderList = slotHeaderList.filter(col => col != colEncMsgRaw);
|
|
|
|
// style remaining columns so a vertical border leftmost and rightmost
|
|
if (slotHeaderList.length)
|
|
{
|
|
let colStartRaw = slotHeaderList[0];
|
|
let colEndRaw = slotHeaderList[slotHeaderList.length - 1];
|
|
|
|
let colStart = utl.StrToCssClassName(colStartRaw);
|
|
let colEnd = utl.StrToCssClassName(colEndRaw);
|
|
|
|
columnBorderList.push([colStart, colEnd]);
|
|
}
|
|
|
|
return columnBorderList;
|
|
};
|
|
|
|
for (let slot = 0; slot < 5; ++slot)
|
|
{
|
|
columnGroupLeftRightList.push(...GetSlotColumnBorderList(`slot${slot}.ud`));
|
|
columnGroupLeftRightList.push(...GetSlotColumnBorderList(`slot${slot}.vd`));
|
|
}
|
|
|
|
// apply style to groupings
|
|
for (let columnGroupLeftRight of columnGroupLeftRightList)
|
|
{
|
|
let [colLeft, colRight] = columnGroupLeftRight;
|
|
|
|
cd.SetCssClassProperties(`${colLeft}_col`, {
|
|
borderLeft: "1px solid black",
|
|
borderCollapse: "collapse",
|
|
});
|
|
|
|
cd.SetCssClassProperties(`${colRight}_col`, {
|
|
borderRight: "1px solid black",
|
|
borderCollapse: "collapse",
|
|
});
|
|
}
|
|
}
|
|
|
|
ModifyWebpageFormattingExtendedUserAndVendorDefined(table)
|
|
{
|
|
let cd = new CSSDynamic();
|
|
|
|
for (let slot = 0; slot < 5; ++slot)
|
|
{
|
|
cd.SetCssClassProperties(utl.StrToCssClassName(`slot${slot}.ud.EncMsg_data`), {
|
|
textAlign: "left",
|
|
});
|
|
cd.SetCssClassProperties(utl.StrToCssClassName(`slot${slot}.vd.EncMsg_data`), {
|
|
textAlign: "left",
|
|
});
|
|
}
|
|
}
|
|
|
|
StackColumnNameByDot(table)
|
|
{
|
|
let thList = Array.from(table.querySelectorAll('th'));
|
|
|
|
for (let th of thList)
|
|
{
|
|
let partList = th.innerHTML.split('.');
|
|
|
|
if (partList.length >= 2)
|
|
{
|
|
let prefixList = partList;
|
|
let lastPart = prefixList.pop();
|
|
|
|
let prefixStr = prefixList.join(".");
|
|
|
|
let stackedVal = `${prefixStr}<br/>${lastPart}`;
|
|
th.innerHTML = stackedVal;
|
|
}
|
|
}
|
|
}
|
|
|
|
ApplyHeaderDisplayAliases(table)
|
|
{
|
|
let aliasMap = new Map([
|
|
["HiResReference", "HiResRef"],
|
|
["HiResLatitudeIdx", "HiResLatIdx"],
|
|
["HiResLongitudeIdx", "HiResLngIdx"],
|
|
["EbtLatitudeIdx", "EbtLatIdx"],
|
|
["EbtLongitudeIdx", "EbtLngIdx"],
|
|
["GpsTryLockSeconds", "GpsTryLockSecs"],
|
|
]);
|
|
|
|
let thList = Array.from(table.querySelectorAll("th"));
|
|
for (let th of thList)
|
|
{
|
|
let rawHeader = th.textContent?.trim?.();
|
|
if (!aliasMap.has(rawHeader))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
th.textContent = aliasMap.get(rawHeader);
|
|
}
|
|
}
|
|
|
|
MakeUI()
|
|
{
|
|
let ui = document.createElement("div");
|
|
let dtc = this.#MakeDataTableControls();
|
|
let links = this.#MakeDownloadAndCopyLinks();
|
|
|
|
// assemble
|
|
ui.appendChild(dtc);
|
|
ui.appendChild(links);
|
|
|
|
return ui;
|
|
}
|
|
|
|
#UpdateDownloadAndCopyLinks()
|
|
{
|
|
let FilenamePrefix = () => {
|
|
return `${this.band}_${this.channelPadded}_${this.callsign}_${this.gte}_${this.lte}`;
|
|
};
|
|
this.linkCsv.onclick = e => {
|
|
utl.DownloadCsv(utl.MakeFilename(`${FilenamePrefix()}.csv`), utl.TableToCsv(this.table));
|
|
};
|
|
let kmlFilenameWithAlt = `${FilenamePrefix()}_with_altitude.kml`;
|
|
this.linkKmlWithAltitude.onclick = e => {
|
|
utl.DownloadKml(utl.MakeFilename(kmlFilenameWithAlt), this.#ToKml(this.table, kmlFilenameWithAlt, true));
|
|
};
|
|
let kmlFilenameNoAlt = `${FilenamePrefix()}_no_altitude.kml`;
|
|
this.linkKmlNoAltitude.onclick = e => {
|
|
utl.DownloadKml(utl.MakeFilename(kmlFilenameNoAlt), this.#ToKml(this.table, kmlFilenameNoAlt));
|
|
};
|
|
this.linkCopyToClipboard.onclick = e => {
|
|
utl.CopyToClipboard(this.table);
|
|
};
|
|
}
|
|
|
|
#MakeDownloadAndCopyLinks()
|
|
{
|
|
let container = document.createElement('div');
|
|
|
|
this.linkCsv = document.createElement('span');
|
|
this.linkCsv.innerHTML = "csv";
|
|
|
|
this.linkKmlWithAltitude = document.createElement('span');
|
|
this.linkKmlWithAltitude.innerHTML = "kml w/ altitude";
|
|
|
|
this.linkKmlNoAltitude = document.createElement('span');
|
|
this.linkKmlNoAltitude.innerHTML = "kml no altitude";
|
|
|
|
this.linkCopyToClipboard = document.createElement('span');
|
|
this.linkCopyToClipboard.innerHTML = "(copy to clipboard)";
|
|
|
|
// style
|
|
let domList = [
|
|
this.linkCsv,
|
|
this.linkKmlWithAltitude,
|
|
this.linkKmlNoAltitude,
|
|
this.linkCopyToClipboard,
|
|
];
|
|
|
|
for (let dom of domList)
|
|
{
|
|
dom.style.cursor = "pointer";
|
|
dom.style.color = "blue";
|
|
dom.style.textDecoration = "underline";
|
|
}
|
|
|
|
// pack
|
|
// (download csv or kml w/ altitude or kml no altitude) or (copy to clipboard)
|
|
container.appendChild(document.createTextNode('(download '));
|
|
container.appendChild(this.linkCsv);
|
|
container.appendChild(document.createTextNode(' or '));
|
|
container.appendChild(this.linkKmlWithAltitude);
|
|
container.appendChild(document.createTextNode(' or '));
|
|
container.appendChild(this.linkKmlNoAltitude);
|
|
container.appendChild(document.createTextNode(') or '));
|
|
container.appendChild(this.linkCopyToClipboard);
|
|
|
|
return container;
|
|
}
|
|
|
|
#ToKml(table, filename, withAlt)
|
|
{
|
|
let retVal = "";
|
|
|
|
retVal += `<?xml version="1.0" encoding="UTF-8"?>
|
|
<kml xmlns="http://www.opengis.net/kml/2.2">
|
|
<Document>
|
|
<name>${filename}</name>
|
|
<description>Your Flight</description>
|
|
<Style id="yellowLineGreenPoly">
|
|
<LineStyle>
|
|
<color>7f00ffff</color>
|
|
<width>4</width>
|
|
</LineStyle>
|
|
<PolyStyle>
|
|
<color>7f00ff00</color>
|
|
</PolyStyle>
|
|
</Style>`;
|
|
|
|
if (this.td.Length() >= 1)
|
|
{
|
|
let coordsLast = null;
|
|
|
|
let outputLineNum = 0;
|
|
|
|
this.td.ForEach(row => {
|
|
let grid = this.td.Get(row, "Grid");
|
|
|
|
if (grid)
|
|
{
|
|
++outputLineNum;
|
|
|
|
// cells are already linkified, etc, so extract plain text
|
|
grid = grid.match(/>(.*)</)[1];
|
|
|
|
let [lat, lng] = WSPREncoded.DecodeMaidenheadToDeg(grid);
|
|
|
|
let altM = this.td.Get(row, "AltM");
|
|
|
|
// kind of torn here
|
|
// absolute may cut through the earth with distant spots
|
|
// but clampToGround doesn't have the nice altitude projections
|
|
// let the user decide, default to clamp to ground to avoid cuts through earth
|
|
let altType = "clampToGround";
|
|
if (withAlt)
|
|
{
|
|
altType = "absolute";
|
|
}
|
|
|
|
if (altM == undefined || withAlt != true)
|
|
{
|
|
altM = "0";
|
|
altType = "clampToGround";
|
|
}
|
|
|
|
// remove the comma character
|
|
altM = altM.replace(",", "");
|
|
|
|
// modify coords in a way that ensures two lat/lng coordinates in a row are never the
|
|
// same, such that google earth will not complain
|
|
if (outputLineNum % 2)
|
|
{
|
|
lat += 0.000000001;
|
|
}
|
|
|
|
let dtLocal = this.td.Get(row, "DateTimeLocal");
|
|
|
|
let coords = `${lng},${lat},${altM}\n`;
|
|
|
|
if (coordsLast)
|
|
{
|
|
retVal +=
|
|
` <Placemark>
|
|
<name>${dtLocal}</name>
|
|
<description>${dtLocal}</description>
|
|
<styleUrl>#yellowLineGreenPoly</styleUrl>
|
|
<LineString>
|
|
<extrude>1</extrude>
|
|
<tessellate>1</tessellate>
|
|
<altitudeMode>${altType}</altitudeMode>
|
|
<coordinates>
|
|
`;
|
|
retVal += ` ` + coordsLast;
|
|
retVal += ` ` + coords;
|
|
retVal +=
|
|
` </coordinates>
|
|
</LineString>
|
|
</Placemark>
|
|
`;
|
|
|
|
}
|
|
|
|
coordsLast = coords;
|
|
}
|
|
});
|
|
}
|
|
|
|
// add in north and south pole, and finish xml
|
|
retVal += `
|
|
<Placemark>
|
|
<name>North Pole</name>
|
|
<description>North Pole</description>
|
|
<Point>
|
|
<coordinates>0,90,0</coordinates>
|
|
</Point>
|
|
</Placemark>
|
|
<Placemark>
|
|
<name>South Pole</name>
|
|
<description>South Pole</description>
|
|
<Point>
|
|
<coordinates>0,-90,0</coordinates>
|
|
</Point>
|
|
</Placemark>
|
|
</Document>
|
|
</kml>`;
|
|
|
|
retVal += "\n";
|
|
|
|
return retVal;
|
|
}
|
|
|
|
#MakeDataTableControls()
|
|
{
|
|
let NotifyVisibilityChanged = () => {
|
|
this.Emit("DATA_TABLE_VISIBILITY_CHANGED");
|
|
};
|
|
let resetToDefaultFnList = [];
|
|
|
|
// Helper function
|
|
let FnClassHideShow = (colList, show) => {
|
|
let cd = new CSSDynamic();
|
|
|
|
let display = "none";
|
|
if (show)
|
|
{
|
|
display = "table-cell";
|
|
}
|
|
|
|
for (let col of colList)
|
|
{
|
|
cd.SetCssClassProperties(utl.StrToCssClassName(`${col}_col`), {
|
|
display: display,
|
|
});
|
|
}
|
|
};
|
|
|
|
this.fnClassHideShow = FnClassHideShow;
|
|
|
|
let FnClassPreviewHover = (colList, show) => {
|
|
if (!this.table)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (let col of colList)
|
|
{
|
|
let cssHdr = utl.StrToCssClassName(`${col}_hdr`);
|
|
let cssData = utl.StrToCssClassName(`${col}_data`);
|
|
|
|
for (let dom of this.table.querySelectorAll(`.${cssData}`))
|
|
{
|
|
dom.style.backgroundColor = show ? "rgb(215, 237, 255)" : "";
|
|
dom.style.color = show ? "black" : "";
|
|
}
|
|
|
|
for (let dom of this.table.querySelectorAll(`.${cssHdr}`))
|
|
{
|
|
dom.style.backgroundColor = show ? "rgb(215, 237, 255)" : "";
|
|
dom.style.color = show ? "black" : "";
|
|
}
|
|
}
|
|
};
|
|
|
|
let WireHoverPreview = (labelDom, colListOrGetter) => {
|
|
let getColList = () => {
|
|
if (typeof colListOrGetter == "function")
|
|
{
|
|
return colListOrGetter() ?? [];
|
|
}
|
|
|
|
return colListOrGetter ?? [];
|
|
};
|
|
|
|
labelDom.addEventListener("mouseenter", () => {
|
|
labelDom.style.backgroundColor = "rgb(215, 237, 255)";
|
|
FnClassPreviewHover(getColList(), true);
|
|
});
|
|
labelDom.addEventListener("mouseleave", () => {
|
|
labelDom.style.backgroundColor = "";
|
|
FnClassPreviewHover(getColList(), false);
|
|
});
|
|
};
|
|
|
|
let MakeCheckboxControl = (name, specList) => {
|
|
let storageKey = `checkbox.${name}`;
|
|
let storedVal = localStorage.getItem(storageKey);
|
|
let storedMap = new Map();
|
|
|
|
if (storedVal != null)
|
|
{
|
|
try
|
|
{
|
|
let obj = JSON.parse(storedVal);
|
|
for (let [col, checked] of Object.entries(obj))
|
|
{
|
|
storedMap.set(col, !!checked);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
let update = () => {
|
|
let obj = {};
|
|
for (const spec of specList)
|
|
{
|
|
FnClassHideShow([spec.col], spec.input.checked);
|
|
obj[spec.col] = !!spec.input.checked;
|
|
}
|
|
|
|
localStorage.setItem(storageKey, JSON.stringify(obj));
|
|
NotifyVisibilityChanged();
|
|
};
|
|
|
|
let MakeLabelForSpec = (spec) => {
|
|
let label = document.createElement("label");
|
|
label.style.whiteSpace = "nowrap";
|
|
|
|
label.appendChild(spec.input);
|
|
label.appendChild(document.createTextNode(` ${spec.label}`));
|
|
WireHoverPreview(label, [spec.col]);
|
|
|
|
return label;
|
|
};
|
|
|
|
let MakeContainer = (filterFn) => {
|
|
let container = document.createElement("span");
|
|
let specListFiltered = specList.filter(filterFn);
|
|
|
|
for (let idx = 0; idx < specListFiltered.length; ++idx)
|
|
{
|
|
let spec = specListFiltered[idx];
|
|
let label = MakeLabelForSpec(spec);
|
|
container.appendChild(label);
|
|
|
|
if (idx != specListFiltered.length - 1)
|
|
{
|
|
container.appendChild(document.createTextNode(" "));
|
|
}
|
|
}
|
|
|
|
return container;
|
|
};
|
|
|
|
for (let idx = 0; idx < specList.length; ++idx)
|
|
{
|
|
let spec = specList[idx];
|
|
let input = document.createElement("input");
|
|
input.type = "checkbox";
|
|
input.checked = storedMap.has(spec.col) ? storedMap.get(spec.col) : !!spec.checked;
|
|
input.addEventListener("change", update);
|
|
spec.input = input;
|
|
}
|
|
|
|
update();
|
|
resetToDefaultFnList.push(() => {
|
|
for (const spec of specList)
|
|
{
|
|
spec.input.checked = !!spec.checked;
|
|
}
|
|
update();
|
|
});
|
|
|
|
return {
|
|
commonUi: MakeContainer(spec => !!spec.checked),
|
|
lessCommonUi: MakeContainer(spec => !spec.checked),
|
|
makeCustomUiFromLabels: (labelGroupList) => {
|
|
let outer = document.createElement("div");
|
|
outer.style.display = "inline-grid";
|
|
outer.style.gridTemplateColumns = `repeat(${labelGroupList.length}, auto)`;
|
|
outer.style.columnGap = "16px";
|
|
outer.style.alignItems = "start";
|
|
outer.style.justifyContent = "start";
|
|
outer.style.width = "max-content";
|
|
|
|
for (let labelList of labelGroupList)
|
|
{
|
|
let col = document.createElement("div");
|
|
col.style.display = "flex";
|
|
col.style.flexDirection = "column";
|
|
col.style.gap = "2px";
|
|
|
|
for (let labelText of labelList)
|
|
{
|
|
let spec = specList.find(spec => spec.label == labelText);
|
|
if (spec)
|
|
{
|
|
col.appendChild(MakeLabelForSpec(spec));
|
|
}
|
|
}
|
|
|
|
outer.appendChild(col);
|
|
}
|
|
|
|
return outer;
|
|
},
|
|
};
|
|
};
|
|
|
|
let MakeSingleCheckbox = (storageKey, labelText, defaultChecked, onChange) => {
|
|
let container = document.createElement("span");
|
|
let label = document.createElement("label");
|
|
label.style.whiteSpace = "nowrap";
|
|
|
|
let input = document.createElement("input");
|
|
input.type = "checkbox";
|
|
input.checked = WsprSearchUiDataTableVisibility.GetStoredToggle(storageKey, defaultChecked);
|
|
input.addEventListener("change", () => {
|
|
localStorage.setItem(storageKey, input.checked ? "yes" : "no");
|
|
onChange(input.checked);
|
|
NotifyVisibilityChanged();
|
|
});
|
|
|
|
label.appendChild(input);
|
|
label.appendChild(document.createTextNode(` ${labelText}`));
|
|
if (typeof onChange.colListGetter == "function")
|
|
{
|
|
WireHoverPreview(label, onChange.colListGetter);
|
|
}
|
|
container.appendChild(label);
|
|
|
|
onChange(input.checked);
|
|
resetToDefaultFnList.push(() => {
|
|
input.checked = defaultChecked;
|
|
localStorage.setItem(storageKey, input.checked ? "yes" : "no");
|
|
onChange(input.checked);
|
|
NotifyVisibilityChanged();
|
|
});
|
|
|
|
return container;
|
|
};
|
|
|
|
let GetUserVendorDefinedColList = (type, raw) => {
|
|
let colList = [];
|
|
for (let slot = 0; slot < 5; ++slot)
|
|
{
|
|
let prefix = `slot${slot}.${type}.`;
|
|
for (let col of this.tdSource?.GetHeaderList?.() ?? [])
|
|
{
|
|
if (!col.startsWith(prefix))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let isRaw = col == `${prefix}EncMsg`;
|
|
if ((raw && isRaw) || (!raw && !isRaw))
|
|
{
|
|
colList.push(col);
|
|
}
|
|
}
|
|
}
|
|
|
|
return colList;
|
|
};
|
|
|
|
let udDecodedOnChange = checked => {
|
|
FnClassHideShow(GetUserVendorDefinedColList("ud", false), checked);
|
|
};
|
|
udDecodedOnChange.colListGetter = () => GetUserVendorDefinedColList("ud", false);
|
|
let udDecodedControl = MakeSingleCheckbox("udDecodedVisible", "Decoded", true, udDecodedOnChange);
|
|
|
|
let udRawOnChange = checked => {
|
|
FnClassHideShow(GetUserVendorDefinedColList("ud", true), checked);
|
|
};
|
|
udRawOnChange.colListGetter = () => GetUserVendorDefinedColList("ud", true);
|
|
let udRawControl = MakeSingleCheckbox("udRawVisible", "Raw", true, udRawOnChange);
|
|
|
|
let vdDecodedOnChange = checked => {
|
|
FnClassHideShow(GetUserVendorDefinedColList("vd", false), checked);
|
|
};
|
|
vdDecodedOnChange.colListGetter = () => GetUserVendorDefinedColList("vd", false);
|
|
let vdDecodedControl = MakeSingleCheckbox("vdDecodedVisible", "Decoded", true, vdDecodedOnChange);
|
|
|
|
let vdRawOnChange = checked => {
|
|
FnClassHideShow(GetUserVendorDefinedColList("vd", true), checked);
|
|
};
|
|
vdRawOnChange.colListGetter = () => GetUserVendorDefinedColList("vd", true);
|
|
let vdRawControl = MakeSingleCheckbox("vdRawVisible", "Raw", true, vdRawOnChange);
|
|
|
|
let MakeInlineControlGroup = (uiList) => {
|
|
let container = document.createElement("span");
|
|
let first = true;
|
|
for (let ui of uiList)
|
|
{
|
|
if (!ui)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!first)
|
|
{
|
|
container.appendChild(document.createTextNode(" "));
|
|
}
|
|
|
|
container.appendChild(ui);
|
|
first = false;
|
|
}
|
|
|
|
return container;
|
|
};
|
|
|
|
let dateTimeSpecList = WsprSearchUiDataTableVisibility.GetDateTimeSpecList();
|
|
let dateTimeControl = MakeCheckboxControl("dateTimeVisible", [
|
|
{ label: "DateTimeUtc", ...dateTimeSpecList[0] },
|
|
{ label: "DateTimeLocal", ...dateTimeSpecList[1] },
|
|
]);
|
|
|
|
let basicTelemetrySpecList = WsprSearchUiDataTableVisibility.GetCheckboxSpecList("basicTelemetryVisible");
|
|
let expandedBasicTelemetrySpecList = WsprSearchUiDataTableVisibility.GetCheckboxSpecList("expandedBasicTelemetryVisible");
|
|
let highResLocationSpecList = WsprSearchUiDataTableVisibility.GetCheckboxSpecList("highResLocationVisible");
|
|
let resolvedSpecList = WsprSearchUiDataTableVisibility.GetCheckboxSpecList("resolvedVisible");
|
|
let regularType1SpecList = WsprSearchUiDataTableVisibility.GetCheckboxSpecList("regularType1Visible");
|
|
let heartbeatSpecList = WsprSearchUiDataTableVisibility.GetCheckboxSpecList("heartbeatVisible");
|
|
|
|
// Handle Basic Telemetry column hide/show
|
|
let basicTelemetryControl = MakeCheckboxControl("basicTelemetryVisible", [
|
|
{ label: "GpsValid", ...basicTelemetrySpecList[0] },
|
|
{ label: "Grid56", ...basicTelemetrySpecList[1] },
|
|
{ label: "Grid6", ...basicTelemetrySpecList[2] },
|
|
{ label: "Lat", ...basicTelemetrySpecList[3] },
|
|
{ label: "Lng", ...basicTelemetrySpecList[4] },
|
|
{ label: "TempC", ...basicTelemetrySpecList[5] },
|
|
{ label: "TempF", ...basicTelemetrySpecList[6] },
|
|
{ label: "Voltage", ...basicTelemetrySpecList[7] },
|
|
{ label: "AltM", ...basicTelemetrySpecList[8] },
|
|
{ label: "AltFt", ...basicTelemetrySpecList[9] },
|
|
{ label: "KPH", ...basicTelemetrySpecList[10] },
|
|
{ label: "MPH", ...basicTelemetrySpecList[11] },
|
|
]);
|
|
let basicTelemetryLessCommonUi = basicTelemetryControl.makeCustomUiFromLabels([
|
|
["Grid6", "Grid56", "Lat", "Lng", "Voltage"],
|
|
["TempF", "AltFt", "MPH"],
|
|
["TempC", "AltM", "KPH"],
|
|
]);
|
|
|
|
let expandedBasicTelemetryControl = MakeCheckboxControl("expandedBasicTelemetryVisible", [
|
|
{ label: "GpsValid", ...expandedBasicTelemetrySpecList[0] },
|
|
{ label: "Voltage", ...expandedBasicTelemetrySpecList[1] },
|
|
{ label: "Lat", ...expandedBasicTelemetrySpecList[2] },
|
|
{ label: "Lng", ...expandedBasicTelemetrySpecList[3] },
|
|
{ label: "LatIdx", ...expandedBasicTelemetrySpecList[4] },
|
|
{ label: "LngIdx", ...expandedBasicTelemetrySpecList[5] },
|
|
{ label: "TempF", ...expandedBasicTelemetrySpecList[6] },
|
|
{ label: "AltFt", ...expandedBasicTelemetrySpecList[7] },
|
|
{ label: "TempC", ...expandedBasicTelemetrySpecList[8] },
|
|
{ label: "AltM", ...expandedBasicTelemetrySpecList[9] },
|
|
]);
|
|
let expandedBasicTelemetryLessCommonUi = expandedBasicTelemetryControl.makeCustomUiFromLabels([
|
|
["Lat", "Lng", "LatIdx", "LngIdx"],
|
|
["TempF", "AltFt"],
|
|
["TempC", "AltM"],
|
|
]);
|
|
|
|
let highResLocationControl = MakeCheckboxControl("highResLocationVisible", [
|
|
{ label: "Lat", ...highResLocationSpecList[0] },
|
|
{ label: "Lng", ...highResLocationSpecList[1] },
|
|
{ label: "Ref", ...highResLocationSpecList[2] },
|
|
{ label: "LatIdx", ...highResLocationSpecList[3] },
|
|
{ label: "LngIdx", ...highResLocationSpecList[4] },
|
|
]);
|
|
|
|
let resolvedControl = MakeCheckboxControl("resolvedVisible", [
|
|
{ label: "Lat", ...resolvedSpecList[0] },
|
|
{ label: "Lng", ...resolvedSpecList[1] },
|
|
{ label: "TempF", ...resolvedSpecList[2] },
|
|
{ label: "TempC", ...resolvedSpecList[3] },
|
|
{ label: "Voltage", ...resolvedSpecList[4] },
|
|
{ label: "AltFt", ...resolvedSpecList[5] },
|
|
{ label: "AltM", ...resolvedSpecList[6] },
|
|
{ label: "KPH", ...resolvedSpecList[7] },
|
|
{ label: "MPH", ...resolvedSpecList[8] },
|
|
{ label: "AltChgFpm", ...resolvedSpecList[9] },
|
|
{ label: "GpsMPH", ...resolvedSpecList[10] },
|
|
{ label: "DistMi", ...resolvedSpecList[11] },
|
|
{ label: "AltChgMpm", ...resolvedSpecList[12] },
|
|
{ label: "GpsKPH", ...resolvedSpecList[13] },
|
|
{ label: "DistKm", ...resolvedSpecList[14] },
|
|
{ label: "SolAngle", ...resolvedSpecList[15] },
|
|
{ label: "RxStationCount", ...resolvedSpecList[16] },
|
|
{ label: "WinFreqDrift", ...resolvedSpecList[17] },
|
|
]);
|
|
let resolvedCommonUi = resolvedControl.makeCustomUiFromLabels([
|
|
["Lat", "Lng", "Voltage"],
|
|
["TempF", "AltFt", "AltChgFpm"],
|
|
["MPH", "GpsMPH", "DistMi"],
|
|
["TempC", "AltM", "AltChgMpm"],
|
|
["KPH", "GpsKPH", "DistKm"],
|
|
["SolAngle", "RxStationCount", "WinFreqDrift"],
|
|
]);
|
|
|
|
let regularType1Control = MakeCheckboxControl("regularType1Visible", [
|
|
{ label: "Call", ...regularType1SpecList[0] },
|
|
{ label: "Grid", ...regularType1SpecList[1] },
|
|
{ label: "Power", ...regularType1SpecList[2] },
|
|
{ label: "Lat", ...regularType1SpecList[3] },
|
|
{ label: "Lng", ...regularType1SpecList[4] },
|
|
]);
|
|
|
|
// Handle Heartbeat column hide/show
|
|
let heartbeatControl = MakeCheckboxControl("heartbeatVisible", [
|
|
{ label: "UptimeMinutes", ...heartbeatSpecList[0] },
|
|
{ label: "GpsLockType", ...heartbeatSpecList[1] },
|
|
{ label: "GpsTryLockSecs", ...heartbeatSpecList[2] },
|
|
{ label: "GpsSatsInViewCount", ...heartbeatSpecList[3] },
|
|
{ label: "TxFreqHzIdx", ...heartbeatSpecList[4] },
|
|
{ label: "TxFreqMhz", ...heartbeatSpecList[5] },
|
|
]);
|
|
|
|
// Handle Pro column hide/show
|
|
let proOnChange = checked => {
|
|
this.showProColumn = checked;
|
|
FnClassHideShow(["Pro"], this.showProColumn);
|
|
NotifyVisibilityChanged();
|
|
};
|
|
proOnChange.colListGetter = () => ["Pro"];
|
|
let proControl = MakeSingleCheckbox("proVisible", "Pro", true, proOnChange);
|
|
|
|
// text to show
|
|
let summary = document.createElement('summary');
|
|
summary.innerHTML = "Data Table Controls (click to open/close)";
|
|
|
|
let applyDefaultColumnVisibility = () => {
|
|
for (let fn of resetToDefaultFnList)
|
|
{
|
|
fn();
|
|
}
|
|
};
|
|
|
|
let btnResetDefaults = document.createElement("button");
|
|
btnResetDefaults.type = "button";
|
|
btnResetDefaults.textContent = "Default Column Visibility";
|
|
btnResetDefaults.style.marginBottom = "8px";
|
|
btnResetDefaults.addEventListener("click", () => {
|
|
applyDefaultColumnVisibility();
|
|
});
|
|
|
|
let table = document.createElement("table");
|
|
let thead = document.createElement("thead");
|
|
let trHead = document.createElement("tr");
|
|
let thView = document.createElement("th");
|
|
let thCommon = document.createElement("th");
|
|
let thLessCommon = document.createElement("th");
|
|
thView.textContent = "Source";
|
|
thCommon.textContent = "Show Common";
|
|
thLessCommon.textContent = "Show Less Common";
|
|
thView.style.textAlign = "left";
|
|
thCommon.style.textAlign = "left";
|
|
thLessCommon.style.textAlign = "left";
|
|
thView.style.paddingRight = "10px";
|
|
thCommon.style.paddingRight = "10px";
|
|
trHead.appendChild(thView);
|
|
trHead.appendChild(thCommon);
|
|
trHead.appendChild(thLessCommon);
|
|
thead.appendChild(trHead);
|
|
|
|
let tbody = document.createElement("tbody");
|
|
let rowList = [
|
|
["DateTime", dateTimeControl.commonUi, dateTimeControl.lessCommonUi],
|
|
["RegularType1", regularType1Control.commonUi, regularType1Control.lessCommonUi],
|
|
[`<a href="/pro/telemetry/basic/" target="_blank">BasicTelemetry</a>`, basicTelemetryControl.commonUi, basicTelemetryLessCommonUi],
|
|
["Merged / Synth", resolvedCommonUi, null],
|
|
[`<a href="/pro/telemetry/extended/Heartbeat/" target="_blank">Heartbeat</a>`, heartbeatControl.commonUi, heartbeatControl.lessCommonUi],
|
|
[`<a href="/pro/telemetry/extended/ExpandedBasicTelemetry/" target="_blank">ExpandedBasicTelemetry</a>`, expandedBasicTelemetryControl.commonUi, expandedBasicTelemetryLessCommonUi],
|
|
[`<a href="/pro/telemetry/extended/HighResLocation/" target="_blank">HighResLocation</a>`, highResLocationControl.commonUi, highResLocationControl.lessCommonUi],
|
|
["UserDefined", MakeInlineControlGroup([udRawControl, udDecodedControl]), null],
|
|
["VendorDefined", MakeInlineControlGroup([vdRawControl, vdDecodedControl]), null],
|
|
["Pro", proControl, null],
|
|
];
|
|
|
|
for (const [labelHtml, commonUi, lessCommonUi] of rowList)
|
|
{
|
|
let tr = document.createElement("tr");
|
|
|
|
let tdLabel = document.createElement("td");
|
|
tdLabel.innerHTML = labelHtml;
|
|
tdLabel.style.paddingRight = "10px";
|
|
|
|
let tdCommon = document.createElement("td");
|
|
tdCommon.style.paddingRight = "10px";
|
|
if (commonUi)
|
|
{
|
|
tdCommon.appendChild(commonUi);
|
|
}
|
|
|
|
let tdLessCommon = document.createElement("td");
|
|
if (lessCommonUi)
|
|
{
|
|
tdLessCommon.appendChild(lessCommonUi);
|
|
}
|
|
|
|
tr.appendChild(tdLabel);
|
|
tr.appendChild(tdCommon);
|
|
tr.appendChild(tdLessCommon);
|
|
tbody.appendChild(tr);
|
|
}
|
|
|
|
table.appendChild(thead);
|
|
table.appendChild(tbody);
|
|
|
|
// make details container
|
|
let details = document.createElement('details');
|
|
details.open = this.#GetDataTableControlsOpenState();
|
|
details.addEventListener("toggle", () => {
|
|
this.#SetDataTableControlsOpenState(details.open);
|
|
});
|
|
|
|
this.#RunUiUpgradeIfNeeded({
|
|
details,
|
|
applyDefaultColumnVisibility,
|
|
});
|
|
|
|
// assemble
|
|
details.appendChild(summary);
|
|
details.appendChild(btnResetDefaults);
|
|
details.appendChild(table);
|
|
details.appendChild(document.createElement('br'));
|
|
|
|
return details;
|
|
}
|
|
|
|
#GetDataTableControlsOpenState()
|
|
{
|
|
let cookieName = `${WsprSearchUiDataTableController.DETAILS_COOKIE_NAME}=`;
|
|
let cookiePartList = document.cookie.split(";");
|
|
for (let cookiePart of cookiePartList)
|
|
{
|
|
cookiePart = cookiePart.trim();
|
|
if (cookiePart.startsWith(cookieName))
|
|
{
|
|
return cookiePart.substring(cookieName.length) == "1";
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#SetDataTableControlsOpenState(open)
|
|
{
|
|
let maxAgeSeconds = 60 * 60 * 24 * 365;
|
|
let val = open ? "1" : "0";
|
|
document.cookie = `${WsprSearchUiDataTableController.DETAILS_COOKIE_NAME}=${val}; path=/; max-age=${maxAgeSeconds}; samesite=lax`;
|
|
}
|
|
|
|
#GetStoredUiUpgradeVersion()
|
|
{
|
|
return Number(localStorage.getItem(WsprSearchUiDataTableController.UI_UPGRADE_VERSION_KEY) ?? 0);
|
|
}
|
|
|
|
#SetStoredUiUpgradeVersion(version)
|
|
{
|
|
localStorage.setItem(WsprSearchUiDataTableController.UI_UPGRADE_VERSION_KEY, String(version));
|
|
}
|
|
|
|
#NeedsUiUpgrade()
|
|
{
|
|
return this.#GetStoredUiUpgradeVersion() < WsprSearchUiDataTableController.UI_UPGRADE_VERSION;
|
|
}
|
|
|
|
#RunUiUpgradeStep(version, { details, applyDefaultColumnVisibility })
|
|
{
|
|
switch (version)
|
|
{
|
|
case 1:
|
|
details.open = true;
|
|
this.#SetDataTableControlsOpenState(true);
|
|
applyDefaultColumnVisibility();
|
|
break;
|
|
|
|
case 2:
|
|
this.Emit("SHOW_MAP_CONTROL_HELP");
|
|
break;
|
|
}
|
|
}
|
|
|
|
#RunUiUpgradeIfNeeded(args)
|
|
{
|
|
if (!this.#NeedsUiUpgrade())
|
|
{
|
|
return;
|
|
}
|
|
|
|
let versionStored = this.#GetStoredUiUpgradeVersion();
|
|
for (let version = versionStored + 1; version <= WsprSearchUiDataTableController.UI_UPGRADE_VERSION; ++version)
|
|
{
|
|
this.#RunUiUpgradeStep(version, args);
|
|
this.#SetStoredUiUpgradeVersion(version);
|
|
}
|
|
}
|
|
|
|
#RenderDataTable()
|
|
{
|
|
if (this.tdSource == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// full re-render (controls + table)
|
|
this.cfg.container.innerHTML = "";
|
|
this.proRowControllerList = [];
|
|
|
|
// copy data so it can be modified without affecting other holders
|
|
this.td = this.tdSource.Clone();
|
|
|
|
// jazz up data content
|
|
this.ModifyTableContentsForDisplay(this.td);
|
|
|
|
// build the shell immediately, then parse/append body rows incrementally
|
|
this.table = this.#MakeIncrementalTableShell(this.td.GetDataTable());
|
|
|
|
// jazz up webpage presentation
|
|
this.ModifyWebpageFormatting(this.td, this.table);
|
|
|
|
// save width with dotted column headers (ie slotx.field)
|
|
this.StackColumnNameByDot(this.table);
|
|
this.ApplyHeaderDisplayAliases(this.table);
|
|
this.#ApplyUserVendorDefinedVisibility();
|
|
|
|
// replace with new
|
|
this.cfg.container.appendChild(this.ui);
|
|
this.cfg.container.appendChild(this.table);
|
|
|
|
this.#AppendTableRowsIncrementally(this.renderTableToken);
|
|
}
|
|
|
|
#MakeIncrementalTableShell(dataTable)
|
|
{
|
|
if (!dataTable?.length)
|
|
{
|
|
return utl.MakeTable([[]]);
|
|
}
|
|
|
|
return utl.MakeTable([dataTable[0]]);
|
|
}
|
|
|
|
#AppendTableRowsIncrementally(token)
|
|
{
|
|
let dataTable = this.td.GetDataTable();
|
|
let tbody = this.table?.tBodies?.[0];
|
|
if (!tbody || dataTable.length <= 1)
|
|
{
|
|
this.#WireProRowDialogs();
|
|
this.#TryHandlePendingJumpToData();
|
|
return;
|
|
}
|
|
|
|
let rowHeader = dataTable[0];
|
|
let classAttrByIdx = rowHeader.map(colVal => {
|
|
let colClass = "";
|
|
let dataClass = "";
|
|
|
|
try
|
|
{
|
|
colClass = utl.StrToCssClassName(`${colVal}_col`);
|
|
dataClass = utl.StrToCssClassName(`${colVal}_data`);
|
|
}
|
|
catch (e)
|
|
{
|
|
}
|
|
|
|
let classList = [colClass, dataClass].filter(str => str != "");
|
|
return classList.length ? ` class="${classList.join(" ")}"` : "";
|
|
});
|
|
|
|
let EscapeHtml = (val) => {
|
|
return String(val)
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """);
|
|
};
|
|
|
|
let rowIdx = 1;
|
|
let CHUNK_ROW_COUNT = 200;
|
|
|
|
let appendChunk = () => {
|
|
if (token != this.renderTableToken || tbody.isConnected == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let html = "";
|
|
let trListAdded = [];
|
|
let rowIdxStart = rowIdx;
|
|
let count = 0;
|
|
|
|
while (rowIdx < dataTable.length && count < CHUNK_ROW_COUNT)
|
|
{
|
|
let row = dataTable[rowIdx];
|
|
|
|
let hasElement = false;
|
|
for (let idx = 0; idx < row.length; ++idx)
|
|
{
|
|
if (row[idx] instanceof Element)
|
|
{
|
|
hasElement = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasElement)
|
|
{
|
|
if (html != "")
|
|
{
|
|
tbody.insertAdjacentHTML("beforeend", html);
|
|
let trListNew = Array.from(tbody.rows).slice(-(rowIdx - rowIdxStart));
|
|
trListAdded.push(...trListNew);
|
|
html = "";
|
|
rowIdxStart = rowIdx;
|
|
}
|
|
|
|
let tr = document.createElement("tr");
|
|
for (let idx = 0; idx < row.length; ++idx)
|
|
{
|
|
let td = document.createElement("td");
|
|
|
|
let colClass = utl.StrToCssClassName(`${rowHeader[idx]}_col`);
|
|
let dataClass = utl.StrToCssClassName(`${rowHeader[idx]}_data`);
|
|
if (colClass != "")
|
|
{
|
|
td.classList.add(colClass);
|
|
}
|
|
if (dataClass != "")
|
|
{
|
|
td.classList.add(dataClass);
|
|
}
|
|
|
|
let val = row[idx];
|
|
if (val instanceof Element)
|
|
{
|
|
td.appendChild(val);
|
|
}
|
|
else if (typeof val == "string" && val.indexOf("<") != -1)
|
|
{
|
|
td.innerHTML = val;
|
|
}
|
|
else
|
|
{
|
|
td.textContent = val ?? "";
|
|
}
|
|
|
|
tr.appendChild(td);
|
|
}
|
|
|
|
tbody.appendChild(tr);
|
|
trListAdded.push(tr);
|
|
++rowIdx;
|
|
++count;
|
|
rowIdxStart = rowIdx;
|
|
continue;
|
|
}
|
|
|
|
html += "<tr>";
|
|
for (let idx = 0; idx < row.length; ++idx)
|
|
{
|
|
let colVal = row[idx];
|
|
let cellHtml = "";
|
|
if (typeof colVal == "string" && colVal.indexOf("<") != -1)
|
|
{
|
|
cellHtml = colVal;
|
|
}
|
|
else
|
|
{
|
|
cellHtml = EscapeHtml(colVal ?? "");
|
|
}
|
|
|
|
html += `<td${classAttrByIdx[idx]}>${cellHtml}</td>`;
|
|
}
|
|
html += "</tr>";
|
|
|
|
++rowIdx;
|
|
++count;
|
|
}
|
|
|
|
if (html != "")
|
|
{
|
|
let rowCountHtml = rowIdx - rowIdxStart;
|
|
tbody.insertAdjacentHTML("beforeend", html);
|
|
let trListNew = Array.from(tbody.rows).slice(-rowCountHtml);
|
|
trListAdded.push(...trListNew);
|
|
}
|
|
|
|
this.#WireProRowDialogsInRows(trListAdded, rowIdx - trListAdded.length - 1);
|
|
this.#TryHandlePendingJumpToData();
|
|
|
|
if (rowIdx < dataTable.length)
|
|
{
|
|
if (window.requestIdleCallback)
|
|
{
|
|
window.requestIdleCallback(() => appendChunk(), { timeout: 100 });
|
|
}
|
|
else
|
|
{
|
|
window.setTimeout(() => {
|
|
window.requestAnimationFrame(() => appendChunk());
|
|
}, 0);
|
|
}
|
|
}
|
|
};
|
|
|
|
appendChunk();
|
|
}
|
|
|
|
#ApplyUserVendorDefinedVisibility()
|
|
{
|
|
if (!this.fnClassHideShow || !this.tdSource)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let applyPrefixVisibility = (type, showDecoded, showRaw) => {
|
|
let decodedColList = [];
|
|
let rawColList = [];
|
|
|
|
for (let slot = 0; slot < 5; ++slot)
|
|
{
|
|
let prefix = `slot${slot}.${type}.`;
|
|
for (let col of this.tdSource.GetHeaderList())
|
|
{
|
|
if (!col.startsWith(prefix))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (col == `${prefix}EncMsg`)
|
|
{
|
|
rawColList.push(col);
|
|
}
|
|
else
|
|
{
|
|
decodedColList.push(col);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.fnClassHideShow(decodedColList, showDecoded);
|
|
this.fnClassHideShow(rawColList, showRaw);
|
|
};
|
|
|
|
applyPrefixVisibility(
|
|
"ud",
|
|
WsprSearchUiDataTableVisibility.GetStoredToggle("udDecodedVisible", true),
|
|
WsprSearchUiDataTableVisibility.GetStoredToggle("udRawVisible", false),
|
|
);
|
|
|
|
applyPrefixVisibility(
|
|
"vd",
|
|
WsprSearchUiDataTableVisibility.GetStoredToggle("vdDecodedVisible", true),
|
|
WsprSearchUiDataTableVisibility.GetStoredToggle("vdRawVisible", false),
|
|
);
|
|
}
|
|
|
|
#WireProRowDialogs()
|
|
{
|
|
let tbody = this.table.tBodies[0];
|
|
if (!tbody)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let trList = Array.from(tbody.rows);
|
|
this.#WireProRowDialogsInRows(trList, 0);
|
|
}
|
|
|
|
#WireProRowDialogsInRows(trList, rowIdxOffset = 0)
|
|
{
|
|
for (let rowIdx = 0; rowIdx < trList.length; ++rowIdx)
|
|
{
|
|
let tr = trList[rowIdx];
|
|
tr.dataset.rowIdx = `${rowIdxOffset + rowIdx}`;
|
|
let emoji = tr.querySelector(".proInsightEmoji");
|
|
if (!emoji)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let controller = null;
|
|
|
|
emoji.addEventListener("click", () => {
|
|
if (controller == null)
|
|
{
|
|
controller = new WsprSearchUiDataTableRowProController({
|
|
data: {
|
|
dt: this.td,
|
|
rowIdx: rowIdxOffset + rowIdx,
|
|
wsprSearch: this.wsprSearch,
|
|
band: this.band,
|
|
channel: this.channel,
|
|
},
|
|
});
|
|
|
|
this.proRowControllerList.push(controller);
|
|
document.body.appendChild(controller.GetUI());
|
|
}
|
|
|
|
controller.Show();
|
|
});
|
|
}
|
|
}
|
|
|
|
#TryHandlePendingJumpToData()
|
|
{
|
|
if (!this.pendingJumpToDataTs || !this.table || !this.td)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let targetRowIdx = -1;
|
|
this.td.ForEach((row, idx) => {
|
|
if (targetRowIdx == -1 && this.td.Get(row, "DateTimeLocal") == this.pendingJumpToDataTs)
|
|
{
|
|
targetRowIdx = idx;
|
|
}
|
|
});
|
|
|
|
if (targetRowIdx == -1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let tr = this.table.querySelector(`tbody tr[data-row-idx="${targetRowIdx}"]`);
|
|
if (!tr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let headerHeight = this.table.tHead?.offsetHeight ?? 0;
|
|
let top = window.scrollY + tr.getBoundingClientRect().top - headerHeight - 2;
|
|
window.scrollTo({
|
|
top: Math.max(0, top),
|
|
behavior: "auto",
|
|
});
|
|
|
|
this.pendingJumpToDataTs = null;
|
|
}
|
|
}
|