/* * TablesServlet specific JavaScript * * TODO: add enum descriptions to schema and use them for converting states, and * for calc'ing the "next" state * TODO: improve performance when client is sitting on page while lengthy list is loaded into JMRI * TODO I18N titles and headers * TODO: debug JMRI handling for firing route, signalHead lit and held, signalMast lit and held */ var jmri = null; //convert page parms into array to use for filtering rows const urlSearchParams = new URLSearchParams(window.location.search); const params = Object.fromEntries(urlSearchParams.entries()); //handle an error message returned via the websocket from the server // parms: html error code, message is the message text function showError(code, message) { $("#activity-alert").removeClass("show").addClass("hidden"); $("table#jmri-data").removeClass("show").addClass("hidden"); $("#warning-no-data").removeClass("show").addClass("hidden"); $("#error-message").html("Error " + code + ":" + message); $("#error-message").removeClass("hidden").addClass("show"); } //parm is the array of items from which to build table function rebuildTable(data) { tableType = $("html").data("table-type"); if (data[0] && data[0].type !== tableType) { if (tableType.startsWith(data[0].type) || (tableType=="memories" && data[0].type=="memory") || (tableType=="roster" && data[0].type=="rosterEntry")) { //server returns singular for plural request window.location = "/tables/" + data[0].type; //redirect to singular page } jmri.log("incoming type '" + data[0].type + "' does not match current page '" + tableType + "', ignoring."); return; } $("#activity-alert").removeClass("hidden").addClass("show"); $("table#jmri-data").removeClass("show").addClass("hidden"); $("#warning-no-data").removeClass("show").addClass("hidden"); $("#error-message").removeClass("show").addClass("hidden"); if (data.length) { //build header row from first row of data var thead = ''; $.each(Object.keys(data[0].data), function (index, value) { thead += "" + value + ""; }); thead += ''; $("table#jmri-data thead").html(thead); //build all data rows for table body var tbody = ''; var rows = 0; data.forEach(function (item) { //loop thru rows in json var keep = true; $.each(params, function (index, value) { //compare against filter parms, skipping unless all match if (displayCellValue(item.type, index, item.data[index]).toLowerCase() != value.toLowerCase()) { keep = false; jmri.log("no match for " +index+"="+value+" != " + item.data[index]) return false; } }); if (keep) { jmri.socket.send(item.type, { name: item.data.name }); //request updates from server tbody += ""; tbody += buildRow(item.data) + ''; rows++; } }); if (rows) { $("table#jmri-data tbody").html(tbody); $("table#jmri-data").removeClass("hidden").addClass("show"); var newTableObject = document.getElementById("jmri-data"); sorttable.makeSortable(newTableObject); hideEmptyColumns("table#jmri-data tr th"); sortByFirstColumn(); } else { $("#warning-no-data").removeClass("hidden").addClass("show"); } } else { $("#warning-no-data").removeClass("hidden").addClass("show"); } $("#activity-alert").removeClass("show").addClass("hidden"); //setup for clicking on certain columns to send state changes $('table.idTag,table.light,table.route,table.sensor,table.turnout,table.car,table.engine,table.signalHead,table.signalMast') .on('click', 'td.locationUnknown,td.outOfService,td.state,td.lit,td.held', function (e) { rowName = $(this).parent('tr').data('name'); colName = e.target.className; currValue = $(this).data('value').toString(); jmri.socket.send(tableType, { 'name': rowName, [colName]: getNextValue(currValue) }, 'post'); }); } //handle the toggling of the next Value for clicks var getNextValue = function(value){ switch (value) { case '0': return '4'; case '1': return '4'; case '2': return '4'; case '4': return '2'; case 'true': return 'false'; case 'false': return 'true'; case 'yes': return 'no'; case 'no': return 'yes'; default: return value; //no match, leave it the same } }; //returns the html for a single row from that row's data object function buildRow(data) { var r = ""; tableType = $("html").data("table-type"); //note: syntax below required since some JMRI json objects have a "length" attribute equal 0 $.each(Object.keys(data), function (index, value) { r += "" + displayCellValue(tableType, value, data[value]) + ""; }); return r; } //find row by key and replace it with generated html for that row function replaceRow(key, data) { var row = $("table#jmri-data tr[data-name='" + key + "']"); if ($(row).length) { row.html(buildRow(data)); hideEmptyColumns("table#jmri-data tr th"); } else { jmri.log("row not found for name='" + key + "'"); } } /* convert each cell into more human-readable form */ function displayCellValue(type, colName, value) { if (value == null) { return ""; //return empty string for any null value } if (type == "type" && colName == "name"){ //if list is of json types, return "" + value + ""; // create link to table for this type } if ($.isArray(value)) { if (value.length == 0) { return ""; } if (typeof value[0] === "object") { //check first item to see if associative array ret = ""; comma = ""; value.forEach(function(item) { //return list if (item) { if (item.name) { ret += comma + item.name; if (item.value) { ret += "=" + item.value; } else if (item.userName) { ret += " " + item.userName; } else if (item.label) { ret += " " + item.label; } } else { Object.keys(item).forEach(function(key) { //otherwise list the array of pairs ret += comma + key + ":" + item[key]; }); } comma = ", "; } }); return ret; } ret = ""; comma = ""; value.forEach(function(item) { //otherwise build and return simple array list ret += comma + item; comma = ", "; }); return ret; } if (typeof value === "object") { if (value.type == "idTag"){ return value.data.userName // handle idTag object by displaying userName } else if (value.userName) { return value.userName; // return userName of object if it has one } else if (value.name) { return value.name; // return name of object if it has one } else { return "[obj]"; //placeholder } } //convert known states to human-readable strings, if not known show as is if ((colName == "state") || (colName == "occupiedSense")) { switch (type) { case "turnout": switch (value) { case jmri.UNKNOWN: return "unknown"; case jmri.CLOSED: return "closed"; case jmri.THROWN: return "thrown"; case jmri.INCONSISTENT: return "inconsistent"; default: return value; } case "route": case "sensor": case "layoutBlock": switch (value) { case jmri.UNKNOWN: return "unknown"; case jmri.ACTIVE: return "active"; case jmri.INACTIVE: return "inactive"; case jmri.INCONSISTENT: return "inconsistent"; default: return value; } case "block": switch (value) { case jmri.UNKNOWN: return "unknown"; case 2: return "occupied"; case 4: return "unoccupied"; default: return value; } case "light": switch (value) { case jmri.UNKNOWN: return "unknown"; case 2: return "on"; case 4: return "off"; default: return value; } default: return value; //not special, just return the passed in value } } return value; //otherwise return unchanged value } //replace some special chars with html equivalents function htmlEncode(html) { return document.createElement('a').appendChild( document.createTextNode(html)).parentNode.innerHTML; }; function hideEmptyColumns(selector) { $(selector).each(function (index) { //select all tds in this column var tds = $(this).parents('table').find('tr td:nth-child(' + (index + 1) + ')'); //check if all the cells in this column are empty if (tds.length === tds.filter(':empty').length) { $(this).addClass("hidden"); //hide header tds.addClass("hidden"); //hide cells } else { $(this).removeClass("hidden"); //show header tds.removeClass("hidden"); //show cells } }); } function sortByFirstColumn() { var firstTH = document.getElementsByTagName("th")[0]; sorttable.innerSortFunction.apply(firstTH, []); } //-----------------------------------------javascript processing starts here (main) --------------------------------------------- $(document).ready(function () { // add table type to heading and title $("#table-type").text($("html").data("table-type")); document.title = $("h1.title").text(); //show filter if used if (!$.isEmptyObject(params)) { $('#filter-text').text("Filter: " + urlSearchParams.toString().replace("&",", ")); } jmri = $.JMRI({ // when we get the hello message, send a websocket list request which // returns the list and sets up change listeners // note: the functions and parameter names must match exactly those in jquery.jmri.js hello: function (data) { jmri.getList($("html").data("table-type")); // request list and updates for the table-type }, // everything calls console() console: function (originalData) { var data = JSON.parse(originalData); // jmri.log("in console: data=" + JSON.stringify(data).substr(0, 180) + "..."); if ($.isArray(data)) { // if its an array, rebuildTable(data); // replace the table with the array list } else if ((data.type) && (data.type === "error")) { showError(data.data.code, data.data.message); //display any errors returned } else if ((data.type) && (!data.type.match("pong|hello|goodbye"))) { //skip control messages replaceRow(data.data.name, data.data); // if single item, update the row } }, }); });