const DEFAULT_COLUMN_ORDER = [ "Callsign", "Band", "Mode", "Calling", "Rig", "Wanted", "Grid", "Msg", "POTA", "DXCC", "Flag", "State", "County", "Cont", "dB", "Freq", "DT", "Dist", "Azim", "CQz", "ITUz", "PX", "LoTW", "eQSL", "OQRS", "Life", "Spot", "OAMS", "Age", "UTC" ] 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 = formatCallsign((callObj.DEcallHTML || callObj.DEcall)) } let acks = window.opener.GT.acknowledgedCalls || {}; if (acks[callObj.DEcall]) { attrs.html = `${attrs.html} ` attrs.title = `${attrs.title} - ${acks[callObj.DEcall].message}` } return attrs } }, Rig: { compare: callObjSimpleComparer("instance"), tableData: (callObj) => ({ html: callObj.instance }) }, Band: { compare: callObjSimpleComparer("band"), tableData: (callObj) => ({ style: `color: #${window.opener.GT.pskColors[callObj.band]};`, html: callObj.band }) }, Mode: { compare: callObjSimpleComparer("mode"), tableData: (callObj) => ({ style: `color: #${CR.modeColors[callObj.mode] || "888888"};`, html: callObj.mode }) }, Grid: { compare: callObjSimpleComparer("grid"), tableData: (callObj) => ({ rawAttrs: callObj.style.grid, onClick: `centerOn("${callObj.grid}")`, html: (callObj.grid.length > 0 ? callObj.grid : " ") }) }, Calling: { compare: callObjLocaleComparer("DXcall"), tableData: (callObj) => ({ rawAttrs: callObj.style.calling, name: callObj.CQ ? "CQ" : "Calling", html: (CR.rosterSettings.wantRRCQ && callObj.RR73) ? "RR73" : formatCallsign(callObj.DXcallHTML || callObj.DXcall) }) }, Msg: { compare: callObjLocaleComparer("DXcall"), tableData: (callObj) => ({ name: "Msg", html: callObj.msgHTML || htmlEntities(callObj.msg) }) }, DXCC: { compare: (a, b) => window.opener.myDxccCompare(a.callObj, b.callObj), tableData: (callObj) => ({ title: window.opener.GT.dxccInfo[callObj.dxcc].pp, name: `DXCC (${callObj.dxcc})`, rawAttrs: callObj.style.dxcc, html: (callObj.dxccSuffix ? [window.opener.GT.dxccToAltName[callObj.dxcc], callObj.dxccSuffix].join(" ") : window.opener.GT.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 // Because we're sorting on the county name, the data contains "CO,Adams", we don't want to sort by state. 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.GT.cntyToCounty[callObj.cnty] : " " } if (callObj.cnty && callObj.qual == false) { attrs.title = $.i18n("rosterColumns.County.title") attrs.onClick = `window.opener.lookupCallsign("${callObj.DEcall}", "${callObj.grid}")` attrs.html = attrs.html + " +" + String(window.opener.GT.zipToCounty[callObj.zipcode].length - 1) attrs.style = "cursor: pointer; color: cyan;" } 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: (callObj.distance > 0 ? Math.round(callObj.distance * MyCircle.validateRadius(window.opener.distanceUnit.value)) : " ") }) }, Azim: { compare: callObjSimpleComparer("heading"), tableData: (callObj) => ({ style: "color: yellow;", html: (callObj.distance > 0 ? Math.round(callObj.heading) : " ") }) }, CQz: { compare: false, tableData: (callObj) => ({ name: "CQz", rawAttrs: callObj.style.cqz, html: [callObj.cqz, callObj.cqzSuffix].join(" ") }) }, ITUz: { compare: false, tableData: (callObj) => ({ name: "ITUz", rawAttrs: callObj.style.ituz, html: callObj.ituz }) }, 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.GT.lotwCallsigns) { if (CR.rosterSettings.maxLoTW < 27) { let months = (CR.day - window.opener.GT.lotwCallsigns[callObj.DEcall]) / 30; if (months > CR.rosterSettings.maxLoTW) { return { style: "color: yellow;", align: "center", title: `${$.i18n("rosterColumns.LoTW.NoUpdate")} ${toYM(Number(months))}`, html: "?" } } else { return { style: "color: #0F0;", align: "center", title: `${$.i18n("rosterColumns.LoTW.LastUpdate")}${ window.opener.userDayString(window.opener.GT.lotwCallsigns[callObj.DEcall] * 86400000) }`, html: "✔" } } } else { return { style: "color: #0F0;", align: "center", title: `${$.i18n("rosterColumns.LoTW.LastUpdate")}${ window.opener.userDayString(window.opener.GT.lotwCallsigns[callObj.DEcall] * 86400000) }`, html: "✔" } } } else { return { html: " " } } } }, eQSL: { compare: false, tableData: (callObj) => ({ style: "color: #0F0;", align: "center", html: (callObj.DEcall in window.opener.GT.eqslCallsigns ? "✔" : " ") }) }, OQRS: { compare: false, tableData: (callObj) => ({ style: "color: #0F0;", align: "center", html: (callObj.DEcall in window.opener.GT.oqrsCallsigns ? "✔" : " ") }) }, Life: { compare: callObjSimpleComparer("life"), tableData: (callObj) => ({ style: "color: #EEE;", class: "lifeCol", id: `lm${callObj.hash}`, html: toDHMS(timeNowSec() - callObj.life) }) }, 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: "" } } } else { return { html: " " } } } }, UTC: { compare: callObjSimpleComparer("age"), tableData: (callObj) => ({ style: "color: #EEE;", html: callObj.UTC }) }, Age: { compare: callObjSimpleComparer("age"), tableData: (callObj) => ({ style: "color: #EEE;", class: "timeCol", id: `tm${callObj.hash}`, html: toDHMS(timeNowSec() - callObj.age) }) }, Spot: { compare: (a, b) => { let cutoff = timeNowSec() - window.opener.GT.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: callObjSimpleComparer("pota"), tableData: (callObj) => ({ name: "POTA", rawAttrs: callObj.style.pota, title: potaColumnHover(callObj), html: potaColumnRef(callObj) }) }, 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 }) }) } } function potaColumnRef(callObj) { if (callObj.pota) { return callObj.pota; } else { return " "; } } function potaColumnHover(callObj) { let value = ""; if (callObj.pota in window.opener.GT.pota.parks) { value += callObj.pota + " - " + window.opener.GT.pota.parks[callObj.pota].name + "\n"; } return value; } WANTED_ORDER = ["call", "qrz", "watcher", "cont", "dxcc", "cqz", "ituz", "dxccMarathon", "cqzMarathon", "state", "pota", "grid", "cnty", "wpx", "oams"]; WANTED_LABELS = {}; function wantedColumnParts(callObj, options) { options = options || {}; if (Object.keys(callObj.hunting).length == 0) { let sendBlank = (typeof options.html != "undefined" && options.html == true); return [(sendBlank ? " " : "")]; } let parts = []; WANTED_ORDER.forEach(field => { let wanted = callObj.hunting[field]; if (wanted == "calling") { parts.push("Calling"); } // else if (wanted == "caller") { parts.push("Called"); } else if (wanted == "hunted" && field == "qrz") { parts.push("Caller"); } else if (wanted == "hunted" && field == "oams") { parts.push("OAMS User"); } else if (wanted == "hunted" && field == "watcher") { parts.push(callObj.watcherKey); } 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] == "Caller") { 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; }