const DEFAULT_COLUMN_ORDER = [ "Callsign", "Band", "Mode", "Calling", "Wanted", "Grid", "Msg", "DXCC", "Flag", "State", "County", "Cont", "dB", "Freq", "DT", "Dist", "Azim", "CQz", "ITUz", "PX", "LoTW", "eQSL", "OQRS", "Life", "Spot", "OAMS", "Age" ] const LEGACY_COLUMN_SORT_ID = { 0: "Callsign", 1: "Grid", 2: "dB", 3: "DT", 4: "Freq", 5: "DXCC", 7: "Dist", 8: "Azim", 9: "State", 10: "Calling", 11: "PX", 12: "Life", 13: "Spot", 14: "OAMS", 15: "County", 16: "Cont" } const getterSimpleComparer = (getter) => (a, b) => { const aVal = getter(a); const bVal = getter(b); if (aVal == null) return 1; if (bVal == null) return -1; if (aVal > bVal) return 1; if (aVal < bVal) return -1; return 0; } const callObjSimpleComparer = (attr) => getterSimpleComparer((elem) => elem.callObj[attr]) const callObjLocaleComparer = (attr) => (a, b) => { if (a.callObj[attr] == null) return 1; if (b.callObj[attr] == null) return -1; return a.callObj[attr].localeCompare(b.callObj[attr]); } const ROSTER_COLUMNS = { Callsign: { compare: callObjLocaleComparer("DEcall"), tableHeader: () => ({ align: "left" }), tableData: (callObj) => { let attrs = { title: callObj.awardReason, name: "Callsign", align: "left", onClick: `initiateQso("${callObj.hash}")`, rawAttrs: callObj.style.call, html: html = callObj.DEcall.formatCallsign() } let acks = window.opener.g_acknowledgedCalls; if (acks[callObj.DEcall]) { attrs.html = `${attrs.html} ` attrs.title = `${attrs.title} - ${acks[callObj.DEcall].message}` } return attrs } }, Band: { compare: false, tableData: (callObj) => ({ style: `color: #${window.opener.g_pskColors[callObj.band]};`, html: callObj.band }) }, Mode: { compare: false, tableData: (callObj) => ({ style: `color: #${g_modeColors[callObj.mode] || "888888"};`, html: callObj.mode }) }, Grid: { compare: callObjSimpleComparer("grid"), tableData: (callObj) => ({ rawAttrs: callObj.style.grid, onClick: `centerOn("${callObj.grid4}")`, html: callObj.grid4 }) }, Calling: { compare: callObjLocaleComparer("DXcall"), tableData: (callObj) => ({ rawAttrs: callObj.style.calling, name: callObj.CQ ? "CQ" : "Calling", html: callObj.DXcall.formatCallsign() }) }, Msg: { compare: callObjLocaleComparer("DXcall"), tableData: (callObj) => ({ html: callObj.msg }) }, DXCC: { compare: (a, b) => window.opener.myDxccCompare(a.callObj, b.callObj), tableData: (callObj) => ({ title: window.opener.g_worldGeoData[window.opener.g_dxccToGeoData[callObj.dxcc]].pp, name: `DXCC (${callObj.dxcc})`, rawAttrs: callObj.style.dxcc, html: window.opener.g_dxccToAltName[callObj.dxcc] }) }, Flag: { compare: (a, b) => window.opener.myDxccCompare(a.callObj, b.callObj), tableData: (callObj) => ({ align: "center", style: "margin:0; padding:0;", html: `` }) }, State: { compare: callObjSimpleComparer("state"), tableData: (callObj) => ({ align: "center", rawAttrs: callObj.style.state, html: callObj.state ? callObj.state.substr(3) : "" }) }, County: { // Not sure why this comparison uses substring, but this is what the original code did compare: getterSimpleComparer((elem) => elem.callObj.cnty && elem.callObj.cnty.substr(3)), tableData: (callObj) => { let attrs = { align: "center", rawAttrs: callObj.style.cnty, html: callObj.cnty ? window.opener.g_cntyToCounty[callObj.cnty] : "" } if (callObj.cnty && callObj.qual) { attrs.title = "ZIP Code matches multiple counties, click to do a full lookup" attrs.onClick = `lookupZip("${callObj.DEcall}", "${callObj.grid4}")` attrs.html = `¿ ${attrs.html} ?` } return attrs } }, Cont: { compare: callObjSimpleComparer("cont"), tableData: (callObj) => ({ align: "center", rawAttrs: callObj.style.cont, html: callObj.cont ? callObj.cont : "" }) }, dB: { compare: callObjSimpleComparer("RSTsent"), tableData: (callObj) => ({ style: "color:#DD44DD;", html: `${callObj.RSTsent}` }) }, Freq: { compare: callObjSimpleComparer("delta"), tableData: (callObj) => ({ style: "color: #00FF00;", html: callObj.delta }) }, DT: { compare: callObjSimpleComparer("dt"), tableData: (callObj) => ({ style: "color: #1E90FF;", html: callObj.dt }) }, Dist: { compare: callObjSimpleComparer("distance"), tableHeader: () => ({ html: `Dist (${window.opener.distanceUnit.value.toLowerCase()})` }), tableData: (callObj) => ({ style: "color: cyan;", html: Math.round(callObj.distance * MyCircle.validateRadius(window.opener.distanceUnit.value)) }) }, Azim: { compare: callObjSimpleComparer("heading"), tableData: (callObj) => ({ style: "color: yellow;", html: Math.round(callObj.heading) }) }, CQz: { compare: false, tableData: (callObj) => ({ name: "CQz", rawAttrs: callObj.style.cqz, html: callObj.cqza.join(",") }) }, ITUz: { compare: false, tableData: (callObj) => ({ name: "ITUz", rawAttrs: callObj.style.ituz, html: callObj.ituza.join(",") }) }, PX: { compare: callObjSimpleComparer("px"), tableData: (callObj) => ({ rawAttrs: callObj.style.px, html: callObj.px ? callObj.px : "" }) }, LoTW: { compare: false, tableData: (callObj) => { if (callObj.DEcall in window.opener.g_lotwCallsigns) { if (g_rosterSettings.maxLoTW < 27) { let months = (g_day - window.opener.g_lotwCallsigns[callObj.DEcall]) / 30; if (months > g_rosterSettings.maxLoTW) { return { style: "color: yellow;", align: "center", title: `Has not updated a QSO in ${Number(months).toYM()}`, html: "?" } } else { return { style: "color: #0F0;", align: "center", title: `Last Upload ${ window.opener.userDayString(window.opener.g_lotwCallsigns[callObj.DEcall] * 86400000) }`, html: "✔" } } } else { return { style: "color: #0F0;", align: "center", title: `Last Upload ${ window.opener.userDayString(window.opener.g_lotwCallsigns[callObj.DEcall] * 86400000) }`, html: "✔" } } } } }, eQSL: { compare: false, tableData: (callObj) => ({ style: "color: #0F0;", align: "center", html: (callObj.DEcall in window.opener.g_eqslCallsigns ? "✔" : "") }) }, OQRS: { compare: false, tableData: (callObj) => ({ style: "color: #0F0;", align: "center", html: (callObj.DEcall in window.opener.g_oqrsCallsigns ? "✔" : "") }) }, Life: { compare: callObjSimpleComparer("life"), tableData: (callObj) => ({ style: "color: #EEE;", class: "lifeCol", id: `lm${callObj.hash}`, html: (timeNowSec() - callObj.life).toDHMS15() }) }, OAMS: { tableHeader: () => ({ description: "Off-Air Message User" }), compare: getterSimpleComparer((elem) => elem.callObj.gt != 0 ? 1 : 0), tableData: (callObj) => { if (callObj.gt != 0) { if (callObj.reason.includes("oams")) { return { align: "center", style: "margin: 0; padding: 0; cursor: pointer; background-clip: content-box; box-shadow: 0 0 4px 4px inset #2222FFFF;", onClick: `openChatToCid("${callObj.gt}")`, html: "" } } else { return { align: "center", style: "margin: 0; padding: 0; cursor: pointer;", onClick: `openChatToCid("${callObj.gt}")`, html: "" } } } } }, Age: { compare: callObjSimpleComparer("time"), tableData: (callObj) => ({ style: "color: #EEE;", class: "timeCol", id: `tm${callObj.hash}`, title: (timeNowSec() - callObj.age).toDHMS(), html: (timeNowSec() - callObj.age).toDHMS15() }) }, Spot: { compare: (a, b) => { let cutoff = timeNowSec() - window.opener.g_receptionSettings.viewHistoryTimeSec; if (a.callObj.spot.when <= cutoff) return -1; if (b.callObj.spot.when <= cutoff) return 1; let aSNR = Number(a.callObj.spot.snr); let bSNR = Number(b.callObj.spot.snr); if (aSNR > bSNR) return 1; if (aSNR < bSNR) return -1; if (a.callObj.spot.when > b.callObj.spot.when) return 1; if (a.callObj.spot.when < b.callObj.spot.when) return -1; return 0; }, tableData: (callObj) => ({ style: "color: #EEE;", class: "spotCol", id: `sp${callObj.hash}`, html: getSpotString(callObj) }) }, POTA: { compare: false, tableData: (callObj) => ({ name: "POTA", rawAttrs: callObj.style.pota, title: callObj.pota ? callObj.pota.name : "", html: callObj.pota ? callObj.pota.reference : "" }) }, Wanted: { compare: (a, b) => wantedColumnComparer(a.callObj, b.callObj), tableData: (callObj) => ({ class: "wantedCol", title: wantedColumnParts(callObj).map(entry => `• ${entry}`).join("\n"), html: wantedColumnParts(callObj).join(" - ", { html: true }) }) } } WANTED_ORDER = ["call", "qrz", "cont", "dxcc", "cqz", "ituz", "state", "grid", "cnty", "wpx", "oams"]; WANTED_LABELS = { cont: "Continent", cqz: "CQ Zone", ituz: "ITU Zone", dxcc: "DXCC", state: "State", grid: "Grid", cnty: "County", wpx: "WPX", call: "Call", oams: "OAMS" } function wantedColumnParts(callObj, options) { options = options || {}; if (!callObj.hunting) return []; let parts = []; WANTED_ORDER.forEach(field => { let wanted = callObj.hunting[field]; if (wanted == "calling") { parts.push("Calling"); } else if (wanted == "hunted" && field == "qrz") { parts.push("QRZ"); } else if (wanted == "hunted" && field == "oams") { parts.push("OAMS User"); } else if (wanted == "hunted") { parts.push(`${options.html ? "" : ""}New ${WANTED_LABELS[field]}${options.html ? "" : ""}`); } else if (wanted == "worked") { parts.push(`Worked ${WANTED_LABELS[field]}`); } else if (wanted == "mixed") { parts.push(`${callObj.band} ${WANTED_LABELS[field]}`); } else if (wanted == "mixed-worked") { parts.push(`${callObj.band} ${WANTED_LABELS[field]}`); parts.push(`Worked ${WANTED_LABELS[field]}`); } else if (wanted == "worked-and-mixed") { parts.push(`Worked ${callObj.band} ${WANTED_LABELS[field]}`); } }) if (parts[0] == "Calling" && parts[1] == "Calling") { parts.shift(); parts.shift(); parts.unshift(`${options.html ? "" : ""}Working${options.html ? "" : ""}`); } return parts; } function wantedColumnWeighter(callObj, field) { let wanted = callObj.hunting[field]; // We use negative numbers so that sorting is "reversed" by default, placing most interesting items up top. if (wanted == "calling" || wanted == "caller") return -10; else if (wanted == "hunted") return -5; else if (wanted == "worked") return -4; else if (wanted == "mixed") return -3; else if (wanted == "mixed-worked") return -2; else if (wanted == "worked-and-mixed") return -1; else return 0; } function wantedColumnComparer(a, b) { if (!a.hunting) return 1; if (!b.hunting) return -1; for (const index in WANTED_ORDER) { const field = WANTED_ORDER[index]; const aWeight = wantedColumnWeighter(a, field); const bWeight = wantedColumnWeighter(b, field); if (aWeight < bWeight) return 1; if (aWeight > bWeight) return -1; } return 0; }