Files
protoloon/js/WsprSearchUiDataTableController.js
2026-04-02 17:39:02 -06:00

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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
};
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;
}
}