function MessagePanel(el) { this.el = el; this.render(); this.initClearButton(); } MessagePanel.prototype.supportsMessage = function(message) { return false; }; MessagePanel.prototype.render = function() { }; MessagePanel.prototype.pushMessage = function(message) { }; // automatic clearing is not enabled by default. call this method from the constructor to enable MessagePanel.prototype.initClearTimer = function() { var me = this; if (me.removalInterval) clearInterval(me.removalInterval); me.removalInterval = setInterval(function () { me.clearMessages(1000); }, 15000); }; MessagePanel.prototype.clearMessages = function(toRemain) { var $elements = $(this.el).find('tbody tr'); // limit to 1000 entries in the list since browsers get laggy at some point var toRemove = $elements.length - toRemain; if (toRemove <= 0) return; $elements.slice(0, toRemove).remove(); }; MessagePanel.prototype.initClearButton = function() { var me = this; me.clearButton = $( '
Clear
' ); me.clearButton.css({ position: 'absolute', top: '10px', right: '10px' }); me.clearButton.on('click', function() { me.clearMessages(0); }); $(me.el).append(me.clearButton); }; MessagePanel.prototype.htmlEscape = function(input) { return $('
').text(input).html() }; MessagePanel.prototype.scrollToBottom = function() { var $t = $(this.el).find('table'); $t.scrollTop($t[0].scrollHeight); }; function WsjtMessagePanel(el) { MessagePanel.call(this, el); this.initClearTimer(); this.qsoModes = ['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65', 'MSK144']; this.beaconModes = ['WSPR', 'FST4W']; this.modes = [].concat(this.qsoModes, this.beaconModes); } WsjtMessagePanel.prototype = Object.create(MessagePanel.prototype); WsjtMessagePanel.prototype.supportsMessage = function(message) { return this.modes.indexOf(message['mode']) >= 0; }; WsjtMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '' + '' + '
UTCdBDTFreqMessage
' )); }; WsjtMessagePanel.prototype.pushMessage = function(msg) { var $b = $(this.el).find('tbody'); var t = new Date(msg['timestamp']); var pad = function (i) { return ('' + i).padStart(2, "0"); }; var linkedmsg = msg['msg']; var matches; if (this.qsoModes.indexOf(msg['mode']) >= 0) { matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); if (matches && matches[2] !== 'RR73') { linkedmsg = this.htmlEscape(matches[1]) + '' + matches[2] + ''; } else { linkedmsg = this.htmlEscape(linkedmsg); } } else if (this.beaconModes.indexOf(msg['mode']) >= 0) { matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/); if (matches) { linkedmsg = this.htmlEscape(matches[1]) + '' + matches[2] + '' + this.htmlEscape(matches[3]); } else { linkedmsg = this.htmlEscape(linkedmsg); } } $b.append($( '' + '' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '' + '' + msg['db'] + '' + '' + msg['dt'] + '' + '' + msg['freq'] + '' + '' + linkedmsg + '' + '' )); this.scrollToBottom(); } $.fn.wsjtMessagePanel = function(){ if (!this.data('panel')) { this.data('panel', new WsjtMessagePanel(this)); } return this.data('panel'); }; function PacketMessagePanel(el) { MessagePanel.call(this, el); this.initClearTimer(); } PacketMessagePanel.prototype = Object.create(MessagePanel.prototype); PacketMessagePanel.prototype.supportsMessage = function(message) { return message['mode'] === 'APRS'; }; PacketMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '' + '
UTCCallsignCoordComment
' )); }; PacketMessagePanel.prototype.pushMessage = function(msg) { var $b = $(this.el).find('tbody'); var pad = function (i) { return ('' + i).padStart(2, "0"); }; if (msg.type && msg.type === 'thirdparty' && msg.data) { msg = msg.data; } var source = msg.source; var callsign; if ('object' in source) { callsign = source.object; } else if ('item' in source) { callsign = source.item; } else { callsign = source.callsign; if ('ssid' in source) { callsign += '-' + source.ssid; } } var timestamp = ''; if (msg.timestamp) { var t = new Date(msg.timestamp); timestamp = pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) } var link = ''; var classes = []; var styles = {}; var overlay = ''; var stylesToString = function (s) { return $.map(s, function (value, key) { return key + ':' + value + ';' }).join('') }; if (msg.symbol) { classes.push('aprs-symbol'); classes.push('aprs-symboltable-' + (msg.symbol.table === '/' ? 'normal' : 'alternate')); styles['background-position-x'] = -(msg.symbol.index % 16) * 15 + 'px'; styles['background-position-y'] = -Math.floor(msg.symbol.index / 16) * 15 + 'px'; if (msg.symbol.table !== '/' && msg.symbol.table !== '\\') { var s = {}; s['background-position-x'] = -(msg.symbol.tableindex % 16) * 15 + 'px'; s['background-position-y'] = -Math.floor(msg.symbol.tableindex / 16) * 15 + 'px'; overlay = '
'; } } else if (msg.lat && msg.lon) { classes.push('openwebrx-maps-pin'); overlay = ''; } var attrs = [ 'class="' + classes.join(' ') + '"', 'style="' + stylesToString(styles) + '"' ].join(' '); if (msg.lat && msg.lon) { link = '' + overlay + ''; } else { link = '
' + overlay + '
' } $b.append($( '' + '' + timestamp + '' + '' + callsign + '' + '' + link + '' + '' + this.htmlEscape(msg.comment || msg.message || '') + '' + '' )); this.scrollToBottom(); }; $.fn.packetMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new PacketMessagePanel(this)); } return this.data('panel'); }; PocsagMessagePanel = function(el) { MessagePanel.call(this, el); this.initClearTimer(); } PocsagMessagePanel.prototype = Object.create(MessagePanel.prototype); PocsagMessagePanel.prototype.supportsMessage = function(message) { return message['mode'] === 'Pocsag'; }; PocsagMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '
AddressMessage
' )); }; PocsagMessagePanel.prototype.pushMessage = function(msg) { var $b = $(this.el).find('tbody'); $b.append($( '' + '' + msg.address + '' + '' + this.htmlEscape(msg.message) + '' + '' )); this.scrollToBottom(); }; $.fn.pocsagMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new PocsagMessagePanel(this)); } return this.data('panel'); }; AdsbMessagePanel = function(el) { MessagePanel.call(this, el); this.aircraft = {} this.aircraftTrackingService = false; this.initClearTimer(); } AdsbMessagePanel.prototype = Object.create(MessagePanel.prototype); AdsbMessagePanel.prototype.supportsMessage = function(message) { return message["mode"] === "ADSB"; }; AdsbMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
ICAOFlightAltitudeSpeedTrackV/SPositionMessages
' )); }; AdsbMessagePanel.prototype.pushMessage = function(message) { if (!('icao' in message)) return; if (!(message.icao in this.aircraft)) { var el = $(""); $(this.el).find('tbody').append(el); this.aircraft[message.icao] = { el: el, messages: 0 } } var state = this.aircraft[message.icao]; Object.assign(state, message); state.lastSeen = Date.now(); state.messages += 1; var ifDefined = function(input, formatter) { if (typeof(input) !== 'undefined') { if (formatter) return formatter(input); return input; } return ""; } var coordRound = function(i) { return Math.round(i * 1000) / 1000; } var getPosition = function(state) { if (!('lat' in state) || !('lon') in state) return ''; return '' + coordRound(state.lat) + ', ' + coordRound(state.lon) + ''; } state.el.html( '' + this.linkify(state, state.icao) + '' + '' + this.linkify(state, ifDefined(state.identification)) + '' + '' + ifDefined(state.altitude) + '' + '' + ifDefined(state.groundspeed || state.IAS || state.TAS, Math.round) + '' + '' + ifDefined(state.groundtrack || state.heading, Math.round) + '' + '' + ifDefined(state.verticalspeed) + '' + '' + getPosition(state) + '' + '' + state.messages + '' ); }; AdsbMessagePanel.prototype.clearMessages = function(toRemain) { var now = Date.now(); var me = this; Object.entries(this.aircraft).forEach(function(e) { if (now - e[1].lastSeen > toRemain) { delete me.aircraft[e[0]]; e[1].el.remove(); } }) }; AdsbMessagePanel.prototype.initClearTimer = function() { var me = this; if (me.removalInterval) clearInterval(me.removalInterval); me.removalInterval = setInterval(function () { me.clearMessages(30000); }, 15000); }; AdsbMessagePanel.prototype.setAircraftTrackingService = function(service) { this.aircraftTrackingService = service; }; AdsbMessagePanel.prototype.linkify = function(state, text) { var link = false; switch (this.aircraftTrackingService) { case 'flightaware': link = 'https://flightaware.com/live/modes/' + state.icao; if (state.identification) link += "/ident/" + state.identification link += '/redirect'; break; case 'planefinder': if (state.identification) link = 'https://planefinder.net/flight/' + state.identification; break; } if (link) { return '' + text + ''; } return text; }; $.fn.adsbMessagePanel = function () { if (!this.data('panel')) { this.data('panel', new AdsbMessagePanel(this)); } return this.data('panel'); }; IsmMessagePanel = function(el) { MessagePanel.call(this, el); this.initClearTimer(); }; IsmMessagePanel.prototype = Object.create(MessagePanel.prototype); IsmMessagePanel.prototype.supportsMessage = function(message) { return message['mode'] === 'ISM'; }; IsmMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '' + '
ModelIDChannelData
' )); }; IsmMessagePanel.prototype.pushMessage = function(message) { var $b = $(this.el).find('tbody'); var ifDefined = function(input, formatter) { if (typeof(input) !== 'undefined') { if (formatter) return formatter(input); return input; } return ""; } var mergeRemainingMessage = function(input, exclude) { return Object.entries(input).map(function(entry) { if (exclude.includes(entry[0])) return ''; return entry[0] + ': ' + entry[1] + ';'; }).join(' '); } $b.append($( '' + '' + ifDefined(message.model) + '' + '' + ifDefined(message.id) + '' + '' + ifDefined(message.channel) + '' + '' + this.htmlEscape(mergeRemainingMessage(message, ['model', 'id', 'channel', 'mode', 'time'])) + '' + '' )); this.scrollToBottom(); }; $.fn.ismMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new IsmMessagePanel(this)); } return this.data('panel'); }; AircraftMessagePanel = function(el) { MessagePanel.call(this, el); } AircraftMessagePanel.prototype = Object.create(MessagePanel.prototype); AircraftMessagePanel.prototype.renderAcars = function(acars) { if (acars['more']) { return '

Partial ACARS message

'; } var details = '

ACARS message

'; if ('flight' in acars) { details += '
Flight: ' + this.handleFlight(acars['flight']) + '
'; } details += '
Registration: ' + acars['reg'].replace(/^\.+/g, '') + '
'; if ('media-adv' in acars) { details += '
Media advisory
'; var mediaadv = acars['media-adv']; if ('current_link' in mediaadv) { details += '
Current link: ' + mediaadv['current_link']['descr']; } if ('links_avail' in mediaadv) { details += '
Available links: ' + mediaadv['links_avail'].map(function (l) { return l['descr']; }).join(', ') + '
'; } } else if ('arinc622' in acars) { var arinc622 = acars['arinc622']; if ('adsc' in arinc622) { var adsc = arinc622['adsc']; if ('tags' in adsc) { adsc['tags'].forEach(function(tag) { if ('basic_report' in tag) { var basic_report = tag['basic_report']; details += '
Basic ADS-C report
'; details += '
Position: ' + basic_report['lat'] + ', ' + basic_report['lon'] + '
'; details += '
Altitude: ' + basic_report['alt'] + '
'; } else if ('cancel_all_contracts' in tag) { details += '
Cancel all ADS-C contracts
'; } else if ('cancel_contract' in tag) { details += '
Cancel ADS-C contract
'; } else { details += '
Unsupported tag
'; } }); } else { details += '
Other ADS-C data
'; } } } else { // plain text details += '
Label: ' + acars['label'] + '
'; details += '
' + acars['msg_text'] + '
'; } return details; }; AircraftMessagePanel.prototype.handleFlight = function(raw) { return raw.replace(/^([0-9A-Z]{2})0*([0-9A-Z]+$)/, '$1$2'); }; HfdlMessagePanel = function(el) { AircraftMessagePanel.call(this, el); this.initClearTimer(); } HfdlMessagePanel.prototype = Object.create(AircraftMessagePanel.prototype); HfdlMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '
SourceDestinationDetails
' )); }; HfdlMessagePanel.prototype.supportsMessage = function(message) { return message['mode'] === 'HFDL'; }; HfdlMessagePanel.prototype.renderPosition = function(hfnpdu) { if ('pos' in hfnpdu) { var pos = hfnpdu['pos']; var lat = pos['lat'] || 180; var lon = pos['lon'] || 180; if (Math.abs(lat) <= 90 && Math.abs(lon) <= 180) { return '
Position: ' + pos['lat'] + ', ' + pos['lon'] + '
'; } } return ''; }; HfdlMessagePanel.prototype.renderLogon = function(lpdu) { var details = '' if (lpdu['ac_info'] && lpdu['ac_info']['icao']) { details += '
ICAO: ' + lpdu['ac_info']['icao'] + '
'; } if (lpdu['hfnpdu']) { var hfnpdu = lpdu['hfnpdu']; if (hfnpdu['flight_id'] && hfnpdu['flight_id'] !== '') { details += '
Flight: ' + this.handleFlight(lpdu['hfnpdu']['flight_id']) + '
' } details += this.renderPosition(hfnpdu); } return details; }; HfdlMessagePanel.prototype.pushMessage = function(message) { var $b = $(this.el).find('tbody'); var src = ''; var dst = ''; var details = JSON.stringify(message); var renderAddress = function(a) { return a['id']; } // TODO remove safety net once parsing is complete try { var payload = message['hfdl']; if ('spdu' in payload) { var spdu = payload['spdu']; src = renderAddress(spdu['src']); details = '

HFDL Squitter message

' details += '
Systable version: ' + spdu['systable_version'] + '
'; if ('gs_status' in spdu) { details += spdu['gs_status'].map(function(gs){ return '
Ground station ' + gs['gs']['id'] + ' is operating on frequency ids ' + gs['freqs'].map(function(f) {return f['id']; }).join(', ') + '
'; }).join('') } } else if ('lpdu' in payload) { var lpdu = payload['lpdu']; src = renderAddress(lpdu['src']); dst = renderAddress(lpdu['dst']); if (lpdu['type']['id'] === 13 || lpdu['type']['id'] === 29) { // unnumbered data var hfnpdu = lpdu['hfnpdu']; if (hfnpdu['type']['id'] === 209) { // performance data details = '

Performance data

'; details += '
Flight: ' + this.handleFlight(hfnpdu['flight_id']) + '
'; details += this.renderPosition(hfnpdu); } else if (hfnpdu['type']['id'] === 255) { // enveloped data if ('acars' in hfnpdu) { details = this.renderAcars(hfnpdu['acars']); } } } else if (lpdu['type']['id'] === 47) { // logon denied details = '

Logon denied

'; } else if (lpdu['type']['id'] === 63) { details = '

Logoff request

'; if (lpdu['ac_info'] && lpdu['ac_info']['icao']) { details += '
ICAO: ' + lpdu['ac_info']['icao'] + '
'; } } else if (lpdu['type']['id'] === 79) { details = '

Logon resume

'; details += this.renderLogon(lpdu); } else if (lpdu['type']['id'] === 95) { details = '

Logon resume confirmation

'; } else if (lpdu['type']['id'] === 143) { details = '

Logon request

'; details += this.renderLogon(lpdu); } else if (lpdu['type']['id'] === 159) { details = '

Logon confirmation

'; if (lpdu['ac_info'] && lpdu['ac_info']['icao']) { details += '
ICAO: ' + lpdu['ac_info']['icao'] + '
'; } if (lpdu['assigned_ac_id']) { details += '
Assigned aircraft ID: ' + lpdu['assigned_ac_id'] + '
'; } } else if (lpdu['type']['id'] === 191) { details = '

Logon request (DLS)

'; details += this.renderLogon(lpdu); } } } catch (e) { console.error(e, e.stack); } $b.append($( '' + '' + src + '' + '' + dst + '' + '' + details + '' + '' )); this.scrollToBottom(); }; $.fn.hfdlMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new HfdlMessagePanel(this)); } return this.data('panel'); }; Vdl2MessagePanel = function(el) { AircraftMessagePanel.call(this, el); this.initClearTimer(); } Vdl2MessagePanel.prototype = Object.create(AircraftMessagePanel.prototype); Vdl2MessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '
SourceDestinationDetails
' )); }; Vdl2MessagePanel.prototype.supportsMessage = function(message) { return message['mode'] === 'VDL2'; }; Vdl2MessagePanel.prototype.pushMessage = function(message) { var $b = $(this.el).find('tbody'); var src = ''; var dst = ''; var details = JSON.stringify(message); var renderAddress = function(a) { return '
' + a['addr'] + '
' + a['type'] + ( 'status' in a ? ' (' + a['status'] + ')' : '' ) + '
' } // TODO remove safety net once parsing is complete try { var payload = message['vdl2']; if ('avlc' in payload) { var avlc = payload['avlc']; src = renderAddress(avlc['src']); dst = renderAddress(avlc['dst']); if (avlc['frame_type'] === 'S') { details = '

Supervisory frame

'; if (avlc['cmd'] === 'Receive Ready') { details = '

Receive Ready

'; } } else if (avlc['frame_type'] === 'I') { details = '

Information frame

'; if ('acars' in avlc) { details = this.renderAcars(avlc['acars']); } else if ('x25' in avlc) { var x25 = avlc['x25']; if (!('reasm_status' in x25) || ['skipped', 'complete'].includes(x25['reasm_status'])) { details = '

X.25 frame

'; if ('clnp' in x25) { var clnp = x25['clnp'] if ('cotp' in clnp) { var cotp = clnp['cotp']; if ('cpdlc' in cotp) { var cpdlc = cotp['cpdlc']; details = '

CPDLC

'; if ('atc_downlink_message' in cpdlc) { var atc_downlink_message = cpdlc['atc_downlink_message']; if ('msg_data' in atc_downlink_message) { var msg_data = atc_downlink_message['msg_data']; if ('msg_elements' in msg_data) { details += '
' + msg_data['msg_elements'].map(function(e) { return e['msg_element']['choice_label']; }).join(', ') + '
'; } } else { details += '
' + JSON.stringify(cpdlc) + '
'; } } } if ('adsc_v2' in cotp) { var adsc_v2 = cotp['adsc_v2'] details = '

ADS-C v2 Frame

'; if ('adsc_report' in adsc_v2) { var adsc_report = adsc_v2['adsc_report']; var data = adsc_report['data']; if ('periodic_report' in data) { details += '
Periodic report
'; details += this.processReport(data['periodic_report']); } else if ('event_report' in data) { details += '
Event report
'; details += this.processReport(data['event_report']); } } } } } } else { details = '

Partial X.25 frame

'; } } } else if (avlc['frame_type'] === 'U') { details = '

Unnumbered frame

'; if ('xid' in avlc) { var xid = avlc['xid']; details = '

' + xid['type_descr'] + '

'; } } } } catch (e) { console.error(e, e.stack); } $b.append($( '' + '' + src + '' + '' + dst + '' + '' + details + '' + '' )); this.scrollToBottom(); }; Vdl2MessagePanel.prototype.processReport = function(report) { var details = ''; if ('position' in report) { var lat = position['lat'] var lon = position['lon'] details += '
Position: ' + lat['deg'] + '° ' + lat['min'] + '\' ' + lat['sec'] + '" ' + lat['dir'] + ',' + lon['deg'] + '° ' + lat['min'] + '\' ' + lat['sec'] + '" ' + lat['dir'] + '
'; details += '
Altitude: ' + position['alt']['val'] + '
'; } return details; } $.fn.vdl2MessagePanel = function() { if (!this.data('panel')) { this.data('panel', new Vdl2MessagePanel(this)); } return this.data('panel'); };