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(250); }, 15000); }; // Clear all currently shown messages. 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(); }; // Add CLEAR button to the message list. 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); }; // Scroll to the bottom of the message list. MessagePanel.prototype.scrollToBottom = function() { var $t = $(this.el).find('tbody'); $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 linkedmsg = msg['msg']; var matches; if (this.qsoModes.indexOf(msg['mode']) >= 0) { matches = linkedmsg.match(/^(.*?)([A-Z0-9\/]+)\s([A-Z0-9\/]+)\s(([A-R]{2}[0-9]{2})|(R?[+\-]?[0-9]{2})|(RRR))$/); if (matches) { var destination = matches[2]!=='CQ' && matches[2]!=='DX' && matches[2]!=='TEST'? Utils.linkifyCallsign(matches[2]) : matches[2]; var locator = matches[5] && matches[5]!=='RR73'? Utils.linkifyLocator(matches[5]) : matches[4]; linkedmsg = Utils.htmlEscape(matches[1]) + destination + ' ' + Utils.linkifyCallsign(matches[3]) + ' ' + locator; } else { linkedmsg = Utils.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 = Utils.linkifyCallsign(matches[1]) + ' ' + Utils.linkifyLocator(matches[2]) + ' ' + Utils.htmlEscape(matches[3]); } else { linkedmsg = Utils.htmlEscape(linkedmsg); } } $b.append($( '' + '' + Utils.HHMMSS(msg['timestamp']) + '' + '' + 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') || (message['mode'] === 'AIS'); }; PacketMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '' + '
UTCCallsignCoordComment
' )); }; PacketMessagePanel.prototype.pushMessage = function(msg) { var $b = $(this.el).find('tbody'); if (msg.type && msg.type === 'thirdparty' && msg.data) { msg = msg.data; } var source = msg.source; if (msg.type) { if (msg.type === 'nmea') { // Do not show AIS-specific stuff for now return; } if (msg.type === 'item') { source = msg.item; } if (msg.type === 'object') { source = msg.object; } } var timestamp = msg.timestamp? Utils.HHMMSS(msg.timestamp) : ''; 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 = Utils.linkToMap(source, overlay, attrs); } else { link = '
' + overlay + '
' } // Linkify source based on what it is (vessel or HAM callsign) source = msg.mode === 'AIS'? Utils.linkifyVessel(source) : Utils.linkifyCallsign(source); $b.append($( '' + '' + timestamp + '' + '' + source + '' + '' + link + '' + '' + Utils.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 + '' + '' + Utils.htmlEscape(msg.message) + '' + '' )); this.scrollToBottom(); }; $.fn.pocsagMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new PocsagMessagePanel(this)); } return this.data('panel'); }; PageMessagePanel = function(el) { MessagePanel.call(this, el); this.initClearTimer(); } PageMessagePanel.prototype = Object.create(MessagePanel.prototype); PageMessagePanel.prototype.supportsMessage = function(message) { return (message['mode'] === 'FLEX') || (message['mode'] === 'POCSAG'); }; PageMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '
CapCodeModeTime
' )); }; PageMessagePanel.prototype.pushMessage = function(msg) { // Get color from the message, default to white var color = msg.hasOwnProperty('color')? msg.color : '#FFF'; // Get channel from the message (FLEX only) var channel = msg.hasOwnProperty('channel')? '/' + msg.channel : ''; // Append message header (address, time, etc) var $b = $(this.el).find('tbody'); $b.append($( '' + '' + msg.address + '' + '' + msg.mode + msg.baud + channel + '' + '' + Utils.HHMMSS(msg.timestamp) + '' + '' ).css('background-color', color).css('color', '#000')); // Append message body (text) if (msg.hasOwnProperty('message')) { $b.append($( '' + Utils.htmlEscape(msg.message) + '' )); } // Jump list to the last received message this.scrollToBottom(); }; $.fn.pageMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new PageMessagePanel(this)); } return this.data('panel'); }; HfdlMessagePanel = function(el) { MessagePanel.call(this, el); this.initClearTimer(); this.modes = ['HFDL', 'VDL2', 'ADSB', 'ACARS']; } HfdlMessagePanel.prototype = Object.create(MessagePanel.prototype); HfdlMessagePanel.prototype.supportsMessage = function(message) { return this.modes.indexOf(message['mode']) >= 0; }; HfdlMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '' + '
TimeFlightAircraftData
' )); }; HfdlMessagePanel.prototype.pushMessage = function(msg) { var bcolor = msg.color? msg.color : '#000'; var fcolor = msg.color? '#000' : '#FFF'; var data = msg.type? msg.type : ''; // Only linkify ICAO-compliant flight IDs var flight = !msg.flight? '' : !msg.flight.match(/^[A-Z]{3}[0-9]+[A-Z]*$/)? msg.flight : Utils.linkifyFlight(msg.flight); var aircraft = msg.aircraft? Utils.linkifyFlight(msg.aircraft) : msg.icao? Utils.linkifyIcao(msg.icao) : ''; var tstamp = msg.msgtime? '' + msg.msgtime + '' : msg.timestamp? Utils.HHMMSS(msg.timestamp) : ''; // Add location, altitude, speed, etc var data = ''; if (msg.lat && msg.lon) { data += '@' + msg.lat.toFixed(4) + ',' + msg.lon.toFixed(4); } if (msg.altitude) data += ' ⤒' + msg.altitude + 'ft'; if (msg.vspeed>0) data += ' ↗' + msg.vspeed + 'ft/m'; if (msg.vspeed<0) data += ' ↘' + (-msg.vspeed) + 'ft/m'; if (msg.speed) data += ' →' + msg.speed + 'kt'; if (msg.origin) data += ' ↰' + msg.origin; if (msg.destination) data += ' ↳' + msg.destination; // If no location data in the message, use message type as data if (!data.length && msg.type) data = msg.type; // Make data point to the map if (data.length && msg.mapid) data = Utils.linkToMap(msg.mapid, data); // Append report var $b = $(this.el).find('tbody'); $b.append($( '' + '' + tstamp + '' + '' + flight + '' + '' + aircraft + '' + '' + data + '' + '' ).css('background-color', bcolor).css('color', fcolor)); // Append messsage if present if (msg.message) { $b.append($( '' + Utils.htmlEscape(msg.message) + '' )); } // Jump list to the last received message this.scrollToBottom(); }; $.fn.hfdlMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new HfdlMessagePanel(this)); } return this.data('panel'); }; AdsbMessagePanel = function(el) { MessagePanel.call(this, el); this.clearButton.css('display', 'none'); } AdsbMessagePanel.prototype = Object.create(MessagePanel.prototype); AdsbMessagePanel.prototype.supportsMessage = function(message) { return message['mode'] === 'ADSB-LIST'; }; AdsbMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
FlightAircraftSquawkDistAlt (ft)Speed (kt)Signal
' )); }; AdsbMessagePanel.prototype.pushMessage = function(msg) { // Must have list of aircraft if (!msg.aircraft) return; // Create new table body var body = ''; var odd = false; msg.aircraft.forEach(entry => { // Signal strength var rssi = entry.rssi? entry.rssi + ' dB' : ''; // Flight identificators var flight = entry.flight? Utils.linkifyFlight(entry.flight) : ''; var aircraft = entry.aircraft? Utils.linkifyFlight(entry.aircraft) : entry.icao? Utils.linkifyIcao(entry.icao) : ''; // Altitude and climb / descent var alt = entry.altitude? '' + entry.altitude : ''; if (entry.vspeed) { var vspeed = entry.vspeed; vspeed = vspeed>0? vspeed + '↑' : (-vspeed) + '↓'; alt = vspeed + ' '.repeat(6 - alt.length) + alt; } // Speed and direction var speed = entry.speed? '' + entry.speed : ''; if (entry.course) { var dir = Utils.degToCompass(entry.course); speed = dir + ' '.repeat(5 - speed.length) + speed; } // Replace squawk with emergency status, if present var squawk = entry.squawk? entry.squawk : ''; if (entry.emergency && (entry.emergency!=='NONE')) { squawk = '
 ' + entry.emergency + ' 
'; } // Compute distance to the receiver var distance = ''; var receiver_pos = Utils.getReceiverPos(); if (receiver_pos && entry.lat && entry.lon) { var id = entry.icao? entry.icao : entry.aircraft? entry.aircraft : entry.flight? entry.flight : null; distance = Utils.distanceKm(entry, receiver_pos) + ' km'; if (id) distance = Utils.linkToMap(id, distance); } body += '' + '' + flight + '' + '' + aircraft + '' + '' + squawk + '' + '' + distance + '' + '' + alt + '' + '' + speed + '' + '' + rssi + '' + '\n'; odd = !odd; }); // Assign new table body $(this.el).find('tbody').html(body); }; $.fn.adsbMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new AdsbMessagePanel(this)); } return this.data('panel'); }; DscMessagePanel = function(el) { MessagePanel.call(this, el); this.initClearTimer(); } DscMessagePanel.prototype = Object.create(MessagePanel.prototype); DscMessagePanel.prototype.supportsMessage = function(message) { return message['mode'] === 'DSC'; }; DscMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '' + '
UTCFromToData
' )); }; DscMessagePanel.prototype.pushMessage = function(msg) { var bcolor = msg.color? msg.color : '#000'; var fcolor = msg.color? '#000' : '#FFF'; var src = msg.src? Utils.linkifyVessel(msg.src) : ''; var dst = msg.dst? Utils.linkifyVessel(msg.dst) : ''; var data = ( (msg.category? ' ' + msg.category : '') + (msg.format? ' ' + msg.format : '') + (msg.eos? ' ' + msg.eos : '') + (!msg.ecc && !msg.data? ' ?' : '') ).trim().toUpperCase(); // Format timestamp var timestamp = msg.time? '' + msg.time + '' : msg.timestamp? Utils.HHMMSS(msg.timestamp) : ''; // Format debugging data var symbols = ''; if (msg.data) { symbols = msg.data.replace( /(.*)\|(.*)/, ' $1 | $2 …' ); } // Combine remaining attributes into a message var message = ( (msg.distress? ' ' + msg.distress : '') + (msg.id? ' SHIP ' + Utils.linkifyVessel(msg.id) : '') + (msg.loc? ' AT ' + msg.loc : '') + (msg.num? ' DIAL ' + msg.num : '') + (msg.rxfreq? ' RX ' + Utils.printFreq(msg.rxfreq) : '') + (msg.txfreq? ' TX ' + Utils.printFreq(msg.txfreq) : '') + symbols ).trim(); // Append report var $b = $(this.el).find('tbody'); $b.append($( '' + '' + timestamp + '' + '' + src + '' + '' + dst + '' + '' + data + '' + '' ).css('background-color', bcolor).css('color', fcolor)); // Append messsage if present if (message) { $b.append($( '' + message + '' )); } // Jump list to the last received message this.scrollToBottom(); }; $.fn.dscMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new DscMessagePanel(this)); } return this.data('panel'); }; IsmMessagePanel = function(el) { MessagePanel.call(this, el); this.initClearTimer(); // These are basic message attributes this.basicInfo = ['mode', 'id', 'model', 'timestamp', 'freq', 'color']; } IsmMessagePanel.prototype = Object.create(MessagePanel.prototype); IsmMessagePanel.prototype.supportsMessage = function(message) { return message['mode'] === 'ISM'; }; IsmMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '' + '' + '
IDDeviceTime
' )); }; IsmMessagePanel.prototype.formatAttr = function(msg, key) { return('' + '
' + '' + key + '' + '' + msg[key] + '' + '
' ); }; IsmMessagePanel.prototype.pushMessage = function(msg) { // Get basic information, assume white color if missing var address = msg.hasOwnProperty('id')? msg.id : '???'; var device = msg.hasOwnProperty('model')? msg.model : ''; var tstamp = msg.hasOwnProperty('timestamp')? Utils.HHMMSS(msg.timestamp) : ''; var color = msg.hasOwnProperty('color')? msg.color : '#FFF'; // Append message header (address, time, etc) var $b = $(this.el).find('tbody'); $b.append($( '' + '' + address + '' + '' + device + '' + '' + tstamp + '' + '' ).css('background-color', color).css('color', '#000')); // Append attributes in pairs, skip basic information var last = null; for (var key in msg) { if (this.basicInfo.indexOf(key) < 0) { var cell = this.formatAttr(msg, key); if (!last) { last = cell; } else { $b.append($('' + last + cell + '')); last = null; } } } // Last row if (last) $b.append($('' + last + '')); // Jump list to the last received message this.scrollToBottom(); }; $.fn.ismMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new IsmMessagePanel(this)); } return this.data('panel'); }; SstvMessagePanel = function(el) { MessagePanel.call(this, el); this.initClearTimer(); } SstvMessagePanel.prototype = Object.create(MessagePanel.prototype); SstvMessagePanel.prototype.supportsMessage = function(message) { return message['mode'] === 'SSTV'; }; SstvMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '
TV
' )); }; SstvMessagePanel.prototype.pushMessage = function(msg) { var $b = $(this.el).find('tbody'); if(msg.hasOwnProperty('message')) { // Append a new debug message text // See service log for debug output instead // $b.append($('' + msg.message + '')); // this.scrollToBottom(); } else if(msg.width>0 && msg.height>0 && !msg.hasOwnProperty('line')) { var f = msg.frequency>0? ' at ' + Math.floor(msg.frequency/1000) + 'kHz' : ''; var h = '
' + msg.timestamp + ' ' + msg.width + 'x' + msg.height + ' ' + msg.sstvMode + f + '
'; var c = '
' + '
'; // Append a new canvas $b.append($('' + h + c + '')); $b.scrollTop($b[0].scrollHeight); // Save canvas context and dimensions for future use this.ctx = $(this.el).find('canvas').get(-1).getContext("2d"); this.width = msg.width; this.height = msg.height; } else if(msg.width>0 && msg.height>0 && msg.line>=0 && msg.hasOwnProperty('pixels')) { // Will copy pixels to img var pixels = atob(msg.pixels); var img = this.ctx.createImageData(msg.width, 1); // Convert BMP BGR pixels into HTML RGBA pixels for (var x = 0; x < msg.width; x++) { img.data[x*4 + 0] = pixels.charCodeAt(x*3 + 2); img.data[x*4 + 1] = pixels.charCodeAt(x*3 + 1); img.data[x*4 + 2] = pixels.charCodeAt(x*3 + 0); img.data[x*4 + 3] = 0xFF; } // Render scanline this.ctx.putImageData(img, 0, msg.line); } }; $.fn.sstvMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new SstvMessagePanel(this)); } return this.data('panel'); }; FaxMessagePanel = function(el) { MessagePanel.call(this, el); this.initClearTimer(); } FaxMessagePanel.prototype = Object.create(MessagePanel.prototype); FaxMessagePanel.prototype.supportsMessage = function(message) { return message['mode'] === 'Fax'; }; FaxMessagePanel.prototype.render = function() { $(this.el).append($( '' + '' + '' + '' + '' + '
Fax
' )); }; FaxMessagePanel.prototype.pushMessage = function(msg) { var $b = $(this.el).find('tbody'); if(msg.hasOwnProperty('message')) { // Append a new debug message text // See service log for debug output instead // $b.append($('' + msg.message + '')); // this.scrollToBottom(); } else if(msg.width>0 && msg.height>0 && !msg.hasOwnProperty('line')) { var f = msg.frequency>0? ' at ' + Math.floor(msg.frequency/1000) + 'kHz' : ''; var h = '
' + msg.timestamp + ' ' + msg.width + 'x' + msg.height + ' ' + msg.faxMode + f + '
'; var c = '
' + '
'; // Append a new canvas $b.append($('' + h + c + '')); this.scrollToBottom(); // Save canvas context and dimensions for future use this.ctx = $(this.el).find('canvas').get(-1).getContext("2d"); this.width = msg.width; this.height = msg.height; } else if(msg.width>0 && msg.height>0 && msg.line>=0 && msg.hasOwnProperty('pixels')) { // Will copy pixels to img var img = this.ctx.createImageData(msg.width, 1); var pixels; // Unpack RLE-compressed line of pixels if(!msg.rle) { pixels = atob(msg.pixels); } else { var rle = atob(msg.pixels); pixels = ''; for(var x=0 ; x' + '' + 'Freq' + 'Text' + '' + '' + '' )); }; CwSkimmerMessagePanel.prototype.pushMessage = function(msg) { // Must have some text if (!msg.text) return; // Clear cache if requested if (msg.changed) this.texts = []; // Current time var now = Date.now(); // Modify or add a new entry var j = this.texts.findIndex(function(x) { return x.freq >= msg.freq }); if (j < 0) { // Append a new entry this.texts.push({ freq: msg.freq, text: msg.text, ts: now }); } else if (this.texts[j].freq == msg.freq) { // Update existing entry this.texts[j].text = (this.texts[j].text + msg.text).slice(-64); this.texts[j].ts = now; } else { // Insert a new entry this.texts.splice(j, 0, { freq: msg.freq, text: msg.text, ts: now }); } // Generate table body var body = ''; for (var j = 0 ; j < this.texts.length ; j++) { // Limit the lifetime of entries depending on their length var cutoff = 5000 * this.texts[j].text.length; if (now - this.texts[j].ts >= cutoff) { this.texts.splice(j--, 1); } else { var f = Math.floor(this.texts[j].freq / 100.0) / 10.0; body += '' + f.toFixed(1) + '' + this.texts[j].text + '\n'; } } // Assign new table body $(this.el).find('tbody').html(body); }; $.fn.cwskimmerMessagePanel = function() { if (!this.data('panel')) { this.data('panel', new CwSkimmerMessagePanel(this)); } return this.data('panel'); };