/********************************************************************************************** * panel Servlet - Draw JMRI panels on browser screen * Retrieves panel xml from JMRI and builds panel client-side from that xml, including * click functions. Sends and listens for changes to panel elements using the JSON WebSocket server. * If no parm "name" passed, page will list links to available panels. * Include parm protect=yes to treat panel as read-only * Approach: Read panel's xml and create widget objects in the browser with all needed attributes. * There are 5 "widgetFamily"s: text, input, icon, drawn and switch. States are handled by storing member's * iconX, textX, cssX where X is the state. The corresponding members are "shown" whenever the state changes. * CSS classes are used throughout to attach events to correct widgets, as well as control appearance. * The JSON type is used to send changes to JSON server and to listen for changes made elsewhere. * Drawn widgets are handled by drawing directly on the javascript "canvas" layer. * Switch widgets are handled by drawing directly on an individual javascript "canvas", placed in a flexbox layout. * * See java/src/jmri/server/json/JsonNamedBeanSocketService.java#onMessage() for GET method that adds a listener. * See JMRI Web Server - Panel Servlet in help/en/html/web/PanelServlet.shtml for an example description of * the interaction between the Web Servlets, the Web Browser and the JMRI application. * * TODO: show error dialog while retrying connection * TODO: add Cancel button to return to home page on errors (not found, etc.) * TODO: handle "&" in usernames (see Indicator Demo 00.xml) * TODO: update drawn track on color and width changes (would need to create system objects to reflect these chgs) * TODO: research movement of locoicons ("promote" locoicon to system entity in JMRI?, add panel-level listeners?) * TODO: deal with mouseleave, mouseout, touchout, etc. Slide off Stop button on rb1 for example. * TODO: alignment of text sensorIcons without fixed width is very different. Recommended workaround is to use fixed width. * TODO: add support for slipturnouticon (one2beros) * TODO: handle (and test) disableWhenOccupied for layoutslip * TODO: handle block color and track widths for turntable raytracks * TODO: fix JMRI WARN about username on memoryicons * **********************************************************************************************/ var log = new Logger(); //persistent (global) variables var $gWidgets = {}; //array of all widget objects, key=CSSId var $gPanelList = {}; //store list of available panels var $gPanel = {}; //store overall panel info var whereUsed = {}; //associative array of array of elements indexed by systemName or userName var audioIconIDs = {}; //associative array of audio icons var occupancyNames = {}; //associative array of array of elements indexed by occupancy sensor name var $oblockNames = {}; //associative array of array of elements indexed by occupancy block name (CPE panels) var $gPts = {}; //array of all points, key="pointname.pointtype" (used for layoutEditor panels) var $gBlks = {}; //array of all blocks, key="blockname" (used for layoutEditor panels) var $gCtx; //persistent context of canvas layer var $gDashArray = [12, 12]; //on,off of dashed lines var $rows = 1; //persistent storage of shared switchboard property number of rows, if 0 use autoRows var $total = 1; //persistent storage of shared switchboard property total number of items displayed var $autoRows = 0; var $activeColor = 'red'; var $inactiveColor = 'gray'; var $unknownColor = 'gray'; var $showUserName = 'no'; var DOWNEVENT = 'touchstart mousedown'; // check both touch and mouse events var UPEVENT = 'touchend mouseup'; var BLUR = 'blur'; var KEYUP = 'keyup'; var CHANGE = 'change'; var SIZE = 3; // default factor for circles var UNKNOWN = '0'; // constants to match JSON Server state names var ACTIVE = '2'; var CLOSED = '2'; var INACTIVE = '4'; var THROWN = '4'; var INCONSISTENT = '8'; var ALLOCATED = 0x10; // constants to match JSON Server oblock status names var RUNNING = 0x20; // Oblock that running train has reached var OUT_OF_SERVICE = 0x40; // Oblock that should not be used var TRACK_ERROR = 0x80; // Oblock has Error var CLOSEDCLOSED = '5'; // constants for slipturnouticon var CLOSEDTHROWN = '7'; var THROWNCLOSED = '9'; var THROWNTHROWN = '11'; var PT_CEN = ".POS_POINT"; // named constants for point types var PT_A = ".TURNOUT_A"; var PT_B = ".TURNOUT_B"; var PT_C = ".TURNOUT_C"; var PT_D = ".TURNOUT_D"; var LEVEL_XING_A = ".LEVEL_XING_A"; var LEVEL_XING_B = ".LEVEL_XING_B"; var LEVEL_XING_C = ".LEVEL_XING_C"; var LEVEL_XING_D = ".LEVEL_XING_D"; var SLIP_A = ".SLIP_A"; var SLIP_B = ".SLIP_B"; var SLIP_C = ".SLIP_C"; var SLIP_D = ".SLIP_D"; var STATE_AC = 0x02; var STATE_BD = 0x04; var STATE_AD = 0x06; var STATE_BC = 0x08; var DARK = 0x00; //named constants for signalhead states var RED = 0x01; var FLASHRED = 0x02; var YELLOW = 0x04; var FLASHYELLOW = 0x08; var GREEN = 0x10; var FLASHGREEN = 0x20; var LUNAR = 0x40; var FLASHLUNAR = 0x80; var HELD = 0x0100; //additional to deal with "Held" pseudo-state var RH_TURNOUT = "RH_TURNOUT"; //named constants for turnout types var LH_TURNOUT = "LH_TURNOUT"; var WYE_TURNOUT = "WYE_TURNOUT"; var DOUBLE_XOVER = "DOUBLE_XOVER"; var RH_XOVER = "RH_XOVER"; var LH_XOVER = "LH_XOVER"; var SINGLE_SLIP = "SINGLE_SLIP"; var DOUBLE_SLIP = "DOUBLE_SLIP"; var jmri = null; var jmri_logging = false; /****************************************************************** * ======= Debug functions ======= */ // log object properties function $logProperties(obj) { if (jmri_logging) { var $propList = ""; for (var $propName in obj) { if (isDefined(obj[$propName])) { $propList += ($propName + "='" + obj[$propName] + "', "); } } log.log("$logProperties(obj): " + $propList + "."); } } function isUndefined(x) { return (typeof x === "undefined"); } function isDefined(x) { return (typeof x !== "undefined"); } /****************************************************************** * ======= Primary functions ======= */ // request the panel xml from the server, and set up callback to process the response var requestPanelXML = function(panelName) { $("#activity-alert").addClass("show").removeClass("hidden"); $.ajax({ type: "GET", url: "/panel/" + panelName + "?format=xml", // request proper url success: function(data, textStatus, jqXHR) { processPanelXML(data, textStatus, jqXHR); setTitle($gPanel["name"]); // set final title once load completes, helps with testing // set new attribute data-panel-name on the panel-area div to the panel name so that a user's css can use it. $("#panel-area").attr("data-panel-name", $gPanel["name"]); }, error: function( jqXHR, textStatus, errorThrown) { alert("Error retrieving panel xml from server. Please press OK to retry.\n\nDetails: " + textStatus + " - " + errorThrown); window.location = window.location.pathname; }, async: true, timeout: 15000, // very long timeout, since this can be a slow process for complicated panels dataType: "xml" }); }; // process the response returned for the requestPanelXML command function processPanelXML($returnedData, $success, $xhr) { $('div#messageText').text("rendering panel from xml, please wait..."); $("#activity-alert").addClass("show").removeClass("hidden"); var $xml = $($returnedData); //jQuery-ize returned data for easier access //remove whitespace $xml.xmlClean(); //get the panel-level values from the xml var $panel = $xml.find('panel'); $($panel[0].attributes).each(function() { $gPanel[this.name] = this.value; }); $("#panel-area").width($gPanel.width); $("#panel-area").height($gPanel.height); //override "Allow Layout Control" attribute when protect parm set to "yes" var protect = getParameterByName("protect"); if (protect == "yes") { $gPanel["controlling"] = "no"; } // insert the canvas layer and set up context used by LayoutEditor "drawn" objects, set some defaults if ($gPanel.paneltype == "LayoutPanel") { createPanelCanvas(); //insure canvas layer is available for drawing } // set up context used by SwitchboardEditor "beanswitch" objects, set some defaults if ($gPanel.paneltype == "Switchboard") { $("#panel-area").width("100%"); // reset to fill the (mobile) screen $("#panel-area").height("100%"); // reset to fill the (mobile) screen // background color already set for #panel-area, inherited $activeColor = $gPanel.activecolor; $inactiveColor = $gPanel.inactivecolor; $showUserName = $gPanel.showusername; $total = Number($gPanel.total); $rows = Number($gPanel.rows); if ($rows == 0) { // AutoRows set, automatically choose grid showing largest tiles using flexbox $("#panel-area").css({'display': "flex", 'flex-flow': "row wrap"}) $autoRows = 1; $rows = autoRows(window.screen.width, window.screen.height - 200); // use (mobile) screen size, leave space for header // check browser window (window.innerWidth) size vs whole screen (window.screen.width) $swWidth = Math.ceil(0.95*Math.min(window.screen.width, window.innerWidth)*Math.max(0.01, Math.min(1, 1/(Math.ceil($total/$rows))))); $swWidth = Math.max(Math.min($swWidth, 200), 70); // catch extreme width result $swHeight = Math.ceil(0.9*Math.min(window.screen.height, window.innerHeight)/Math.max(0.01, $rows)); $swHeight = Math.max($swHeight, 90); // minimum height to display 2 labels // 0.9 to leave room for the Switchboard name label at top } else { $swWidth = Math.ceil(0.95*($gPanel.panelwidth)*Math.max(0.01, Math.min(1, 1/(Math.ceil($total/$rows))))); // calculate from jmri rows number, 95pc to fit on screen // Math.min(1,... to prevent >100% width calc (when hide unconnected selected) // Math.max(0.001,... to prevent 0 width in case 0 items are connected // 1/Math.ceil($total/$rows) to account for unused tiles: // include RxC unused cells in calc: for 22 switches we need at least 24 tiles (4x6, 3x8, 2x12 etc) $swHeight = Math.ceil(0.9*$gPanel.panelheight/Math.max(0.01, $rows)); // Math.max(0.001,... to prevent 0 division in case 0 items are connected } var onOffSpans = ""; if (($gPanel.type == "L") && ($gPanel.controlling == "yes")) { // handlers to switch on/off, I18N onOffSpans = " All Off All On"; // handlers added later } // add short banner at top of Swb $("#panel-area").append("
 Switchboard "" + $gPanel.name + "" (conn: " + $gPanel.connection + ", type: " + $gPanel.type + ")" + onOffSpans + "
"); // TODO I18N } // process all elements in the panel xml, drawing them on screen, and building persistent array of widgets $panel.contents().each( function() { var $widget = new Array(); $widget['widgetType'] = this.nodeName; $widget['scale'] = "1"; //default to no scale $widget['degrees'] = 0; //default to no rotation $widget['rotation'] = 0; // default to no rotation // convert attributes to an object array $(this.attributes).each(function() { $widget[this.name] = this.value; }); //default various css attributes to not-set, then set in later code as needed var $hoverText = ""; //set value as JMRI would if "Allow Layout Control" turned off if ($gPanel.controlling != "yes") { $widget['forcecontroloff'] = "true"; } // add and normalize the various type-unique values, from the various spots they are stored // icon names based on states returned from JSON server, $widget['state'] = UNKNOWN; //initial state is unknown $widget.jsonType = ""; //default to no JSON type (avoid undefined) if (isUndefined($widget["systemName"]) && isDefined($widget["id"])) { $widget.systemName = $widget["id"]; //set systemName from id if missing } $widget["id"] = "widget-" + $gUnique(); //set id to a unique value (since same element can be in multiple widgets) $widget['widgetFamily'] = $getWidgetFamily($widget, this); $widget['extraAttributes'] = ""; //some type-specific attrs var $jc = ""; if (isDefined($widget["class"])) { var $ta = $widget["class"].split('.'); //get last part of java class name for a css class $jc = $ta[$ta.length - 1]; } if ($widget.widgetFamily == "switch") { $widget['classes'] = $widget.widgetType + " " + $jc; // rest of classes are not used on a switch } else { $widget['classes'] = $widget.widgetType + " " + $widget.widgetFamily + " rotatable " + $jc; } if ($widget.momentary == "true") { $widget.classes += " momentary "; } if ($widget.hidden == "yes") { $widget.classes += " hidden "; } if (isDefined($widget.showtooltip) && $widget.showtooltip == "true") { //set tooltip for custom tooltip var ht = $(this).find('tooltip').text(); if (ht != "") $widget['hoverText'] = ht; } // set additional values in this widget switch ($widget.widgetFamily) { case "icon" : $widget['styles'] = $getTextCSSFromObj($widget); switch ($widget.widgetType) { case "positionablelabel" : $widget['icon' + UNKNOWN] = $(this).find('icon').attr('url'); $widget['rotation'] = $(this).find('icon').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('icon').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('icon').attr('scale'); break; case "audioicon" : $widget.jsonType = 'audio'; // JSON object type $widget['identity'] = $(this).find('Identity').text(); audioIconIDs['audioicon:'+$widget['identity']] = $widget; // Ensure the key is a string, not a number $widget['icon' + UNKNOWN] = $(this).find('icon').attr('url'); $widget['sound'] = $(this).attr('sound'); $widget['onClickOperation'] = $(this).attr('onClickOperation'); $widget['audio_widget'] = new Audio($widget['sound']); $widget['playSoundWhenJmriPlays'] = $(this).attr('playSoundWhenJmriPlays') == "yes"; $widget['stopSoundWhenJmriStops'] = $(this).attr('stopSoundWhenJmriStops') == "yes"; $widget['rotation'] = $(this).find('icon').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('icon').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('icon').attr('scale'); $widget.classes += " " + $widget.jsonType + " clickable"; //make it clickable if (!$('#' + $widget.id).hasClass('clickable')) { $('#' + $widget.id).addClass("clickable"); $('#' + $widget.id).bind(UPEVENT, $handleClick); } jmri.getAudio($widget.systemName); jmri.getAudioIcon($widget['identity']); break; case "logixngicon" : $widget['identity'] = $(this).find('Identity').text(); $widget['icon' + UNKNOWN] = $(this).find('icon').attr('url'); $widget['rotation'] = $(this).find('icon').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('icon').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('icon').attr('scale'); $widget.classes += " " + $widget.jsonType + " clickable"; //make it clickable if (!$('#' + $widget.id).hasClass('clickable')) { $('#' + $widget.id).addClass("clickable"); $('#' + $widget.id).bind(UPEVENT, $handleClick); } break; case "linkinglabel" : $widget['icon' + UNKNOWN] = $(this).find('icon').attr('url'); $widget['rotation'] = $(this).find('icon').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('icon').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('icon').attr('scale'); $url = $(this).find('url').text(); $widget['url'] = $url; //default to using url value as is if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } break; case "indicatortrackicon" : // TODO clean up unused icon copies, carefully // named after (o)block $widget['icon' + UNKNOWN] = $(this).find('iconmap').find('ClearTrack').attr('url'); // clear via oblock $widget['icon2'] = $(this).find('iconmap').find('OccupiedTrack').attr('url'); // occupied via sensor $widget['icon4'] = $widget['icon' + UNKNOWN]; // clear via sensor $widget['icon8'] = $widget['icon' + UNKNOWN]; // status from sensor inconsistent $widget['icon16'] = $(this).find('iconmap').find('AllocatedTrack').attr('url'); // $widget['icon32'] = $(this).find('iconmap').find('PositionTrack').attr('url'); // Running $widget['icon64'] = $(this).find('iconmap').find('DontUseTrack').attr('url'); // Not in use $widget['icon128'] = $(this).find('iconmap').find('ErrorTrack').attr('url'); // Power Error $widget['iconOccupied' + UNKNOWN] = $(this).find('iconmap').find('OccupiedTrack').attr('url'); $widget['iconOccupied2'] = $(this).find('iconmap').find('OccupiedTrack').attr('url'); $widget['iconOccupied16'] = $(this).find('iconmap').find('OccupiedTrack').attr('url'); // Allocated + Occupied $widget['iconOccupied32'] = $(this).find('iconmap').find('PositionTrack').attr('url'); $widget['iconOccupied128'] = $(this).find('iconmap').find('ErrorTrack').attr('url'); $widget['rotation'] = $(this).find('iconmap').find('ClearTrack').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('iconmap').find('ClearTrack').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('iconmap').find('ClearTrack').attr('scale'); // CPE CircuitBuilder Oblocks if ($(this).find('occupancysensor').text()) { // store occupancy sensor name $widget['occupancysensor'] = $(this).find('occupancysensor').text(); $widget['name'] = $widget.occupancysensor; $widget['occupancyblock'] = "none"; // clear oblockname //console.log("ITI SENSOR=" + $widget['occupancysensor']); //$widget.jsonType = "sensor"; // JSON object type - not necessary jmri.getSensor($widget["occupancysensor"]); // listen for occupancy changes } else if ($(this).find('oblocksysname').text() && ($(this).find('oblocksysname').text() != "none")) { // extract the occupancyblock name $widget['oblocksysname'] = $(this).find('oblocksysname').text(); $widget['name'] = $(this).find('occupancyblock').text(); // display name of oblock in hovertext, like CPE $widget['occupancysensor'] = "none"; // clear occ.sensorname //console.log("ITI OBLOCK =" + $widget['oblocksysname']); jmri.getOblock($widget["oblocksysname"]); // listen for oblock changes via json, fired by OBlock#setState() // store ControlPanelEditor oblocks where-used $store_occupancyblock($widget.id, $widget.oblocksysname); } $widget['occupancystate'] = UNKNOWN; break; case "indicatorturnouticon" : $widget['name'] = $(this).find('turnout').text(); // it could be empty on incomplete indicators $widget.jsonType = 'turnout'; // JSON object type $widget['icon' + UNKNOWN] = $(this).find('iconmaps').find('ClearTrack').find('BeanStateUnknown').attr('url'); $widget['icon2'] = $(this).find('iconmaps').find('ClearTrack').find('TurnoutStateClosed').attr('url'); // Clear + Closed $widget['icon4'] = $(this).find('iconmaps').find('ClearTrack').find('TurnoutStateThrown').attr('url'); // Clear + Thrown $widget['icon8'] = $(this).find('iconmaps').find('ClearTrack').find('BeanStateInconsistent').attr('url'); $widget['icon16'] = $(this).find('iconmaps').find('AllocatedTrack').find('BeanStateUnknown').attr('url'); // Allocated + ? $widget['icon18'] = $(this).find('iconmaps').find('AllocatedTrack').find('TurnoutStateClosed').attr('url'); // Allocated + Closed $widget['icon20'] = $(this).find('iconmaps').find('AllocatedTrack').find('TurnoutStateThrown').attr('url'); // Allocated + Thrown $widget['icon22'] = $(this).find('iconmaps').find('AllocatedTrack').find('BeanStateInconsistent').attr('url');// Allocated + X $widget['icon32'] = $(this).find('iconmaps').find('AllocatedTrack').find('BeanStateUnknown').attr('url'); // Running + ? (should be Occupied, see below) $widget['icon34'] = $(this).find('iconmaps').find('PositionTrack').find('TurnoutStateClosed').attr('url'); // Running + Closed $widget['icon36'] = $(this).find('iconmaps').find('PositionTrack').find('TurnoutStateThrown').attr('url'); // Running + Thrown $widget['icon38'] = $(this).find('iconmaps').find('PositionTrack').find('BeanStateInconsistent').attr('url'); // Running + X $widget['icon64'] = $(this).find('iconmaps').find('DontUseTrack').find('BeanStateUnknown').attr('url'); // Not in use + ? $widget['icon66'] = $(this).find('iconmaps').find('DontUseTrack').find('TurnoutStateClosed').attr('url'); // Not in use + Closed $widget['icon68'] = $(this).find('iconmaps').find('DontUseTrack').find('TurnoutStateThrown').attr('url'); // Not in use + Thrown $widget['icon70'] = $(this).find('iconmaps').find('DontUseTrack').find('BeanStateInconsistent').attr('url'); // Not in use + X $widget['icon128'] = $(this).find('iconmaps').find('ErrorTrack').find('BeanStateUnknown').attr('url'); // Power Error + ? $widget['icon130'] = $(this).find('iconmaps').find('ErrorTrack').find('TurnoutStateClosed').attr('url'); // Power Error + Closed $widget['icon132'] = $(this).find('iconmaps').find('ErrorTrack').find('TurnoutStateThrown').attr('url'); // Power Error + Thrown $widget['icon134'] = $(this).find('iconmaps').find('ErrorTrack').find('BeanStateInconsistent').attr('url'); // Power Error + X $widget['iconOccupied' + UNKNOWN] = $(this).find('iconmaps').find('OccupiedTrack').find('BeanStateUnknown').attr('url');// 4 icons for $widget['iconOccupied2'] = $(this).find('iconmaps').find('OccupiedTrack').find('TurnoutStateClosed').attr('url'); // occ.detect $widget['iconOccupied4'] = $(this).find('iconmaps').find('OccupiedTrack').find('TurnoutStateThrown').attr('url'); // by sensor $widget['iconOccupied8'] = $(this).find('iconmaps').find('OccupiedTrack').find('BeanStateInconsistent').attr('url'); // $widget['iconOccupied16'] = $(this).find('iconmaps').find('OccupiedTrack').find('BeanStateUnknown').attr('url'); // 4 icons for $widget['iconOccupied18'] = $(this).find('iconmaps').find('OccupiedTrack').find('TurnoutStateClosed').attr('url'); // occ.detect $widget['iconOccupied20'] = $(this).find('iconmaps').find('OccupiedTrack').find('TurnoutStateThrown').attr('url'); // by oblock $widget['iconOccupied22'] = $(this).find('iconmaps').find('OccupiedTrack').find('BeanStateInconsistent').attr('url');// $widget['iconOccupied32'] = $(this).find('iconmaps').find('AllocatedTrack').find('BeanStateUnknown').attr('url'); // Running + ? $widget['iconOccupied34'] = $(this).find('iconmaps').find('PositionTrack').find('TurnoutStateClosed').attr('url'); // Running + Closed $widget['iconOccupied36'] = $(this).find('iconmaps').find('PositionTrack').find('TurnoutStateThrown').attr('url'); // Running + Thrown $widget['iconOccupied38'] = $(this).find('iconmaps').find('PositionTrack').find('BeanStateInconsistent').attr('url'); // Running + X $widget['iconOccupied128'] = $(this).find('iconmaps').find('ErrorTrack').find('BeanStateUnknown').attr('url'); $widget['iconOccupied130'] = $(this).find('iconmaps').find('ErrorTrack').find('TurnoutStateClosed').attr('url'); $widget['iconOccupied132'] = $(this).find('iconmaps').find('ErrorTrack').find('TurnoutStateThrown').attr('url'); $widget['iconOccupied134'] = $(this).find('iconmaps').find('ErrorTrack').find('BeanStateInconsistent').attr('url'); // no icons for Occupied + DontUseTrack $widget['rotation'] = $(this).find('iconmaps').find('ClearTrack').find('BeanStateUnknown').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('iconmaps').find('ClearTrack').find('BeanStateUnknown').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('iconmaps').find('ClearTrack').find('BeanStateUnknown').attr('scale'); if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } // CPE CircuitBuilder Oblocks if ($(this).find('occupancysensor').text()) { // instead, store occupancy sensor name $widget['occupancyblock'] = "none"; // clear oblockname $widget['occupancysensor'] = $(this).find('occupancysensor').text(); //console.log("ITOI SENSOR =" + $widget['occupancysensor']); jmri.getSensor($widget["occupancysensor"]); // listen for occupancy changes $store_occupancysensor($widget.id, $widget.occupancysensor); // only do that now we know no oblock is set } else if ($(this).find('oblocksysname').text() && ($(this).find('oblocksysname').text() != "none")) { // extract the occupancy block name $widget['oblocksysname'] = $(this).find('oblocksysname').text(); $widget['occupancysensor'] = "none"; // clear oblockname //console.log("ITOI OBLOCK =" + $widget['oblocksysname']); jmri.getOblock($widget["oblocksysname"]); // listen for oblock changes, fired by Block#setState(), under development $store_occupancyblock($widget.id, $widget.oblocksysname); } $widget['occupancystate'] = UNKNOWN; jmri.getTurnout($widget["systemName"]); break; case "turnouticon" : $widget['name'] = $widget.turnout; //normalize name $widget.jsonType = "turnout"; // JSON object type $widget['icon' + UNKNOWN] = $(this).find('icons').find('unknown').attr('url'); $widget['icon2'] = $(this).find('icons').find('closed').attr('url'); $widget['icon4'] = $(this).find('icons').find('thrown').attr('url'); $widget['icon8'] = $(this).find('icons').find('inconsistent').attr('url'); $widget['rotation'] = $(this).find('icons').find('unknown').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('icons').find('unknown').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('icons').find('unknown').attr('scale'); if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } jmri.getTurnout($widget["systemName"]); break; case "outputindicator" : $widget['name'] = $widget.turnout; //normalize name $widget.jsonType = "turnout"; // JSON object type $widget['icon' + UNKNOWN] = $(this).find('icons').find('unknown').attr('url'); $widget['icon2'] = $(this).find('icons').find('closed').attr('url'); $widget['icon4'] = $(this).find('icons').find('thrown').attr('url'); $widget['icon8'] = $(this).find('icons').find('inconsistent').attr('url'); $widget['rotation'] = $(this).find('icons').find('unknown').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('icons').find('unknown').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('icons').find('unknown').attr('scale'); if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } jmri.getTurnout($widget["systemName"]); break; case "sensoricon" : $widget['name'] = $widget.sensor; //normalize name $widget.jsonType = "sensor"; // JSON object type $widget['icon' + UNKNOWN] = $(this).find('unknown').attr('url'); $widget['icon2'] = $(this).find('active').attr('url'); $widget['icon4'] = $(this).find('inactive').attr('url'); $widget['icon8'] = $(this).find('inconsistent').attr('url'); $widget['rotation'] = $(this).find('unknown').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('unknown').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('unknown').attr('scale'); if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } if (isUndefined($widget["systemName"])) $widget["systemName"] = $widget.name; jmri.getSensor($widget["systemName"]); break; case "LightIcon" : $widget['name'] = $widget.light; //normalize name $widget.jsonType = "light"; // JSON object type $widget['icon' + UNKNOWN] = $(this).find('icons').find('unknown').attr('url'); $widget['icon2'] = $(this).find('icons').find('on').attr('url'); $widget['icon4'] = $(this).find('icons').find('off').attr('url'); $widget['icon8'] = $(this).find('icons').find('inconsistent').attr('url'); $widget['rotation'] = $(this).find('icons').find('unknown').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('icons').find('unknown').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('unknown').attr('scale'); if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } if (isUndefined($widget["systemName"])) $widget["systemName"] = $widget.name; jmri.getLight($widget["systemName"]); break; case "signalheadicon" : $widget['name'] = $widget.signalhead; //normalize name $widget.jsonType = "signalHead"; // JSON object type $widget['icon' + HELD] = $(this).find('icons').find('held').attr('url'); $widget['icon' + DARK] = $(this).find('icons').find('dark').attr('url'); $widget['icon' + RED] = $(this).find('icons').find('red').attr('url'); if (isUndefined($widget['icon' + RED])) { //look for held if no red $widget['icon' + RED] = $(this).find('icons').find('held').attr('url'); } $widget['icon' + YELLOW] = $(this).find('icons').find('yellow').attr('url'); $widget['icon' + GREEN] = $(this).find('icons').find('green').attr('url'); $widget['icon' + FLASHRED] = $(this).find('icons').find('flashred').attr('url'); $widget['icon' + FLASHYELLOW] = $(this).find('icons').find('flashyellow').attr('url'); $widget['icon' + FLASHGREEN] = $(this).find('icons').find('flashgreen').attr('url'); $widget['icon' + LUNAR] = $(this).find('icons').find('lunar').attr('url'); $widget['icon' + FLASHLUNAR] = $(this).find('icons').find('lunar').attr('url'); $widget['rotation'] = $(this).find('icons').find('dark').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('icons').find('dark').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('icons').find('dark').attr('scale'); if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } jmri.getSignalHead($widget["systemName"]); break; case "signalmasticon" : $widget['name'] = $widget.signalmast; //normalize name $widget.jsonType = "signalMast"; // JSON object type var icons = $(this).find('icons').children(); //get array of icons icons.each(function(i, item) { //loop thru icons array and set all iconXX urls for widget $widget['icon' + $(item).attr('aspect')] = $(item).attr('url'); }); $widget['degrees'] = $(this).attr('degrees') * 1; $widget['scale'] = $(this).attr('scale'); if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } if (isDefined($widget["iconUnlit"])) { $widget['state'] = "Unlit"; //set the initial aspect to Unlit if defined } else { $widget['state'] = "Unknown"; //else set to Unknown } jmri.getSignalMast($widget["systemName"]); break; case "multisensoricon" : //create multiple widgets, 1st with all images, stack others with non-active states set to a clear image // set up siblings array so each widget can also set state of the others $widget.jsonType = "sensor"; // JSON object type $widget['icon' + UNKNOWN] = $(this).find('unknown').attr('url'); $widget['icon4'] = $(this).find('inactive').attr('url'); $widget['icon8'] = $(this).find('inconsistent').attr('url'); $widget['rotation'] = $(this).find('unknown').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('unknown').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('unknown').attr('scale'); if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } $widget['siblings'] = new Array(); //array of related multisensors $widget['hoverText'] = ""; //for override of hovertext var actives = $(this).find('active'); //get array of actives used by this multisensor var $id = $widget.id; actives.each(function(i, item) { //loop thru array once to set up siblings array, to be copied to all siblings $widget.siblings.push($id); $widget.hoverText += $(item).attr('sensor') + " "; //add sibling names to hovertext $id = "widget-" + $gUnique(); //set new id to a unique value for each sibling }); actives.each(function(i, item) { //loop thru array again to create each widget $widget['id'] = $widget.siblings[i]; // use id already set in sibling array $widget.name = $(item).attr('sensor'); $widget['icon2'] = $(item).attr('url'); if (i < actives.size() - 1) { //only save widget and make a new one if more than one active found $preloadWidgetImages($widget); //start loading all images $widget['safeName'] = $safeName($widget.name); //add a html-safe version of name $widget["systemName"] = $widget.name; $gWidgets[$widget.id] = $widget; //store widget in persistent array $drawIcon($widget); //actually place and position the widget on the panel jmri.getSensor($widget["systemName"]); if (!($widget.systemName in whereUsed)) { //set where-used for this new sensor whereUsed[$widget.systemName] = new Array(); } whereUsed[$widget.systemName][whereUsed[$widget.systemName].length] = $widget.id; $widget = jQuery.extend(true, {}, $widget); //get a new copy of widget $widget['icon' + UNKNOWN] = "/web/images/transparent_1x1.png"; $widget['icon4'] = "/web/images/transparent_1x1.png"; //set non-actives to transparent image $widget['icon8'] = "/web/images/transparent_1x1.png"; $widget['state'] = ACTIVE; //to avoid sizing based on the transparent image } }); $widget["systemName"] = $widget.name; jmri.getSensor($widget["systemName"]); break; case "memoryicon" : $widget['name'] = $widget.memory; //normalize name $widget.jsonType = "memory"; // JSON object type $widget['state'] = null; //set initial state to null var memorystates = $(this).find('memorystate'); memorystates.each(function(i, item) { //get any memorystates defined //store icon url in "iconXX" where XX is the state to match $widget['icon' + item.attributes['value'].value] = item.attributes['icon'].value; $widget.state = item.attributes['value'].value; //default state to last defined value to draw icon }); if (isUndefined($widget["systemName"])) $widget["systemName"] = $widget.name; jmri.getMemory($widget["systemName"]); break; case "slipturnouticon" : // added 2022, adapted from indicatorturnouticon // no direct link to a JSON/named bean (systemName = id) // also used for three way turnouts // see java/src/jmri/jmrit/display/SlipTurnoutIcon.java // and java/src/jmri/jmrit/display/configurexml/SlipTurnoutIconXml.java $widget['turnoutEast'] = $(this).find('turnoutEast').text(); $widget['turnoutWest'] = $(this).find('turnoutWest').text(); $widget['name'] = $widget['turnoutEast'] + " " +$widget['turnoutWest']; $widget.jsonType = "turnout"; // JSON object type, used to send commands in $handleClick(e) $widget['slipicontype'] = $(this).find('turnoutType').text(); $widget['slipStateEast'] = UNKNOWN; $widget['slipStateWest'] = UNKNOWN; $widget['slipState'] = UNKNOWN; // combined state // set icons $widget['icon' + UNKNOWN] = $(this).find('unknown').attr('url'); $widget['icon' + INCONSISTENT] = $(this).find('inconsistent').attr('url'); $widget['icon5'] = $(this).find('upperWestToLowerEast').attr('url'); $widget['icon7'] = $widget['icon' + INCONSISTENT]; // state 7 loaded later where supported $widget['icon9'] = $(this).find('lowerWestToLowerEast').attr('url'); $widget['icon11'] = $(this).find('lowerWestToUpperEast').attr('url'); switch ($widget.turnoutType) { case "singleSlip" : // $widget['singleSlipRoute'] = "lowerWestToLowerEast" or "upperWestToUpperEast" if ($widget.singleSlipRoute == "upperWestToUpperEast") { $widget['icon7'] = $(this).find('upperWestToUpperEast').attr('url'); } break; case "threeWay" : // $widget['firstTurnoutExit'] = "upper" or "lower" if ($widget.firstTurnoutExit == "lower") { // swap icons7 and 9 $widget['icon7'] = $widget.icon9; $widget['icon9'] = $(this).find('lowerWestToUpperEast').attr('url'); } break; case "scissor" : $widget['turnoutLowerEast'] = $(this).find('turnoutLowerEast').text(); $widget['turnoutLowerWest'] = $(this).find('turnoutLowerWest').text(); if (isDefined($widget.turnoutLowerEast)) { $widget['singleCrossOver'] = "false"; // connect 2 extra turnouts now to prevent extra switch case below, no need to listen // jmri.getTurnout($widget['turnoutLowerEast']); // jmri.getTurnout($widget['turnoutLowerWest']); } else { $widget['singleCrossOver'] = "true"; } $widget['icon7'] = $widget.icon5; // UWLE $widget['icon5'] = $widget.icon9; // LWLE $widget['icon9'] = $widget.icon11; // LWUE $widget['icon11'] = $widget['icon' + INCONSISTENT]; break; case "doubleSlip" : // default $widget['icon7'] = $(this).find('upperWestToUpperEast').attr('url'); break; } $widget['rotation'] = $(this).find('lowerWestToLowerEast').find('rotation').text() * 1; $widget['degrees'] = ($(this).find('lowerWestToLowerEast').attr('degrees') * 1) - ($widget.rotation * 90); $widget['scale'] = $(this).find('lowerWestToLowerEast').attr('scale'); if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } jmri.getTurnout($widget['turnoutEast']); jmri.getTurnout($widget['turnoutWest']); // add turnout to whereUsed array (as $widget.id + 'e') if (!($widget.turnoutEast in whereUsed)) { //set where-used for this new turnout whereUsed[$widget.turnoutEast] = new Array(); } whereUsed[$widget.turnoutEast][whereUsed[$widget.turnoutEast].length] = $widget.id + "e"; // add turnoutB to whereUsed array (as $widget + 'w') if (!($widget.turnoutWest in whereUsed)) { //set where-used for this new turnout whereUsed[$widget.turnoutWest] = new Array(); } whereUsed[$widget.turnoutWest][whereUsed[$widget.turnoutWest].length] = $widget.id + "w"; // TODO add the extra 2 turnouts to whereUsed that optionally are part of a scissor? break; } $preloadWidgetImages($widget); //start loading all images $widget['safeName'] = $safeName($widget.name); //add a html-safe version of name $gWidgets[$widget.id] = $widget; //store widget in persistent array $drawIcon($widget); //actually place and position the widget on the panel break; case "text" : case "input" : $widget['styles'] = $getTextCSSFromObj($widget); switch ($widget.widgetType) { case "audioicon" : $widget.jsonType = 'audio'; // JSON object type $widget['identity'] = $(this).find('Identity').text(); audioIconIDs['audioicon:'+$widget['identity']] = $widget; // Ensure the key is a string, not a number $widget['sound'] = $(this).attr('sound'); $widget['onClickOperation'] = $(this).attr('onClickOperation'); $widget['audio_widget'] = new Audio($widget['sound']); $widget['playSoundWhenJmriPlays'] = $(this).attr('playSoundWhenJmriPlays') == "yes"; $widget['stopSoundWhenJmriStops'] = $(this).attr('stopSoundWhenJmriStops') == "yes"; $widget.styles['user-select'] = "none"; $widget.classes += " " + $widget.jsonType + " clickable "; if (!$('#' + $widget.id).hasClass('clickable')) { $('#' + $widget.id).addClass("clickable"); $('#' + $widget.id).bind(UPEVENT, $handleClick); } jmri.getAudio($widget.systemName); jmri.getAudioIcon($widget['identity']); break; case "logixngicon" : $widget.jsonType = "logixngicon"; // JSON object type $widget['identity'] = $(this).find('Identity').text(); $widget.styles['user-select'] = "none"; $widget.classes += " " + $widget.jsonType + " clickable "; break; case "sensoricon" : $widget['name'] = $widget.sensor; //normalize name $widget.jsonType = "sensor"; // JSON object type //set each state's text $widget['text' + UNKNOWN] = $(this).find('unknownText').attr('text'); $widget['text2'] = $(this).find('activeText').attr('text'); $widget['text4'] = $(this).find('inactiveText').attr('text'); $widget['text8'] = $(this).find('inconsistentText').attr('text'); //set each state's css attribute array (text color, etc.) $widget['css' + UNKNOWN] = $getTextCSSFromObj($getObjFromXML($(this).find('unknownText')[0])); $widget['css2'] = $getTextCSSFromObj($getObjFromXML($(this).find('activeText')[0])); $widget['css4'] = $getTextCSSFromObj($getObjFromXML($(this).find('inactiveText')[0])); $widget['css8'] = $getTextCSSFromObj($getObjFromXML($(this).find('inconsistentText')[0])); if (isDefined($widget.name) && $widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } if (isUndefined($widget["systemName"])) $widget["systemName"] = $widget.name; jmri.getSensor($widget["systemName"]); break; case "locoicon" : case "trainicon" : //also set the background icon for this one (additional css in .html file) $widget['icon' + UNKNOWN] = $(this).find('icon').attr('url'); $widget.styles['background-image'] = "url('" + $widget['icon' + UNKNOWN] + "')"; $widget['scale'] = $(this).find('icon').attr('scale'); if ($widget.scale != 1) { $widget.styles['background-size'] = $widget.scale * 100 + "%"; $widget.styles['line-height'] = $widget.scale * 20 + "px"; //center vertically } break; case "fastclock" : jmri.getMemory("IMRATEFACTOR"); //enable updates for fast clock rate $widget['name'] = 'IMCURRENTTIME'; // already defined in JMRI $widget.jsonType = 'memory'; $widget.styles['width'] = "166px"; //hard-coded to match original size of clock image $widget.styles['height'] = "166px"; $widget['scale'] = $(this).attr('scale'); if (isUndefined($widget.level)) { $widget['level'] = 10; //if not included in xml } $widget['text'] = "00:00 AM"; $widget['state'] = "00:00 AM"; if (isUndefined($widget["systemName"])) $widget["systemName"] = $widget.name; jmri.getMemory($widget["systemName"]); break; case "reportericon" : $widget['name'] = $widget.reporter; //normalize name $widget.jsonType = "reporter"; // JSON object type $widget['text'] = $widget.reporter; //use name for initial text if (isUndefined($widget["systemName"])) $widget["systemName"] = $widget.name; jmri.getReporter($widget["systemName"]); break; case "BlockContentsIcon" : $widget['name'] = $widget.systemName; //normalize name (id got stepped on) $widget.jsonType = "block"; // JSON object type $widget['text'] = $widget.name; //use name for initial text $widget['state'] = $widget.name; //use name for initial state as well jmri.getBlock($widget["systemName"]); break; case "blockContentsInputIcon" : $widget['name'] = $widget.block; //normalize name $widget.jsonType = "block"; // JSON object type $widget['text'] = $widget.block; //use name for initial text $widget['state'] = $widget.block; //use name for initial state as well if (isUndefined($widget.styles.width)) { //set missing width if (isDefined($widget.colWidth)) { $widget.styles['width'] = $widget.colWidth + "em"; } else { $widget.styles['width'] = "5em"; } } if (isUndefined($widget["systemName"])) $widget["systemName"] = $widget.name; jmri.getBlock($widget["systemName"]); break; case "memoryicon" : case "memoryInputIcon" : case "memoryComboIcon" : if ($widget.class.indexOf("MemorySpinnerIcon") >= 0) { //fix for JMRI's bad element naming for this one $widget.widgetType = "memorySpinnerIcon"; $widget.widgetFamily = "input"; $widget.classes = $widget.classes.replace("memoryicon text", "memorySpinnerIcon input"); $widget['extraAttributes'] = "type='number' min='0' max='100'"; } $widget['name'] = $widget.memory; //normalize name $widget.jsonType = "memory"; // JSON object type $widget['text'] = $widget.memory; //use name for initial text $widget['state'] = $widget.memory; //use name for initial state as well if (isUndefined($widget.styles.width)) { //set missing width if (isDefined($widget.colWidth)) { $widget.styles['width'] = $widget.colWidth + "em"; } else { $widget.styles['width'] = "5em"; } } var items = $(this).find('itemList').children('item'); $widget['items'] = []; items.each(function(i, item) { //get any itemlist defined //store item list in items array $widget.items[item.attributes['index'].value] = item.textContent; }); if (isUndefined($widget["systemName"])) $widget["systemName"] = $widget.name; jmri.getMemory($widget["systemName"]); break; case "linkinglabel" : $url = $(this).find('url').text(); $widget['url'] = $url; //just store url value in widget, for use in click handler if ($widget.forcecontroloff != "true") { $widget.classes += " " + $widget.jsonType + " clickable "; } break; } $widget['safeName'] = $safeName($widget.name); switch ($widget['orientation']) { // use orientation instead of degrees if populated case "vertical_up" : $widget.degrees = 270; case "vertical_down" : $widget.degrees = 90; } $gWidgets[$widget.id] = $widget; //store widget in persistent array var $hoverText = ""; //add html for hoverText (custom tooltip) if populated if (isDefined($widget.hoverText)) { $hoverText = " title='" + $widget.hoverText + "' alt='" + $widget.hoverText + "' "; } if ($widget.widgetFamily=="input") { if ($widget.widgetType=="memoryComboIcon") { var s = ""; $("#panel-area").append(s); } else { $("#panel-area").append(""); } } else { $("#panel-area").append("
" + $widget.text + "
"); } $("#panel-area>#" + $widget.id).css($widget.styles); // apply style array to widget $setWidgetPosition($("#panel-area>#" + $widget.id)); break; case "drawn" : if (jmri_logging) { log.log("case drawn " + $widget.widgetType); $logProperties($widget); } switch ($widget.widgetType) { case "positionablepoint" : //log.log("#### Positionable Point ####"); //just store these points in persistent variable for use when drawing tracksegments and layoutturnouts //End bumpers and Connectors use wrong type, so always store as .POS_POINT $gPts[$widget.ident + ".POS_POINT"] = $widget; break; case "layoutblock" : $widget['state'] = UNKNOWN; //add a state member for this block $widget["blockcolor"] = $widget.trackcolor; //init blockcolor to trackcolor //store these blocks in a persistent var $gBlks[$widget.systemName] = $widget; //log.log("layoutblock:"); $logProperties($widget); //log.log("block[" + $widget.systemName + "].blockcolor: '" + $widget.trackcolor + "'.") jmri.getLayoutBlock($widget.systemName); break; case "layoutturnout" : $widget['id'] = $widget.ident; $widget['name'] = $widget.turnoutname; //normalize name $widget['safeName'] = $safeName($widget.name); //add a html-safe version of name $widget.jsonType = "turnout"; // JSON object type $widget['x'] = $widget.xcen; //normalize x,y $widget['y'] = $widget.ycen; if (isDefined($widget.name) && ($widget.disabled !== "yes")) { $widget.classes += " " + $widget.jsonType + " clickable "; //make it clickable (unless no turnout assigned) } //set widget occupancy sensor from block to speed affected changes later if (isDefined($gBlks[$widget.blockname])) { $widget['occupancysensorA'] = $gBlks[$widget.blockname].occupancysensor; $widget['occupancystateA'] = $gBlks[$widget.blockname].state; } if (isDefined($gBlks[$widget.blockbname])) { $widget['occupancysensorB'] = $gBlks[$widget.blockbname].occupancysensor; $widget['occupancystateB'] = $gBlks[$widget.blockbname].state; } if (isDefined($gBlks[$widget.blockcname])) { $widget['occupancysensorC'] = $gBlks[$widget.blockcname].occupancysensor; $widget['occupancystateC'] = $gBlks[$widget.blockcname].state; } if (isDefined($gBlks[$widget.blockdname])) { $widget['occupancysensorD'] = $gBlks[$widget.blockdname].occupancysensor; $widget['occupancystateD'] = $gBlks[$widget.blockdname].state; } $gWidgets[$widget.id] = $widget; //store widget in persistent array $storeTurnoutPoints($widget); //also store the turnout's 3 end points for other connections $drawTurnout($widget); //draw the turnout // add an empty, but clickable, div to the panel and position it over the turnout circle, if control allowed if ($gPanel.controlling == "yes") { $hoverText = " title='" + $widget.name + "' alt='" + $widget.name + "'"; $("#panel-area").append("
"); var $cr = $gPanel.turnoutcirclesize * SIZE; //turnout circle radius var $cd = $cr * 2; $("#panel-area>#" + $widget.id).css( { position: 'absolute', left: ($widget.x - $cr) + 'px', top: ($widget.y - $cr) + 'px', zIndex: 3, width: $cd + 'px', height: $cd + 'px' }); } if (isUndefined($widget["systemName"])) { $widget["systemName"] = $widget.name; } jmri.getTurnout($widget["systemName"]); if ($widget["occupancysensorA"]) jmri.getSensor($widget["occupancysensorA"]); //listen for occupancy changes if ($widget["occupancysensorB"]) jmri.getSensor($widget["occupancysensorB"]); //listen for occupancy changes if ($widget["occupancysensorC"]) jmri.getSensor($widget["occupancysensorC"]); //listen for occupancy changes if ($widget["occupancysensorD"]) jmri.getSensor($widget["occupancysensorD"]); //listen for occupancy changes break; case 'layoutSlip' : $widget['id'] = $widget.ident; $widget['name'] = $widget.ident; $widget['safeName'] = $safeName($widget.name); //add a html-safe version of name $widget.jsonType = "turnout"; // JSON object type //save the slip state to turnout state information $widget['turnout'] = $(this).find('turnout:first').text(); $widget['turnoutB'] = $(this).find('turnoutB:first').text(); $widget['stateA'] = UNKNOWN; $widget['stateB'] = UNKNOWN; //log.log("tA: " + $widget.turnout + ", tB: " + $widget.turnoutB); $widget['turnoutA_AC'] = Number($(this).find('states').find('A-C').find('turnout').text()); $widget['turnoutA_AD'] = Number($(this).find('states').find('A-D').find('turnout').text()); $widget['turnoutA_BC'] = Number($(this).find('states').find('B-C').find('turnout').text()); $widget['turnoutA_BD'] = Number($(this).find('states').find('B-D').find('turnout').text()); $widget['turnoutB_AC'] = Number($(this).find('states').find('A-C').find('turnoutB').text()); $widget['turnoutB_AD'] = Number($(this).find('states').find('A-D').find('turnoutB').text()); $widget['turnoutB_BC'] = Number($(this).find('states').find('B-C').find('turnoutB').text()); $widget['turnoutB_BD'] = Number($(this).find('states').find('B-D').find('turnoutB').text()); // default to this state $widget['state'] = UNKNOWN; $widget['x'] = $widget.xcen; //normalize x,y $widget['y'] = $widget.ycen; if ((isDefined($widget.turnout) || isDefined($widget.turnoutB)) && ($widget.disabled !== "yes")) { $widget.classes += " " + $widget.jsonType + " clickable "; } //set widget occupancy sensor from block to speed affected changes later if (isDefined($gBlks[$widget.blockname])) { $widget['occupancysensorA'] = $gBlks[$widget.blockname].occupancysensor; $widget['occupancystateA'] = $gBlks[$widget.blockname].state; } if (isDefined($gBlks[$widget.blockbname])) { $widget['occupancysensorB'] = $gBlks[$widget.blockbname].occupancysensor; $widget['occupancystateB'] = $gBlks[$widget.blockbname].state; } if (isDefined($gBlks[$widget.blockcname])) { $widget['occupancysensorC'] = $gBlks[$widget.blockcname].occupancysensor; $widget['occupancystateC'] = $gBlks[$widget.blockcname].state; } if (isDefined($gBlks[$widget.blockdname])) { $widget['occupancysensorD'] = $gBlks[$widget.blockdname].occupancysensor; $widget['occupancystateD'] = $gBlks[$widget.blockdname].state; } $gWidgets[$widget.id] = $widget; //store widget in persistent array $storeSlipPoints($widget); //also store the slip's 4 end points for other connections $drawSlip($widget); //draw the slip if ($gPanel.controlling == "yes") { // convenience variables for points (A, B, C, D) var a = $getPoint($widget.ident + SLIP_A); var b = $getPoint($widget.ident + SLIP_B); var c = $getPoint($widget.ident + SLIP_C); var d = $getPoint($widget.ident + SLIP_D); var $cr = $gPanel.turnoutcirclesize * SIZE; //turnout circle radius var $cd = $cr * 2; //turnout circle diameter // center var cen = [$widget.xcen, $widget.ycen]; // left center var lcen = $point_midpoint(a, b); var ldelta = $point_subtract(cen, lcen); // left fraction var lf = $cr / Math.hypot(ldelta[0], ldelta[1]); // left circle var lcc = $point_lerp(cen, lcen, lf); //add an empty, but clickable, div to the panel and position it over the left turnout circle $hoverText = " title='" + $widget.turnout + "' alt='" + $widget.turnout + "'"; $("#panel-area").append("
"); $("#panel-area>#" + $widget.id + "l").css( {position: 'absolute', left: (lcc[0] - $cr) + 'px', top: (lcc[1] - $cr) + 'px', zIndex: 3, width: $cd + 'px', height: $cd + 'px'}); // right center var rcen = $point_midpoint(c, d); var rdelta = $point_subtract(cen, rcen); // right fraction var rf = $cr / Math.hypot(rdelta[0], rdelta[1]); // right circle var rcc = $point_lerp(cen, rcen, rf); //add an empty, but clickable, div to the panel and position it over the right turnout circle $hoverText = " title='" + $widget.turnoutB + "' alt='" + $widget.turnoutB + "'"; $("#panel-area").append("
"); $("#panel-area>#" + $widget.id + "r").css( {position: 'absolute', left: (rcc[0] - $cr) + 'px', top: (rcc[1] - $cr) + 'px', zIndex: 3, width: $cd + 'px', height: $cd + 'px'}); } // set up notifications (?) jmri.getTurnout($widget["turnout"]); jmri.getTurnout($widget["turnoutB"]); if ($widget["occupancysensorA"]) jmri.getSensor($widget["occupancysensorA"]); //listen for occupancy changes if ($widget["occupancysensorB"]) jmri.getSensor($widget["occupancysensorB"]); //listen for occupancy changes if ($widget["occupancysensorC"]) jmri.getSensor($widget["occupancysensorC"]); //listen for occupancy changes if ($widget["occupancysensorD"]) jmri.getSensor($widget["occupancysensorD"]); //listen for occupancy changes // NOTE: turnout & turnoutB may appear to be swapped here however this is intentional // (since the left turnout controls the right points and vice-versa) and we want // the slip circles to toggle the points (not the turnout) on the corresponding side. // // note: the
areas above have their titles & alts turnouts swapped (left <-> right) also // add turnout to whereUsed array (as $widget.id + 'r') if (!($widget.turnout in whereUsed)) { //set where-used for this new turnout whereUsed[$widget.turnout] = new Array(); } whereUsed[$widget.turnout][whereUsed[$widget.turnout].length] = $widget.id + "r"; // add turnoutB to whereUsed array (as $widget + 'l') if (!($widget.turnoutB in whereUsed)) { //set where-used for this new turnout whereUsed[$widget.turnoutB] = new Array(); } whereUsed[$widget.turnoutB][whereUsed[$widget.turnoutB].length] = $widget.id + "l"; break; case "tracksegment" : //log.log("#### Track Segment ####"); //set widget occupancy sensor from block to speed affected changes later if (isDefined($gBlks[$widget.blockname])) { $widget['occupancysensor'] = $gBlks[$widget.blockname].occupancysensor; $widget['occupancystate'] = $gBlks[$widget.blockname].state; } //store this widget in persistent array, with ident as key $widget['id'] = $widget.ident; $gWidgets[$widget.id] = $widget; if ($widget.bezier == "yes") { $widget['controlpoints'] = $(this).find('controlpoint'); } // find decorations var $decorations = $(this).find('decorations'); //copy arrow decoration // var $arrow = $decorations.find('arrow'); var $arrowstyle = $arrow.attr('style'); if (isDefined($arrowstyle)) { if (Number($arrowstyle) > 0) { $widget['arrow'] = new ArrowDecoration($widget, $arrow); } } //copy bridge decoration // var $bridge = $decorations.find('bridge'); var $bridgeside = $bridge.attr('side'); if (isDefined($bridgeside)) { $widget['bridge'] = new BridgeDecoration($widget, $bridge); } //copy bumper decoration // var $bumper = $decorations.find('bumper'); var $bumperend = $bumper.attr('end'); if (isDefined($bumperend)) { $widget['bumper'] = new BumperDecoration($widget, $bumper); } //copy tunnel decoration // var $tunnel = $decorations.find('tunnel'); var $tunnelside = $tunnel.attr('side'); if (isDefined($tunnelside)) { $widget['tunnel'] = new TunnelDecoration($widget, $tunnel); } if ($widget["occupancysensor"]) jmri.getSensor($widget["occupancysensor"]); //listen for occupancy changes //draw the tracksegment $drawTrackSegment($widget); break; case "levelxing" : $widget['x'] = $widget.xcen; //normalize x,y $widget['y'] = $widget.ycen; //set widget occupancy sensor from block to speed affected changes later //TODO: handle BD block if (isDefined($gBlks[$widget.blocknameac])) { $widget['occupancysensorAC'] = $gBlks[$widget.blocknameac].occupancysensor; $widget['occupancystateAC'] = $gBlks[$widget.blocknameac].state; } if (isDefined($gBlks[$widget.blocknamebd])) { $widget['occupancysensorBD'] = $gBlks[$widget.blocknamebd].occupancysensor; $widget['occupancystateBD'] = $gBlks[$widget.blocknamebd].state; } //store widget in persistent array //$widget['id'] = $widget.ident; $gWidgets[$widget.id] = $widget; //also store the xing's 4 end points for other connections $storeLevelXingPoints($widget); //draw the xing $drawLevelXing($widget); //listen for occupancy changes if ($widget["occupancysensorAC"]) jmri.getSensor($widget["occupancysensorAC"]); if ($widget["occupancysensorBD"]) jmri.getSensor($widget["occupancysensorBD"]); break; case "layoutturntable" : //log.log("#### Layout Turntable ####"); $widget['id'] = $widget.ident; $widget['name'] = $widget.ident; $widget['safeName'] = $safeName($widget.name); //add a html-safe version of name $widget.jsonType = "turnout"; // JSON object type $gWidgets[$widget.id] = $widget; //store widget in persistent array if ($widget.turnoutControlled == "yes") { $widget.classes += " " + $widget.jsonType + " clickable"; //make it clickable if (!$('#' + $widget.id).hasClass('clickable')) { $('#' + $widget.id).addClass("clickable"); $('#' + $widget.id).bind(UPEVENT, $handleClick); } } //get the center var $txcen = $widget.xcen * 1; var $tycen = $widget.ycen * 1; var $tr = $widget.radius * 1; //turntable circle radius var $td = $tr * 2; var $cr = $gPanel.turnoutcirclesize * SIZE; //turnout circle radius var $cd = $cr * 2; //loop thru raytracks, calc and store end of ray point for each $widget['raytracks'] = $(this).find('raytrack'); $widget.raytracks.each(function(i, item) { $logProperties(item); //note:the 50 offset is due to TrackSegment.java TURNTABLE_RAY_OFFSET //var rayID = $widget.ident + "." + (50 + item.attributes.index.value * 1); var rayID = $widget.ident + ".TURNTABLE_RAY_" + (item.attributes.index.value * 1); var $t = {ident:rayID}; var $angle = $toRadians(item.attributes.angle.value); $t['x'] = $txcen + (($tr + $cr) * Math.sin($angle)); $t['y'] = $tycen - (($tr + $cr) * Math.cos($angle)); $gPts[$t.ident] = $t; //store the endpoint of this ray if (isDefined(item.attributes.turnout)) { var turnout = item.attributes.turnout.value; var state = item.attributes.turnoutstate.value; //add an empty, but clickable, div to the panel and position it over the turnout circle, if control allowed if ($gPanel.controlling == "yes") { $("#panel-area").append("
"); } //set up notifications jmri.getTurnout(turnout); // add turnout to whereUsed array (as $widget + 'r') if (!(turnout in whereUsed)) { //set where-used for this new turnout whereUsed[turnout] = new Array(); } whereUsed[turnout].push(rayID); } }); //draw the turntable $drawTurntable($widget); break; case "backgroundColor": // set background color of the window $("body").css({"background-color": "rgb(" + $widget.red + "," + $widget.green + "," + $widget.blue + ")"}); break; case "layoutShape" : //log.log("#### Layout Shape ####"); //store this widget in persistent array, with ident as key $widget['id'] = $widget.ident; $gWidgets[$widget.id] = $widget; $widget['points'] = $(this).find('point'); //draw the LayoutShape $drawLayoutShape($widget); break; case "positionableRectangle" : //just like RoundRect except cornerRadius set to 0; case "positionableRoundRect" : //log.log("#### positionableRoundRect ####"); //copy and reformat some attributes from children into object $widget['width'] = $(this).find('size').attr('width'); $widget['height'] = $(this).find('size').attr('height'); $widget['cornerRadius'] = $(this).find('size').attr('cornerRadius'); if (isUndefined($widget['cornerRadius'])) { $widget['cornerRadius'] = 0; //default to no corner } lc = $(this).find('lineColor'); $widget['lineColor'] = 'rgba('+lc.attr('red')+','+lc.attr('green')+',' + lc.attr('blue')+','+lc.attr('alpha')/256+')'; fc = $(this).find('fillColor'); $widget['fillColor'] = 'rgba('+fc.attr('red')+','+fc.attr('green')+',' + fc.attr('blue')+','+fc.attr('alpha')/256+')'; //store this widget in persistent array, with ident as key $widget['id'] = $widget.ident; $gWidgets[$widget.id] = $widget; //draw the positionableRoundRect $drawPositionableRoundRect($widget); break; case "positionableCircle" : //identical except circle has size radius, case "positionableEllipse" : //ellipse has size width height //copy and reformat some attributes from children into object $widget['radius'] = ($(this).find('size').attr('radius')); if (isDefined($widget['radius'])) { $widget['height'] = $widget.radius; //use radius for height if populated $widget['width'] = $widget.radius; //use radius for width if populated } else { $widget['height'] = ($(this).find('size').attr('height')); $widget['width'] = ($(this).find('size').attr('width')); } lc = $(this).find('lineColor'); $widget['lineColor'] = 'rgba('+lc.attr('red')+','+lc.attr('green')+',' + lc.attr('blue')+','+lc.attr('alpha')/256+')'; fc = $(this).find('fillColor'); $widget['fillColor'] = 'rgba('+fc.attr('red')+','+fc.attr('green')+',' + fc.attr('blue')+','+fc.attr('alpha')/256+')'; //store this widget in persistent array, with ident as key $widget['id'] = $widget.ident; $gWidgets[$widget.id] = $widget; //draw the positionableEllipse $drawPositionableEllipse($widget); break; default: log.warn("unknown $widget.widgetType: " + $widget.widgetType + "."); break; } break; case "switch" : // Switchboard BeanSwitches // they have no x,y $widget['styles'] = {}; // clear built-in styles $widget['name'] = $widget.label; // normalize name from label $widget['text'] = $widget.label; // use label as initial button text too $widget.styles['width'] = $swWidth + "px"; $widget.styles['height'] = $swHeight + "px"; // colors, values from Editor via SwitchboardServlet $widget['swColor' + UNKNOWN] = 'LightGray'; // unknown $widget['swColor2'] = $activeColor; // active = red $widget['swColor4'] = $inactiveColor; // inactive = green $widget['swColor8'] = 'Gray'; // inconsistent if ($widget.connected == "true") { switch ($widget['type']) { case "T" : $widget.jsonType = "turnout"; // JSON object type jmri.getTurnout($widget["systemName"]); // switch follows state on layout break; case "S" : $widget.jsonType = "sensor"; // JSON object type jmri.getSensor($widget["systemName"]); break; case "L": $widget.jsonType = "light"; // JSON object type jmri.getLight($widget["systemName"]); break; // more types of NamedBeans? default : break; // skip } } var $canvas = ""; switch ($widget.shape) { // set each state's text case "symbol" : case "icon" : case "drawing" : // settings for symbol/icon $widget['text' + UNKNOWN] = $widget.text; // show state changes in color, not in label? $widget['text2'] = $(this).find('activeText').attr('text'); $widget['text4'] = $(this).find('inactiveText').attr('text'); $widget['text8'] = $(this).find('inconsistentText').attr('text'); // add a canvas to the text label, reduce canvas HxW to fit inside the div $canvas = ""; // to insert later break; case "button" : // mimick java switchboard buttons default : // add some html to show user name on line 2 when shape is button $widget['text' + UNKNOWN] = getSwitchButtonLabel($(this).find('unknownText').attr('text'), $widget.username); $widget['text2'] = getSwitchButtonLabel($(this).find('activeText').attr('text'), $widget.username); $widget['text4'] = getSwitchButtonLabel($(this).find('inactiveText').attr('text'), $widget.username); $widget['text8'] = getSwitchButtonLabel($(this).find('inconsistentText').attr('text'), $widget.username); } // common settings for all beanswitche shapes $widget.classes += " " + $widget.shape + " "; $widget['state'] = UNKNOWN; // use UNKNOWN for initial state $widget.styles['color'] = $widget.text['color']; // use jmri color // other CSS properties set in css, class .beanswitch if ($widget.connected == "true") { $widget['text'] = $widget.text0; // add UNKNOWN state to label of connected switches $widget.styles['border-color'] = "black"; //$widget['swColor' + UNKNOWN]; $widget.classes += " " + $widget.jsonType + " clickable connected"; } $gWidgets[$widget.id] = $widget; // store widget in persistent array if ($widget.shape == "button") { // "button", put only the text (system + user name) element on the page $("#panel-area").append("
" + $widget.text + "
"); } else { // add a local canvas $("#panel-area").append(""); } $("#panel-area>#" + $widget.id).css($widget.styles); // apply style array to widget // beanswitch setup ready break; default: //log any unsupported widgets, listing childnodes as info $("div#logArea").append("
Unsupported: " + $widget.widgetType + ":"); $(this.attributes).each(function() { $("div#logArea").append(" " + this.name); }); $("div#logArea").append(" | "); $(this.childNodes).each(function() { $("div#logArea").append(" " + this.nodeName); }); break; } // add widget.id to whereUsed array to support updates from layout if ($widget.systemName) { if (!($widget.systemName in whereUsed)) { whereUsed[$widget.systemName] = new Array(); } whereUsed[$widget.systemName][whereUsed[$widget.systemName].length] = $widget.id; } if ($gWidgets[$widget.id]) { // store LayoutEditor occupancy sensors where-used if ($widget.occupancysensor != "none") { $store_occupancysensor($widget.id, $widget.occupancysensor); } $store_occupancysensor($widget.id, $widget.occupancysensorA); $store_occupancysensor($widget.id, $widget.occupancysensorB); $store_occupancysensor($widget.id, $widget.occupancysensorC); $store_occupancysensor($widget.id, $widget.occupancysensorD); $store_occupancysensor($widget.id, $widget.occupancysensorAC); $store_occupancysensor($widget.id, $widget.occupancysensorBD); } } //end of function ); //end of each //only enable click events if panel is marked to allow control if ($gPanel.controlling == "yes") { //hook up mouseup state toggle function to non-momentary clickable widgets, except for multisensor and linkinglabel $('.clickable:not(.momentary):not(.multisensoricon):not(.linkinglabel)').bind(UPEVENT, $handleClick); //hook up mouseup state change function to multisensor (special handling) $('.clickable.multisensoricon').bind('click', $handleMultiClick); //hook up mouseup function to linkinglabel (special handling) $('.clickable.linkinglabel').bind(UPEVENT, $handleLinkingLabelClick); //momentary widgets always go active on mousedown, and inactive on mouseup, current state is ignored $('.clickable.momentary').bind(DOWNEVENT, function(e) { e.stopPropagation(); e.preventDefault(); //prevent double-firing (touch + click) sendElementChange($gWidgets[this.id].jsonType, $gWidgets[this.id].systemName, ACTIVE); //send active on down }).bind(UPEVENT, function(e) { e.stopPropagation(); e.preventDefault(); //prevent double-firing (touch + click) sendElementChange($gWidgets[this.id].jsonType, $gWidgets[this.id].systemName, INACTIVE); //send inactive on up }); //check for update keys and update when needed $('input.input').bind(KEYUP, $handleInputKeyUp); //update when leaving the input $('input.input').bind(BLUR, $handleInputBlur); //and update when select input is changed $('select.input').bind(CHANGE, $handleInputBlur); // Switchboard All Off/All On buttons $(".lightswitch#allOff").bind(UPEVENT, $handleClickAllOff); // all Lights Off $(".lightswitch#allOn").bind(UPEVENT, $handleClickAllOn); // all Lights On } $drawAllDrawnWidgets(); // draw all the drawn widgets once more, to address some bidirectional dependencies in the xml $drawAllSwitchIcons(); // draw icon first time $("#activity-alert").addClass("hidden").removeClass("show"); } // end of processPanelXML /****************************************************************** * ======= Click Handling functions ======= */ // perform regular click-handling, bound to click event for clickable, non-momentary widgets, except for multisensor and linkinglabel. function $handleClick(e) { if (jmri_logging) { log.log("$handleClick()"); } e.stopPropagation(); e.preventDefault(); //prevent double-firing (touch + click) // if (null == $widget) { // $logProperties(this); // } // special case for LE layoutSlips if (this.className.startsWith('layoutSlip ')) { if (this.id.startsWith("SL") && (this.id.endsWith("r") || this.id.endsWith("l"))) { var $slipID = this.id.slice(0, -1); var $widget = $gWidgets[$slipID]; if (this.id.endsWith("l")) { $widget["side"] = "left"; } else if (this.id.endsWith("r")) { $widget["side"] = "right"; } if (jmri_logging) { log.log("\nlayoutSlip-side:" + $widget.side); } // convert current slip state to current turnout states var $oldStateA, $oldStateB; [$oldStateA, $oldStateB] = [$widget.stateA, $widget.stateB]; // determine next slip state var $newState = getNextSlipState($widget); if (jmri_logging) { log.log("$handleClick:layoutSlip: change state from " + slipStateToString($widget.state) + " to " + slipStateToString($newState) + "."); } // convert new slip state to new turnout states var $newStateA, $newStateB; [$newStateA, $newStateB] = getTurnoutStatesForSlipState($widget, $newState); if ($oldStateA != $newStateA) { sendElementChange($widget.jsonType, $widget.turnout, $newStateA); } if ($oldStateB != $newStateB) { sendElementChange($widget.jsonType, $widget.turnoutB, $newStateB); } //jmri_logging = false; } else { log.warn("$handleClick(e): unknown slip widget " + this.id); $logProperties(this); } // special case for LE layoutTurntable } else if (this.className.startsWith('layoutturntable ')) { var $rayID = this.id; var $turntableID = $rayID.split(".")[0]; var $widget = $gWidgets[$turntableID]; $widget.raytracks.each(function(i, item) { $logProperties(item); //note: offset 50 is due to TrackSegment.java TURNTABLE_RAY_OFFSET var rayID = $turntableID + ".TURNTABLE_RAY_" + (item.attributes.index.value * 1); if (rayID == $rayID) { if (isDefined(item.attributes.turnout)) { var turnout = item.attributes.turnout.value; var state = item.attributes.turnoutstate.value; var $newState = (state == 'thrown') ? THROWN : CLOSED; sendElementChange($widget.jsonType, turnout, $newState); } } }); } else if (this.className.startsWith('slipturnouticon')) { // special handling of slipturnouticon, which has (at least) 2 turnouts var $widget = $gWidgets[this.id]; var $newState = $getNextState($widget); // determine next state from current state var $turnoutWestNewState = 0; var $turnoutEastNewState = 0; // we may need to send a command to multiple turnouts switch ($newState) { case 5 : $turnoutWestNewState = CLOSED; $turnoutEastNewState = CLOSED; break; case 7 : $turnoutWestNewState = THROWN; $turnoutEastNewState = CLOSED; break; case 9 : $turnoutWestNewState = CLOSED; $turnoutEastNewState = THROWN; break; case 11 : $turnoutWestNewState = THROWN; $turnoutEastNewState = THROWN; break; } sendElementChange($widget.jsonType, $widget.turnoutWest, $turnoutWestNewState); sendElementChange($widget.jsonType, $widget.turnoutEast, $turnoutEastNewState); if (isDefined($widget.turnoutLowerWest)) { sendElementChange($widget.jsonType, $widget.turnoutLowerWest, $turnoutEastNewState); // note: same as turnoutWest } if (isDefined($widget.turnoutLowerEast)) { sendElementChange($widget.jsonType, $widget.turnoutLowerEast, $turnoutWestNewState); // note: same as turnoutEast } return; } else if (this.className.startsWith('audioicon ')) { // special handling of audioicon var $widget = $gWidgets[this.id]; switch ($widget['onClickOperation']) { case "PlaySoundLocally": if ($widget['audio_widget'].paused) { // Sound is stopped $widget['audio_widget'].loop = false; // $widget['audio_widget'].loop = (playNumLoops == -1); $widget['audio_widget'].play(); } else { // Sound is playing $widget['audio_widget'].pause(); $widget['audio_widget'].currentTime = 0; } break; case "PlaySoundGlobally": if ($widget['state'] == 16) { // Sound is stopped jmri.setAudio($widget.systemName, "Play"); } else if ($widget['state'] == 17) { // Sound is playing jmri.setAudio($widget.systemName, "Stop"); } break; } } else if (this.className.startsWith('logixngicon ')) { // special handling of logixngicon var $widget = $gWidgets[this.id]; jmri.clickLogixNGIcon($widget['identity']); } else { var $widget = $gWidgets[this.id]; var $newState = $getNextState($widget); // determine next state from current state sendElementChange($widget.jsonType, $widget.systemName, $newState); //also send new state to related turnout if (isDefined($widget.turnoutB)) { sendElementChange($widget.jsonType, $widget.turnoutB, $newState); } //used for crossover, LE layoutTurnout type 5 if (isDefined($widget.secondturnoutname)) { //invert 2nd turnout if requested if ($widget.secondturnoutinverted == "true") { $newState = ($newState == CLOSED ? THROWN : CLOSED); } sendElementChange($widget.jsonType, $widget.secondturnoutname, $newState); } } } // perform multisensor click-handling, bound to click event for clickable multisensor widgets. function $handleMultiClick(e) { e.stopPropagation(); e.preventDefault(); //prevent double-firing (touch + click) var $widget = $gWidgets[this.id]; var clickX = (e.offsetX || e.pageX - $(e.target).offset().left); //get click position on the widget var clickY = (e.offsetY || e.pageY - $(e.target).offset().top ); if (jmri_logging) { log.log("handleMultiClick X,Y on WxH: " + clickX + "," + clickY + " on " + this.width + "x" + this.height); } //increment or decrement based on where the click occurred on image var missed = true; //flag if click x,y outside image bounds, indicates we didn't get good values var dec = false; if ($widget.updown == "true") { if (clickY >= 0 && clickY <= this.height) missed = false; if (clickY > this.height / 2) dec = true; } else { if (clickX >= 0 && clickX <= this.width) missed = false; if (clickX < this.width / 2) dec = true; } var displaying = 0; for (i in $widget.siblings) { //determine which is currently active if ($gWidgets[$widget.siblings[i]].state == ACTIVE) { displaying = i; //flag the current active sibling } } var next; //determine which is the next one to be set active (loop around only if click outside object) if (dec) { next = displaying - 1; if (next < 0) if (missed) next = i; else next = 0; } else { next = displaying * 1 + 1; if (next > i) if (missed) next = 0; else next = i; } for (i in $widget.siblings) { //loop through siblings and send changes as needed if (i == next) { if ($gWidgets[$widget.siblings[i]].state != ACTIVE) { sendElementChange('sensor', $gWidgets[$widget.siblings[i]].name, ACTIVE); //set next sensor to active and send command to JMRI server } } else { if ($gWidgets[$widget.siblings[i]].state != INACTIVE) { sendElementChange('sensor', $gWidgets[$widget.siblings[i]].name, INACTIVE); //set all other siblings to inactive if not already } } } } //perform click-handling of linkinglabel widgets (3 cases: complete url or frame: where name is a panel or a frame) function $handleLinkingLabelClick(e) { e.stopPropagation(); e.preventDefault(); //prevent double-firing (touch + click) var $widget = $gWidgets[this.id]; var $url = $widget.url; if ($url.toLowerCase().indexOf("frame:") == 0) { $frameName = $url.substring(6); //if "frame" found, remove it $frameUrl = $gPanelList[$frameName]; //find panel in panel list if (isUndefined($frameUrl)) { $url = "/frame/" + $frameName + ".html"; //not in list, open using frameserver } else { $url = "/panel/" + $frameUrl; //format for panel server } } window.location = $url; //navigate to the specified url } function $handleClickAllOn(e) { // click button on Switchboards //loop thru widgets, setting each connected light to CLOSED/2, when button on top of switchboard was clicked jQuery.each($gWidgets, function($id, $widget) { if ($widget.connected == "true") { sendElementChange($widget.jsonType, $widget.systemName, CLOSED); } }); }; function $handleClickAllOff(e) { // click button on Switchboards //loop thru widgets, setting each connected light to THROWN/4, when button on top of switchboard was clicked jQuery.each($gWidgets, function($id, $widget) { if ($widget.connected == "true") { sendElementChange($widget.jsonType, $widget.systemName, THROWN); } }); }; //update memory or restore the value when certain keystrokes occur function $handleInputKeyUp(e) { if (e.keyCode == 13 || e.keyCode == 9) { //on [Enter] or [Tab], send new value to server var newVal = $(this).val(); var $id = $(this).attr('id'); var $widget = $gWidgets[$id]; jmri.setObject($widget.jsonType, $widget.systemName, newVal); } else if (e.keyCode == 27) { //on [Escape], restore the previous value var oldValue = $(this).data("oldValue") $(this).val(oldValue); } }; //update memory when the focus is lost function $handleInputBlur(e) { var newVal = $(this).val(); var $id = $(this).attr('id'); var $widget = $gWidgets[$id]; jmri.setObject($widget.jsonType, $widget.systemName, newVal); }; // End of Click Handling functions /****************************************************************** * ======= (Control) Panel functions ======= */ //draw an icon-type widget (pass in widget) function $drawIcon($widget) { var $hoverText = ""; if (isDefined($widget.hoverText)) { $hoverText = " title='" + $widget.hoverText + "' alt='" + $widget.hoverText + "'"; } if ($hoverText == "" && isDefined($widget.name)) { // if name available, use it as hover text if still blank $hoverText = " title='" + $widget.name + "' alt='" + $widget.name + "'"; } // additional naming for indicator*icon widgets to reflect occupancy $indicator = ""; $state = ""; if ($widget.widgetType == "indicatortrackicon" || $widget.widgetType == "indicatorturnouticon") { // check oblock status $indicator = ((($widget.occupancystate & 0x2) == 0x2) ? "Occupied" : ""); // look only at bit 2, compare to $redrawIcon() Ostate = ($widget.occupancystate & 0xF0); // binary 11110000, discards (in)active bits in occupancy which we already used above $state = Ostate | $widget.state; // adds Turnout state back in to fetch TO state = position icon // $hoverText is updated for OUT_OF_SERVICE on redraw only } else if ($widget.widgetType == "slipturnouticon") { // check turnout states, compare to $redrawIcon() $state = $widget.slipState; // combined Turnouts state } else { $indicator = ($widget.occupancysensor && $widget.occupancystate == ACTIVE ? "Occupied" : ""); $state = $widget.state; } // add the image to the panel area, with appropriate css classes and id (skip any unsupported) if (isDefined($widget['icon' + $indicator + $state])) { $imgHtml = "" $("#panel-area").append($imgHtml); // put the html in the panel $("#panel-area>#" + $widget.id).css($widget.styles); // apply style array to widget // add overlay text if specified, one layer above, and copy attributes (except background-color) if (isDefined($widget.text)) { $("#panel-area").append("
" + $widget.text + "
"); ovlCSS = {position:'absolute', left: $widget.x + 'px', top: $widget.y + 'px', zIndex: $widget.level*1 + 1, pointerEvents: 'none'}; $.extend(ovlCSS, $widget.styles); // append the styles from the widget delete ovlCSS['background-color']; // clear the background color if (isDefined($widget.fixedHeight)) { $.extend(ovlCSS, {lineHeight: $widget.fixedHeight + 'px'}); // add lineheight for vertical centering (if set) } $("#panel-area>#" + $widget.id + "-overlay").css(ovlCSS); } } else { log.error("ERROR: image not defined for " + $widget.widgetType + " " + $widget.id + ", iconstate=" + $state + " ["+$indicator+"] (icon" + $indicator + $state + ")"); } $setWidgetPosition($("#panel-area #" + $widget.id)); } //draw the analog clock (pass in widget), called on each update of clock function $drawClock($widget) { var $fs = $widget.scale * 100; // scale percentage, used for text var $fcr = $gWidgets['IMRATEFACTOR'].state * 1; // get the fast clock rate factor from its widget var $h = ""; $h += "
" + $widget.state + "
" + $fcr + ":1
"; //add the text $h += ""; //add the clock face $h += ""; //add the hour hand $h += ""; //add the minute hand $("#panel-area>#" + $widget.id).html($h); //set the html for the widget var hours = $widget.state.split(':')[0]; //extract hours from format "H:MM AM" var mins = $widget.state.split(':')[1].split(' ')[0]; //extract minutes var hdegree = hours * 30 + (mins / 2); var hrotate = "rotate(" + hdegree + "deg)"; $("div.fastclock>img.clockhourhand").css({"transform": hrotate}); //set rotation for hour hand var mdegree = mins * 6; var mrotate = "rotate(" + mdegree + "deg)"; $("div.fastclock>img.clockminutehand").css({"transform": mrotate}); //set rotation for minute hand } // end of Control Panel functions //build and return CSS array from attributes passed in var $getTextCSSFromObj = function($widget) { var $retCSS = {}; $retCSS['color'] = ''; //only clear attributes $retCSS['background-color'] = ''; if (isDefined($widget.red)) { $retCSS['color'] = "rgb(" + $widget.red + "," + $widget.green + "," + $widget.blue + ") "; } //check for new hasBackground element, ignore background colors unless set to yes if (isDefined($widget.hasBackground) && $widget.hasBackground == "yes") { $retCSS['background-color'] = "rgb(" + $widget.redBack + "," + $widget.greenBack + "," + $widget.blueBack + ") "; } if (isUndefined($widget.hasBackground) && isDefined($widget.redBack)) { $retCSS['background-color'] = "rgb(" + $widget.redBack + "," + $widget.greenBack + "," + $widget.blueBack + ") "; } if (isDefined($widget.size)) { $retCSS['font-size'] = $widget.size + "px "; } if (isDefined($widget.fontFamily)) { $retCSS['font-family'] = $widget.fontFamily; } if (isDefined($widget.margin)) { $retCSS['padding'] = $widget.margin + "px "; } if (isDefined($widget.borderSize)) { $retCSS['border-width'] = $widget.borderSize + "px "; } if (isDefined($widget.redBorder)) { $retCSS['border-color'] = "rgb(" + $widget.redBorder + "," + $widget.greenBorder + "," + $widget.blueBorder + ") "; $retCSS['border-style'] = 'solid'; } if (isDefined($widget.fixedWidth)) { $retCSS['width'] = $widget.fixedWidth + "px "; } if (isDefined($widget.fixedHeight)) { $retCSS['height'] = $widget.fixedHeight + "px "; } if (isDefined($widget.justification)) { if ($widget.justification == "centre") { $retCSS['text-align'] = "center"; } else { $retCSS['text-align'] = $widget.justification; } } if (isDefined($widget.style)) { switch ($widget.style) { //set font based on style attrib from xml case "1": $retCSS['font-weight'] = 'bold'; break; case "2": $retCSS['font-style'] = 'italic'; break; case "3": $retCSS['font-weight'] = 'bold'; $retCSS['font-style'] = 'italic'; break; } } return $retCSS; }; //get width of an html element by wrapping a copy in a div, then getting width of div function $getElementWidth($e) { o = $e.clone(); o.wrap('
').css({'position': 'absolute', 'float': 'left', 'white-space': 'nowrap', 'visibility': 'hidden'}).appendTo($('body')); w = o.width(); o.remove(); return w; } //place widget in correct position, rotation, z-index and scale. (pass in dom element, to simplify calling from e.load()) var $setWidgetPosition = function(e) { var $id = e.attr('id'); var $widget = $gWidgets[$id]; // look up the widget and get its panel properties if (isDefined($widget) && isDefined(e[0])) { //don't bother if widget not found (never called for beanswitch) var $height = 0; var $width = 0; // use html5 original sizes if available if (isDefined(e[0].naturalHeight)) { $height = e[0].naturalHeight * $widget.scale; } else { $height = e.height() * $widget.scale; } if (isDefined(e[0].naturalWidth)) { $width = e[0].naturalWidth * $widget.scale; } else { $width = e.width() * $widget.scale; } if ($widget.widgetFamily == "text") { //special handling to get width of free-floating text $width = $getElementWidth(e) * $widget.scale; } // calculate x and y adjustment needed to keep upper left of bounding box in the same spot // adapted to match JMRI's NamedIcon.rotate(). Note: transform-origin set in .css file var tx = 0; var ty = 0; if ($height > 0 && ($widget.degrees !== 0 || $widget.scale != 1)) { // only calc offset if needed var $rad = $toRadians($widget.degrees); if (0 <= $widget.degrees && $widget.degrees < 90 || -360 < $widget.degrees && $widget.degrees <= -270) { tx = $height * Math.sin($rad); ty = 0.0; } else if (90 <= $widget.degrees && $widget.degrees < 180 || -270 < $widget.degrees && $widget.degrees <= -180) { tx = $height * Math.sin($rad) - $width * Math.cos($rad); ty = -$height * Math.cos($rad); } else if (180 <= $widget.degrees && $widget.degrees < 270 || -180 < $widget.degrees && $widget.degrees <= -90) { tx = -$width * Math.cos($rad); ty = -$width * Math.sin($rad) - $height * Math.cos($rad); } else /* if (270<=$widget.degrees && $widget.degrees<360) */{ tx = 0.0; ty = -$width * Math.sin($rad); } } // position widget to adjusted position, set z-index, then set rotation e.css({ position : 'absolute', left : (parseInt($widget.x) + tx) + 'px', top : (parseInt($widget.y) + ty) + 'px', zIndex : $widget.level }); if ($widget.degrees !== 0) { var $rot = "rotate(" + $widget.degrees + "deg)"; e.css({ "transform" : $rot }); } // set new height and width if scale specified if ($widget.scale != 1 && $height > 0) { e.css({ height : $height + 'px', width : $width + 'px' }); } // if this is an image that's rotated or scaled, set callback to // reposition on every icon load, as the icons can be different sizes. if (e.is("img") && ($widget.degrees !== 0 || $widget.scale != 1.0)) { e.unbind('load'); e.load(function() { $setWidgetPosition($(this)); }); } } }; // reDraw an icon-based widget to reflect changes to state or occupancy var $reDrawIcon = function($widget) { // additional naming for indicator*icon widgets to reflect occupancy, error presendence status was already filtered in updateOblocks() $indicator = ""; if ($widget.widgetType == "indicatortrackicon" || $widget.widgetType == "indicatorturnouticon") { // check oblock status $indicator = ((($widget.occupancystate & 0x2) == 0x2) ? "Occupied" : ""); // look only at bit 2, compare to $drawIcon() Ostate = ($widget.occupancystate & 0xF0); // binary + 11110000, discards (in)active occupancy info in bits 1-4 $state = (Ostate | $widget.state); // adds Turnout state back in to insert TO state = position icon if (isDefined($widget.name)) { // intended for indicatorturnouts to show they are not clickable $('img#' + $widget.id).attr('title', $widget.name + ((Ostate & 0x40) == OUT_OF_SERVICE ? " (off)" : "")); // explain why not clickable TODO I18N tooltip for OOS + ERROR } } else if ($widget.widgetType == "slipturnouticon") { $state = $widget.slipState; // widget is not a bean, fetch combined state as stored in widget, calculated from 2 turnout states //log.log("STI $redrawIcon state: " + $state); // adjust some states, copied from Display/SlipTurnoutIcon#displayState(int state), not required? // if ($widget.turnoutType == "scissor") { // switch ($state) { // case 5 : // log.log("########### STI $redrawIcon state: " + $state + " set to 0 for Scissor"); // $state = 0; // break; // } // } } else { // default handling $indicator = ($widget.occupancysensor && $widget.occupancystate == ACTIVE ? "Occupied" : ""); $state = $widget.state; } // set image src to requested state's image, if defined if ($widget['icon' + $indicator + $state]) { $('img#' + $widget.id).attr('src', $widget['icon' + $indicator + ($state + "")]); } else if ($widget['defaulticon']) { // if state icon not found, use default icon if provided $('img#' + $widget.id).attr('src', $widget['defaulticon']); } else { log.error("ERROR: image not defined for " + $widget.widgetType + " " + $widget.id + ", state=" + $widget.state + ", status=" + $widget.occupancystate + ", iconstate=" + $state + " ["+$indicator+"] (icon" + $indicator + $state + ")"); } }; // set new value for widget, showing proper icon, return widgets changed var $setWidgetState = function($id, $newState, data) { var $widget = $gWidgets[$id]; // if undefined widget this must be a LE slip or a PE slipTurnoutIcon if (isUndefined($widget)) { // does it have "e" or "w" suffix? it's a slipTurnoutIcon if ($id.endsWith("e") || $id.endsWith("w")) { if (jmri_logging) { log.log("$setWidgetState STI " + $id + " to state " + $newState); } // remove suffix var $slipID = $id.slice(0, -1); // get the slip widget $widget = $gWidgets[$slipID]; // determine combined slipState for icon0/5/7/9/11 $turnoutName = data.name; // systemName //log.log("change from turnout: " + $turnoutName + " to state: " + $newState); if (($turnoutName == $widget.turnoutEast) || (data.userName == $widget.turnoutEast)) { // east turnout // also compare source by userName $widget.slipStateEast = $newState; // store turnout state e } else if (($turnoutName == $widget.turnoutWest) || (data.userName == $widget.turnoutWest)) { // west turnout // also compare source by userName $widget.slipStateWest = $newState; // store turnout state w } // handle changes from the 2 extra turnouts, if defined (they mirror the basic e and w turnouts) // only CCCC, TCCT and CTTC are valid turnout state combinations (for slipState 5, 7 and 9 respectively) if (($turnoutName == $widget.turnoutLowerWest) || (data.userName == $widget.turnoutLowerWest)) { // scissor additional west turnout, handle like turnoutEast if (($newState == CLOSED) && ((($widget.slipStateWest == CLOSED) && ($widget.slipStateEast == CLOSED)) || (($widget.slipStateWest == THROWN) && ($widget.slipStateEast == CLOSED)))) { $widget.slipStateEast = $newState; } else if (($newState == THROWN) && ($widget.slipStateWest == CLOSED) && ($widget.slipStateEast == THROWN)) { $widget.slipStateEast = $newState; } else { $newState = INCONSISTENT; } } if (($turnoutName == $widget.turnoutLowerEast) || (data.userName == $widget.turnoutLowerEast)) { // scissor additional east turnout, handle like turnoutWest if (($newState == CLOSED) && ((($widget.slipStateWest == CLOSED) && ($widget.slipStateEast == CLOSED)) || (($widget.slipStateWest == CLOSED) && ($widget.slipStateEast == THROWN)))) { $widget.slipStateWest = $newState; } else if (($newState == THROWN) && ($widget.slipStateWest == THROWN) && ($widget.slipStateEast == CLOSED)) { $widget.slipStateWest = $newState; } else { $newState = INCONSISTENT; } } if ($widget.slipStateWest == UNKNOWN || $widget.slipStateEast == UNKNOWN) { $widget.slipState = UNKNOWN; // incomplete inputs, set state UNKNOWN } else if ($newState == INCONSISTENT) { $widget.slipState = INCONSISTENT; } else { // fix some special sequences, as in java/src/jmri/jmrit/display/SlipTurnoutIcon.java#displayState(state) if ($widget.turnoutType == "threeWay" && $widget.slipStateWest == THROWN && $widget.slipStateEast == CLOSED) { // ignore slipStateEast CLOSED (slipstate 7), use slipstate 11 instead, like Panel SlipTurnoutIcon.java $widget.slipState = (THROWN << 1) | ($widget.slipStateWest >> 1) | 0x01; } else { $widget.slipState = ($widget.slipStateEast << 1) | ($widget.slipStateWest >> 1) | 0x01; } } log.log("#### $setWidgetState(slipturnouticon " + $slipID + ", " + $widget.slipState + "); (was " + $widget.slipState + ")"); $newState = $widget.slipState; // is overwritten by $newState at end of method, so temp only to pass next if-statement and redraw correctly $id = $slipID; // does it have "l" or "r" suffix? it's an LE slip } else if ($id.endsWith("l") || $id.endsWith("r")) { if (jmri_logging) { log.log("\n#### INFO: clicked slip " + $id + " to state " + $newState); } // remove suffix var $slipID = $id.slice(0, -1); // get the slip widget $widget = $gWidgets[$slipID]; // convert current slip state to current turnout states var $stateA, $stateB; //[$stateA, $stateB] = getTurnoutStatesForSlip($widget); //[$stateA, $stateB] = [$widget.turnout.state, $widget.turnoutB.state]; [$stateA, $stateB] = [$widget.stateA, $widget.stateB]; $widget.state = getSlipStateForTurnoutStates($widget, $stateA, $stateB); if (jmri_logging) { log.log("#### Slip " + $widget.name + " before: " + slipStateToString($widget.state) + ", stateA: " + turnoutStateToString($stateA) + ", stateB: " + turnoutStateToString($stateB)); } // change appropriate turnout state if ($id.endsWith("r")) { if ($stateA != $newState) { if (jmri_logging) { log.log("#### Changed r slip " + $widget.name + " $stateA from " + turnoutStateToString($stateA) + " to " + turnoutStateToString($newState)); } $stateA = $newState; $widget.stateA = $stateA; } } else if ($id.endsWith("l")) { if ($stateB != $newState) { if (jmri_logging) { log.log("#### Changed l slip " + $widget.name + " $stateB from " + turnoutStateToString($stateB) + " to " + turnoutStateToString($newState)); } $stateB = $newState; $widget.stateB = $stateB; } } // turn turnout states back into slip state $newState = getSlipStateForTurnoutStates($widget, $stateA, $stateB); if (jmri_logging) { log.log("#### Slip " + $widget.name + " after: " + slipStateToString($newState) + ", stateA: " + turnoutStateToString($stateA) + ", stateB: " + turnoutStateToString($stateB)); } if ($widget.state != $newState) { if (jmri_logging) { log.log("#### Changing slip " + $widget.name + " from " + slipStateToString($widget.state) + " to " + slipStateToString($newState)); } } //jmri_logging = false; // set $id to slip id $id = $slipID; } else if ($id.startsWith("TUR")) { //log.log("$setWidgetState(" + $id + ", " + $newState + ", " + data + ")"); $logProperties(data); var turntableID = $id.split(".")[0]; $widget = $gWidgets[turntableID]; $widget['activeRayID'] = $id; $widget['activeRayTurnout'] = data.name; $widget['activeRayState'] = turnoutStateToString($newState); $drawTurntable($widget); return; } else { if (jmri_logging) { log.log("$setWidgetState unknown $id: '" + $id + "'."); } return; } } else if ($widget.widgetType == 'layoutSlip') { // JMRI doesn't send slip states, it sends slip turnout states // so ignore this (incorrect) slip state change if (jmri_logging) { log.log("#### $setWidgetState(slip " + $id + ", " + slipStateToString($newState) + "); (was " + slipStateToString($widget.state) + ")"); } return; } if ($widget.state !== $newState) { // don't bother if already this value if (jmri_logging) { log.log("JMRI changed " + $id + " (" + $widget.jsonType + " " + $widget.name + ") from state '" + $widget.state + "' to '" + $newState + "'."); } if (data.type == "sensor" && ($widget.widgetType == "indicatortrackicon" || $widget.widgetType == "indicatortrackicon")) { $widget.occupancystate = $newState; } else { // standard handling of icon widgets $widget.state = $newState; } // override the state with idTag's "name" in a very specific circumstance if (($widget.jsonType == "memory" || $widget.jsonType == "block" || $widget.jsonType == "reporter" ) && $widget.widgetFamily == "icon" && data.value !== null && data.value.type == "idTag") { $widget.state = data.value.data.name; } switch ($widget.widgetFamily) { case "icon" : if ($widget.widgetType == "indicatortrackicon" || $widget.widgetType == "indicatortrackicon") { if ($widget.occupancysensor != "none") { $widget.occupancystate = $newState; //console.log("SET widget " + $widget.id + " to state=" + $newState); } else if ($widget.occupancyblock != "none") { // expected for turnout // if defined, follow the occupancyblock and ignore any sensors, don't set widget.state (used for turnout state) // only pick up the turnout state change, bits 0-4 $widget.state = ($newState & 0xF) ; //console.log("WARNING UNEXPECTED ITOI widget=" + $widget.id + " to state=" + $newState); // TODO clean up } } $reDrawIcon($widget); break; case "input" : $('#' + $id).val($newState); //update the input $('#' + $id).data("oldValue", $newState); //save the current value if needed for [Escape] break; case "text" : if ($widget.jsonType == "memory" || $widget.jsonType == "block" || $widget.jsonType == "reporter" ) { if ($widget.widgetType == "fastclock") { $drawClock($widget); } else { // set memory/block/reporter text or html to new value from server, clearing "null" if ($newState == null) { $('div#' + $id).text(""); } else if ($newState.startsWith("")) { $('div#' + $id).html($newState); } else { $('div#' + $id).text($newState); } } } else { if (isDefined($widget['text' + $newState])) { $('div#' + $id).text($widget['text' + $newState]); // set text to new state's text } if (isDefined($widget['css' + $newState])) { $('div#' + $id).css($widget['css' + $newState]); // set css to new state's css } } break; case "drawn" : if ($widget.widgetType == "layoutturnout") { $drawTurnout($widget); } else if ($widget.widgetType == 'layoutSlip') { $drawSlip($widget); } break; case "switch" : // Switchboard if ($widget.widgetType == "beanswitch" && isDefined($widget['shape'])) { if ($widget.shape == "button") { // update div css $('div#' + $id).text($widget['text' + $newState]); // set text to new state's text $('div#' + $id).css({"background-color": $widget['swColor' + $newState]}); } else { // icon, symbol, slider (drawing) are directly drawn on canvas $widget.text = $widget['text' + $newState]; // set text in Widget to new state's text $drawWidgetSymbol($id, $newState); } // for newly created items, reload web page to activate json binding } break; } $gWidgets[$id].state = $newState; // update the persistent widget to the new state } }; //return a unique ID # when called var $gUnique = function() { if (isUndefined($gUnique.id)) { $gUnique.id = 0; } $gUnique.id++; return $gUnique.id; }; //clean up a name, for example to use as an id var $safeName = function($name) { if (isUndefined($name)) { return "unique-" + $gUnique(); } else { return $name.replace(/:/g, "_").replace(/ /g, "_").replace(/%20/g, "_"); } }; //send request for state change var sendElementChange = function(type, name, state) { //log.log("Sending JMRI " + type + " '" + name + "' state '" + state + "'."); jmri.setObject(type, name, state); }; //show unexpected ajax errors $(document).ajaxError(function(event, xhr, opt, exception) { if (xhr.statusText != "abort" && xhr.status != 0) { var $msg = "AJAX Error requesting " + opt.url + ", status= " + xhr.status + " " + xhr.statusText; $('div#messageText').text($msg); $("#activity-alert").addClass("show").removeClass("hidden"); $('dvi#workingMessage').position({within: "window"}); log.log($msg); return; } if (xhr.statusText == "timeout") { var $msg = "AJAX timeout " + opt.url + ", status= " + xhr.status + " " + xhr.statusText + " resending list...."; log.log($msg); // TODO: need to recover somehow } }); //clear out whitespace from xml, function adapted from //http://stackoverflow.com/questions/1539367/remove-whitespace-and-line-breaks-between-html-elements-using-jquery/3103269#3103269 jQuery.fn.xmlClean = function() { this.contents().filter(function() { if (this.nodeType != 3) { $(this).xmlClean(); return false; } else { return !/\S/.test(this.nodeValue); } }).remove(); } // handle the toggling (or whatever) of the "next" state for the passed-in widget var $getNextState = function($widget) { var $nextState = undefined; $logProperties($widget); if ($widget.widgetType == 'signalheadicon') { //special case for signalheadicons switch ($widget.clickmode * 1) { // logic based on SignalHeadIcon.java case 0 : switch ($widget.state * 1) { // (* 1 is to insure numeric comparisons) case RED: case FLASHRED: $nextState = YELLOW; break; case YELLOW: case FLASHYELLOW: $nextState = GREEN; break; default: //also catches GREEN and FLASHGREEN $nextState = RED; break; } case 1 : // TODO: handle lit/unlit toggle // getSignalHead().setLit(!getSignalHead().getLit()); break; case 2 : // getSignalHead().setHeld(!getSignalHead().getHeld()); $nextState = ($widget.state * 1 == HELD ? RED : HELD); //toggle between red and held states break; case 3: // loop through all elements, finding iconX and get "next one", skipping special ones var $firstState = undefined; var $currentState = undefined; for (k in $widget) { var s = k.substr(4) * 1; //extract the state from current icon var, insure it is treated as numeric //get valid value, name starts with 'icon', but not the HELD or DARK ones if (k.indexOf('icon') == 0 && isDefined($widget[k]) && k != 'icon' + HELD && k != 'icon' + DARK) { if (isUndefined($firstState)) $firstState = s; //remember the first state (for last one) if (isDefined($currentState) && isUndefined($nextState)) $nextState = s; //last one was the current, so this one must be next if (s == $widget.state) $currentState = s; // log.log('key: '+k+" first="+$firstState+" current="+$currentState+" next="+$nextState); } } if (isUndefined($nextState)) $nextState = $firstState; // if still not set, start over } } else if ($widget.widgetType == 'signalmasticon') { // special case for signalmasticons // loop through all elements, finding iconXXX and get next iconXXX, skipping special ones switch ($widget.clickmode * 1) { // logic based on SignalMastIcon.java case 0 : var $firstState = undefined; var $currentState = undefined; for (k in $widget) { var s = k.substr(4); //extract the state from current icon var //look for next icon value, skipping Held, Dark and Unknown if (k.indexOf('icon') == 0 && isDefined($widget[k]) && s != 'Held' && s != 'Dark' && s !='Unlit' && s != 'Unknown') { if (isUndefined($firstState)) $firstState = s; // remember the first state (for last one) if (isDefined($currentState) && isUndefined($nextState)) $nextState = s; // last one was the current, so this one must be next if (s == $widget.state) $currentState = s; } }; if (isUndefined($nextState)) $nextState = $firstState; // if still not set, start over break; case 1 : //TODO: handle lit/unlit states break; case 2 : //toggle between stop and held state $nextState = ($widget.state == "Held" ? "Stop" : "Held"); break; }; } else if ($widget.widgetType == 'slipturnouticon') { // slipturnouticons store the current state in .slipState, not .state switch ($widget.turnoutType) { // logic based on java/src/jmri/jmrit/display/SlipTurnoutIcon.java case "doubleSlip" : $nextState = ($widget.slipState == 11 ? 5 : $widget.slipState + 2); break; case "singleSlip" : if ($widget.singleSlipRoute == "lowerWestToLowerEast") { switch ($widget.slipState) { case 5 : $nextState = 9; break; case 9 : $nextState = 11; break; case 11 : $nextState = 5; break; } } else if ($widget.singleSlipRoute == "upperWestToUpperEast") { switch ($widget.slipState) { case 5 : $nextState = 11; break; case 7 : $nextState = 5; break; case 11 : $nextState = 7; break; } } break; case "threeWay" : if ($widget.firstTurnoutExit == "lower") { $nextState = ($widget.slipState == 9 ? 5 : $widget.slipState + 2); } else { // $widget.firstTurnoutExit == "upper" switch ($widget.slipState) { case 5 : $nextState = 9; break; case 9 : $nextState = 11; break; case 11 : $nextState = 5; break; } } break; case "scissor" : $nextState = ($widget.slipState == 9 ? 5 : $widget.slipState + 2); // State 11 not allowed for a scissor // does not provide 5 after 7 as it would require extra logic $nextState = ($widget.slipState == 9 ? 5 : $widget.slipState + 2); break; }; } else { // default: start with INACTIVE, then toggle to ACTIVE and back (same for turnout states: 2 <> 4) $nextState = ($widget.state == ACTIVE ? INACTIVE : ACTIVE); } if (isUndefined($nextState)) $nextState = $widget.state; //default to no change return $nextState; }; // preload all images referred to by the widget var $preloadWidgetImages = function($widget) { for (k in $widget) { if (k.indexOf('icon') == 0 && isDefined($widget[k]) && $widget[k] !== "yes") { //if attribute names starts with 'icon', it's an image, so preload it $(""); } } }; // determine widget "family" for broadly grouping behaviors // note: not-yet-supported widgets are commented out here so as to return undefined var $getWidgetFamily = function($widget, $element) { if (($widget.widgetType == "positionablelabel" || $widget.widgetType == "linkinglabel" || $widget.widgetType == "audioicon" || $widget.widgetType == "logixngicon") && isDefined($widget.text)) { return "text"; //special case to distinguish text vs. icon labels } if ($widget.widgetType == "sensoricon" && $widget.icon == "no") { return "text"; //special case to distinguish text vs. icon labels } if ($widget.widgetType == "memoryicon" && $($element).find('memorystate').length == 0) { return "text"; //if no memorystate icons, treat as text } switch ($widget.widgetType) { case "locoicon" : case "trainicon" : case "fastclock" : case "BlockContentsIcon" : case "reportericon" : return "text"; break; case "memorySpinnerIcon" : case "memoryComboIcon" : case "memoryInputIcon" : case "blockContentsInputIcon" : return "input"; break; case "positionablelabel" : case "audioicon" : case "logixngicon" : case "linkinglabel" : case "turnouticon" : case "outputindicator" : case "sensoricon" : case "LightIcon" : case "multisensoricon" : case "signalheadicon" : case "signalmasticon" : case "indicatortrackicon" : case "indicatorturnouticon" : case "memoryicon" : case "slipturnouticon" : return "icon"; break; case 'layoutSlip' : case "layoutturnout" : case "tracksegment" : case "positionablepoint" : case "backgroundColor" : case "layoutblock" : case "levelxing" : case "layoutturntable" : case "layoutShape" : case "positionableRectangle" : case "positionableRoundRect" : case "positionableCircle" : case "positionableEllipse" : return "drawn"; break; case "beanswitch" : return "switch"; break; } log.log("unhandled widget type of '" + $widget.widgetType +"' id = "+$widget.id); return; //unrecognized widget returns undefined }; function listPanels(name) { $.ajax({ url: "/panel/?format=json", data: {}, success: function(data, textStatus, jqXHR) { if (data.length !== 0) { $.each(data, function(index, value) { $gPanelList[value.data.userName] = value.data.name; }); } if (name === null || typeof (panelName) === undefined) { if (data.length !== 0) { $("#panel-list").empty(); $("#activity-alert").addClass("hidden").removeClass("show"); $("#panel-list").addClass("show").removeClass("hidden"); $.each(data, function(index, value) { $("#panel-list").append(""); // (12 / col-lg-#) % index + 1 if (4 % (index + 1)) { $("#panel-list").append("
"); } // (12 / col-md-#) % index + 1 if (3 % (index + 1)) { $("#panel-list").append("
"); } // (12 / col-sm-#) % index + 1 if (2 % (index + 1)) { $("#panel-list").append("
"); } }); // resizeThumbnails(); // sometimes gets .thumbnail sizes too small under image. TODO Fix it } else { $("#activity-alert").addClass("hidden").removeClass("show"); $("#warning-no-panels").addClass("show").removeClass("hidden"); } } } }); } function resizeThumbnails() { tallest = 0; $(".thumbnail-image").each(function() { thisHeight = $("img", this).height(); if (thisHeight > tallest) { tallest = thisHeight; } }); $(".thumbnail-image").each(function() { $(this).height(tallest); }); } $(window).resize(function() { resizeThumbnails(); }); //-----------------------------------------javascript processing starts here (main) --------------------------------------------- $(document).ready(function() { // get panel name if passed as a parameter var panelName = getParameterByName("name"); // get panel name if part of the path if (panelName === null || typeof (panelName) === undefined) { var path = $(location).attr('pathname'); path = path.split("/"); if (path.length > 3) { panelName = path[path.length - 2] + "/" + path[path.length - 1]; } } // setup the functional menu items $("#navbar-panel-reload > a").attr("href", location.href); $("#navbar-panel-xml > a").attr("href", location.pathname + "?format=xml"); // show panel thumbnails if no panel name listPanels(panelName); if (panelName === null || typeof (panelName) === undefined) { $("#panel-list").addClass("show").removeClass("hidden"); $("#panel-area").addClass("hidden").removeClass("show"); // hide the Show XML menu when listing panels $("#navbar-panel-xml").addClass("hidden").removeClass("show"); } else { // note: the functions and parameter names must match exactly those in web/js/jquery.jmri.js // see for example jmri/server/json/turnout/turnout-server.json jmri = $.JMRI({ didReconnect: function() { // if a reconnect is triggered, reload the page - it is the // simplest method to refresh every object in the panel log.log("Reloading at reconnect"); location.reload(false); }, audio: function(name, state, data) { $.each(whereUsed[name], function(index, widgetId) { $widget = $gWidgets[widgetId]; $widget['state'] = state; if (state == 16 && $widget['stopSoundWhenJmriStops']) { // Sound is stopped $widget['audio_widget'].pause(); $widget['audio_widget'].currentTime = 0; } else if (state == 17 && $widget['playSoundWhenJmriPlays']) { // Sound is playing $widget['audio_widget'].currentTime = 0; $widget['audio_widget'].loop = (data.playNumLoops == -1); $widget['audio_widget'].play(); } }); }, audioicon: function(identity, command, playNumLoops) { $widget = audioIconIDs['audioicon:'+identity]; if (command == "Play") { $widget['audio_widget'].loop = (playNumLoops == -1); $widget['audio_widget'].play(); } else if (command == "Stop") { $widget['audio_widget'].pause(); $widget['audio_widget'].currentTime = 0; } }, light: function(name, state, data) { updateWidgets(name, state, data); }, block: function(name, value, data) { //console.log("HEARD BLOCK " + name + " value=" + value); if (value !== null) { if (value.type == "idTag") { value = value.data.userName; // for idTags, use the value in userName instead } else if (value.type == "reporter") { value = value.data.value; // for reporters, use the value in data instead } else if (value.type == "rosterEntry") { if (value.data.icon !== null) { value = ""; // for rosterEntries, create an image tag instead } else { value = value.data.name; // if roster icon not set, just show the name } } } updateWidgets(name, value, data); }, oblock: function(name, status, data) { // data contains data.status (Allocated, Occupied,... not state) //console.log("HEARD JSON OBLOCK " + name + " status=" + status + " (" + data.status + ")"); if (data.status !== null) { updateOblocks(name, data.status); // only for indicator(turnout)trackicon widgets } }, layoutBlock: function(name, value, data) { setBlockColor(name, data.blockColor); }, memory: function(name, value, data) { if (value !== null) { //console.log("MEMORY " + name + " value=" + value + " data=" + data); if (value.type == "idTag") { value = value.data.userName; // for idTags, use the value in userName instead } else if (value.type == "reporter"){ value = value.data.value; // for reporters, use the value in data instead } else if (value.type == "rosterEntry") { if (value.data.icon !== null) { value = ""; // for rosterEntries, create an image tag instead } else { value = value.data.name; // if roster icon not set, just show the name } } } updateWidgets(name, value, data); }, reporter: function(name, value, data) { //console.log("REPORTER " + name + " value=" + value + " data=" + data); updateWidgets(name, value, data); }, route: function(name, state, data) { updateWidgets(name, state, data); }, sensor: function(name, state, data) { updateOccupancy(name, state, data); //console.log("Sensor " + name + " state=" + state); updateWidgets(name, state, data); }, signalHead: function(name, state, data) { updateWidgets(name, state, data); }, signalMast: function(name, state, data) { updateWidgets(name, state, data); }, turnout: function(name, state, data) { //console.log("Turnout " + name + " state=" + state); updateWidgets(name, state, data); } }); $("#panel-list").addClass("hidden").removeClass("show"); $("#panel-area").addClass("show").removeClass("hidden"); // include name of panel in page title. Will be updated to userName later setTitle("Loading " + panelName + "..."); //get updates to fast clock rate getRateFactor(); // request actual xml of panel, and process it on return // uses setTimeout simply to not block other JavaScript since // requestPanelXML has a long timeout setTimeout(function() { requestPanelXML(panelName); }, 500); } }); //------------------------------------------- end of main ------------------------------------------- // Add Widget to store fastclock rate function getRateFactor() { $widget = new Array(); $widget.jsonType = "memory"; $widget['name'] = "IMRATEFACTOR"; // already defined in JMRI $widget['id'] = $widget['name']; $widget['safeName'] = $widget['name']; $widget['systemName'] = $widget['name']; $widget['state'] = "1.0"; $gWidgets[$widget.id] = $widget; if (!($widget.systemName in whereUsed)) { //set where-used for this new memory whereUsed[$widget.systemName] = new Array(); } whereUsed[$widget.systemName][whereUsed[$widget.systemName].length] = $widget.id; } /****************************************************************** * ======= Switchboard functions ======= */ // used to find largest tiles on Switchboard screen function autoRows(screenwidth, screenheight) { // calculations repeated from SwitchboardEditor for web display // find cell matrix that allows largest size icons var $cellProp = 1; // assume square tiles prop 1:1 to keep it simple for now var $paneEffectiveWidth = Math.ceil(screenwidth / $cellProp); var $columnsNum = 1; var $rowsNum = 1; var $tileSize = 0.1; // start value var $tileSizeOld = 0; var $totalDisplayed = Math.max($total, 1); // if all items unconnected and set to be hidden, use 1 while ($tileSize > $tileSizeOld) { $rowsNum = ($totalDisplayed + $columnsNum - 1) / $columnsNum; // roundup int $tileSizeOld = $tileSize; // store for comparison $tileSize = Math.min(($paneEffectiveWidth / $columnsNum), ((screenheight - 90) / $rowsNum)); // screenheight-90px to leave room for menubar if ($tileSize <= $tileSizeOld) { break; } $columnsNum++; } return $rowsNum; } function getSwitchButtonLabel(label, subLabel) { if (($showUserName == "no") || (subLabel == "") || isUndefined(subLabel)) { return label; } else { subLabel = subLabel.substring(0, (Math.min(subLabel.length, 25))); return label + " (" + subLabel + ")"; // will wrap but TODO show on 2 lines of text } } // Draw symbol on the beanswitch widget canvas var $drawWidgetSymbol = function(id, state) { // draw on $widget canvas var $canvas = document.getElementById(id + "c"); var shape = $gWidgets[id].shape; if (shape == "button" || typeof $canvas === null) { return; // no canvas created (shape = "buttons") } var ctx = $canvas.getContext("2d"); ctx.save(); // backgroundcolor shows through by inherit ctx.clearRect(0, 0, $canvas.width, $canvas.height); // for alternating text and 'moving' items ctx.fillStyle = (state == "2" ? $activeColor : $inactiveColor); // simple change in color ctx.strokeStyle = "black"; ctx.translate($canvas.width/2, $canvas.height/2); // origin in center of canvas, easy! var radius = Math.min($canvas.width * 0.3, $canvas.height * 0.3); switch (shape) { // draw methods case "icon" : // slider, 1 shape for all switchtypes (S, T, L) ctx.beginPath(); // the sliderspace if (state == "2") { ctx.strokeStyle = $activeColor; } else if (state == "4") { ctx.strokeStyle = $inactiveColor; } else { ctx.strokeStyle = "darkgray"; } ctx.lineCap = "round"; ctx.lineWidth = radius; ctx.moveTo(-radius/2, 0); ctx.lineTo(radius/2, 0); ctx.stroke(); ctx.beginPath(); // the knob var knobX = (state == "2" ? radius/2 : -radius/2); ctx.arc(knobX, 0, radius/2, 0, 2 * Math.PI); ctx.fillStyle = "white"; ctx.fill(); ctx.strokeStyle = "black"; ctx.lineWidth = 1; ctx.stroke(); break; case "drawing" : // Maerklin Keyboard, 1 shape for all switchtypes (S, T, L) // red = upper rounded rect ctx.fillStyle = (state == "2" ? $activeColor : "pink"); ctx.fillRect(-0.5*radius, -1.1*radius, radius, radius/3); // + rounded outline ctx.lineJoin = "round"; ctx.lineWidth = radius/5; ctx.strokeStyle = (state == "2" ? $activeColor : "pink"); ctx.strokeRect(-0.5*radius, -1.1*radius, radius, radius/3); // green = lower rounded rect ctx.fillStyle = (state == "4" ? $inactiveColor : "lightgreen"); ctx.fillRect(-0.5*radius, 1.1*radius, radius, radius/-3); // + rounded outline ctx.lineJoin = "round"; ctx.lineWidth = 10; ctx.strokeStyle = (state == "4" ? $inactiveColor : "lightgreen"); ctx.strokeRect(-0.5*radius, 1.1*radius, radius, radius/-3); // add round LED at top var grd = ctx.createRadialGradient(-0.1*radius, -1.4*radius, 0.5*radius, 0.1*radius, -1.6*radius, 0); grd.addColorStop(0, (state == "2" ? $activeColor : "lightgray")); grd.addColorStop(1, "white"); ctx.fillStyle = grd; ctx.arc(0, -1.55*radius, radius/6, 0, 2 * Math.PI); ctx.fill(); ctx.lineWidth = 0.2; ctx.strokeStyle = "black"; ctx.stroke(); break; case "symbol" : // Mimic classic icons as vector drawing, specific shape per switchtype (S, T, L) switch ($gWidgets[id].type) { case "L" : // light // line (wire) at back ctx.beginPath(); ctx.lineWidth = (state == "2" ? "3" : "1"); // thinner outline if Off ctx.moveTo(-0.4 * $canvas.width, 0); ctx.lineTo(0.4 * $canvas.width, 0); ctx.stroke(); // filled circle var grd = ctx.createRadialGradient(0, 0, 1.5 * radius, 8, -8, 4); grd.addColorStop(0, (state == "2" ? "yellow" : "lightgray")); grd.addColorStop(1, "white"); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(0, 0, radius, 0, 2 * Math.PI); ctx.fill(); ctx.lineWidth = (state == "2" ? "3" : "1"); // thinner outline if Off ctx.stroke(); // cross ctx.lineWidth = 1; ctx.moveTo(radius * -0.74, radius * -0.74); ctx.lineTo(radius * 0.74, radius * 0.74); ctx.stroke(); ctx.lineWidth = 1; ctx.moveTo(radius * -0.74, radius * 0.74); ctx.lineTo(radius * 0.74, radius * -0.74); ctx.stroke(); break; case "S" : // sensor var grd = ctx.createRadialGradient(0, 0, 1.5 * radius, 8, -8, 4); grd.addColorStop(0, (state == "2" ? $activeColor : "lightgray")); grd.addColorStop(1, "white"); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(0, 0, radius, 0, 2 * Math.PI); ctx.fill(); ctx.lineWidth = (state == "2" ? "3" : "1"); // thinner outline if Off ctx.stroke(); break; case "T" : // turnout, orientation on screen same as JMRI default : ctx.lineWidth = radius/2.9; // points, at the back ctx.strokeStyle = "lightgray"; // --angled turnout shape //ctx.moveTo(-0.4 * $canvas.width, -20); //ctx.lineTo(0.1 * $canvas.width, 10); //ctx.lineTo(-0.4 * $canvas.width, 10); // --curved turnout shape ctx.moveTo(0.4 * $canvas.width, 10); ctx.lineTo(-0.4 * $canvas.width, 10); ctx.stroke(); ctx.beginPath(); ctx.arc(0.4 * $canvas.width, 10 - 1.5 * $canvas.width, 1.5 * $canvas.width, 0.5 * Math.PI, 0.675 * Math.PI); // --up to here ctx.stroke(); // active line, start with new color ctx.beginPath(); ctx.strokeStyle = $activeColor; // --angled turnout shape //var endY = (state == "2" ? "10" : "-20"); //ctx.moveTo(0.4 * $canvas.width, 10); //ctx.lineTo(0.1 * $canvas.width, 10); //ctx.lineTo(-0.4 * $canvas.width, endY); // --curved turnout shape if (state == "2") { ctx.moveTo(0.4 * $canvas.width, 10); ctx.lineTo(-0.4 * $canvas.width, 10); } else { ctx.arc(0.4 * $canvas.width, 10 - 1.5 * $canvas.width, 1.5 * $canvas.width, 0.5 * Math.PI, 0.675 * Math.PI); } // --up to here ctx.stroke(); break; } default : // only render label } // draw label (system name + state) text //ctx.restore(); // resets origin and stroke&fill ctx.fillStyle = (state == "0" ? $unknownColor : $gPanel.defaulttextcolor); // simple change in color ctx.font = "16px Arial"; ctx.textAlign = 'center'; if (shape == "drawing") { // text centered vertically between Maerklin buttons ctx.fillText($gWidgets[id].text, 0, 0); } else { ctx.fillText($gWidgets[id].text, 0, -0.5 * $canvas.height + 16); // +16 for text size below top } // draw sublabel (user name) text ctx.font = "italic 10px Arial"; if (shape == "drawing") { // text centered between Maerklin buttons ctx.fillText($gWidgets[id].username, 0, 0.4 * $canvas.height); } else { ctx.fillText($gWidgets[id].username, 0, 0.4 * $canvas.height); } ctx.restore(); // restore color and width back to default }; // End of Swichboard functions /****************************************************************** * ======= Layout Editor functions ======= */ //draw a Tracksegment (pass in widget) function $drawTrackSegment($widget) { //if set to hidden, don't draw anything if ($widget.hidden == "yes") { return; } // if positional points have not been loaded... if (Object.keys($gPts).length == 0) { return; // ... don't try to draw anything yet } //get the endpoints by name var $ep1, $ep2; [$ep1, $ep2] = $getEndPoints$($widget); if (isUndefined($ep1)) { log.warn("can't draw tracksegment " + $widget.ident + ": connect1: " + $widget.connect1name + "." + $widget.type1 + " undefined."); return; } if (isUndefined($ep2)) { log.warn("can't draw tracksegment " + $widget.ident + ": connect2: " + $widget.connect2name + "." + $widget.type2 + " undefined."); return; } $gCtx.save(); // save current line width and color //get width (assume no block assigned) var $width = $gPanel.sidelinetrackwidth; if ($widget.mainline == "yes") { $width = $gPanel.mainlinetrackwidth; } var $color = $getTrackColor($widget); var $blk = $gBlks[$widget.blockname]; if (isDefined($blk)) { $color = $blk.blockcolor; //block assigned; use block width $width = $gPanel.sidelineblockwidth; if ($widget.mainline == "yes") { $width = $gPanel.mainlineblockwidth; } } // set color and width if (isDefined($color)) { $gCtx.strokeStyle = $color; } if (isDefined($width)) { $gCtx.lineWidth = $width; } if ($widget.dashed == "yes") { $gCtx.setLineDash([6, 4]); } if ($widget.bezier == "yes") { $drawTrackSegmentBezier($widget); } else if ($widget.circle == "yes") { $drawTrackSegmentCircle($widget); } else if ($widget.arc == "yes") { //draw arc of ellipse $drawTrackSegmentArc($widget); } else { $drawLine($ep1.x, $ep1.y, $ep2.x, $ep2.y, $color, $width); } if ($widget.dashed == "yes") { $gCtx.setLineDash([]); } //draw its decorations $drawDecorations($widget); $gCtx.restore(); // restore color and width back to default } // $drawTrackSegment function $drawTrackSegmentBezier($widget) { //get the endpoints by name var ep1, ep2; [ep1, ep2] = $getEndPoints($widget); var $cps = $widget.controlpoints; // get the control points var points = [[ep1[0], ep1[1]]]; // first point $cps.each(function( idx, elem ) { // control points points.push($getLayoutPoint(elem)); }); points.push([ep2[0], ep2[1]]); // last point //$point_log("points[0]", points[0]); $drawBezier(points, $gCtx.strokeStyle, $gCtx.lineWidth, 0); } function $drawTrackSegmentCircle($widget) { //get the endpoints by name var $ep1, $ep2; [$ep1, $ep2] = $getEndPoints$($widget); if (isUndefined($widget.angle) || ($widget.angle == 0)) { $widget['angle'] = "90"; } //draw curved line if ($widget.flip == "yes") { $drawArc($ep2.x, $ep2.y, $ep1.x, $ep1.y, $widget.angle); } else { $drawArc($ep1.x, $ep1.y, $ep2.x, $ep2.y, $widget.angle); } } function $drawTrackSegmentArc($widget) { //get the endpoints by name var $ep1, $ep2; [$ep1, $ep2] = $getEndPoints$($widget); var ep1x = Number($ep1.x), ep1y = Number($ep1.y), ep2x = Number($ep2.x), ep2y = Number($ep2.y); if ($widget.flip == "yes") { [ep1x, ep1y, ep2x, ep2y] = [ep2x, ep2y, ep1x, ep1y]; } var x, y; var rw = ep2x - ep1x, rh = ep2y - ep1y; var startAngleRAD, stopAngleRAD; if (rw < 0) { rw = -rw; if (rh < 0) { //log.log("**** QUAD ONE ****"); x = ep1x; y = ep2y; rh = -rh; startAngleRAD = Math.PI / 2; stopAngleRAD = Math.PI; } else { //log.log("**** QUAD TWO ****"); x = ep2x; y = ep1y; startAngleRAD = 0; stopAngleRAD = Math.PI / 2; } } else { if (rh < 0) { //log.log("**** QUAD THREE ****"); x = ep2x; y = ep1y; rh = -rh; startAngleRAD = Math.PI; stopAngleRAD = -Math.PI / 2; } else { //log.log("**** QUAD FOUR ****"); x = ep1x; y = ep2y; startAngleRAD = -Math.PI / 2; stopAngleRAD = 0; } } $drawEllipse(x, y, rw, rh, startAngleRAD, stopAngleRAD); } function $getEndPoints$($widget) { var $ep1 = $gPts[$widget.connect1name + "." + $widget.type1]; var $ep2 = $gPts[$widget.connect2name + "." + $widget.type2]; return [$ep1, $ep2]; } function $getEndPoints($widget) { var $ep1, $ep2; [$ep1, $ep2] = $getEndPoints$($widget); var ep1 = [Number($ep1.x), Number($ep1.y)]; var ep2 = [Number($ep2.x), Number($ep2.y)]; return [ep1, ep2]; } // //draw decorations // function $drawDecorations($widget) { if (isDefined($widget.arrow)) { $widget.arrow.draw(); } if (isDefined($widget.bridge)) { $widget.bridge.draw(); } if (isDefined($widget.bumper)) { $widget.bumper.draw(); } if (isDefined($widget.tunnel)) { $widget.tunnel.draw(); } } // $drawDecorations // draw a turntable (pass in widget) // from jmri.jmrit.display.layoutEditor.layoutTurntable function $drawTurntable($widget) { $logProperties($widget); //get the center var $txcen = $widget.xcen * 1; var $tycen = $widget.ycen * 1; var $tr = $widget.radius * 1; //turntable circle radius var $cr = $gPanel.turnoutcirclesize * SIZE; //turnout circle radius //var $cd = $cr * 2; //the fraction that $cr is of ($tr + $cr) //(used to draw ray tracks from circle to ray end point (control circle)) var f = $cr / ($tr + $cr); //loop thru raytracks drawing each one (and control circles if it has a turnout) $widget.raytracks.each(function(i, item) { $logProperties(item); //var rayID = $widget.ident + "." + (50 + item.attributes.index.value * 1); var rayID = $widget.ident + ".TURNTABLE_RAY_" + (item.attributes.index.value * 1); var $t = $gPts[rayID]; //draw the line from ray endpoint to turntable edge var $t1 = []; $t1['x'] = $t.x - (($t.x - $txcen) * f); $t1['y'] = $t.y - (($t.y - $tycen) * f); $drawLine($t1.x, $t1.y, $t.x, $t.y, $getTrackColor($widget), $gPanel.sidelinetrackwidth); if (isDefined(item.attributes.turnout) && ($gPanel.controlling == "yes")) { // var turnout = item.attributes.turnout.value; // var state = item.attributes.turnoutstate.value; // log.log("$drawTurntable ray # " + i + " turnout: '" + turnout + "', state: " + state); //draw the turnout control circle $drawCircle($t.x, $t.y, $cr, $gPanel.turnoutcirclecolor, 1); } if (isDefined($widget.activeRayID)) { var drawFlag = false; if (isDefined(item.attributes.turnout)) { var turnout = item.attributes.turnout.value; if (turnout == $widget.activeRayTurnout) { var state = item.attributes.turnoutstate.value; if (state.toUpperCase() == $widget.activeRayState) { drawFlag = true; } } } var $angle = $toRadians(item.attributes.angle.value); var $t1 = []; $t1['x'] = $txcen + ($tr * Math.sin($angle)); $t1['y'] = $tycen - ($tr * Math.cos($angle)); var $t2 = []; $t2['x'] = $txcen - ($tr * Math.sin($angle)); $t2['y'] = $tycen + ($tr * Math.cos($angle)); if (drawFlag) { $drawLine($t1.x, $t1.y, $t2.x, $t2.y, $getTrackColor($widget), $gPanel.sidelinetrackwidth); } else { $drawLine($t1.x, $t1.y, $t2.x, $t2.y, $gPanel.backgroundcolor, $gPanel.sidelinetrackwidth); } } }); var $turntablecirclelinewidth = 2; //matches LayoutTurntableView.java $drawCircle($txcen, $tycen, $tr, $getTrackColor($widget), $turntablecirclelinewidth); $drawCircle($txcen, $tycen, $tr / 4, $getTrackColor($widget), $turntablecirclelinewidth); } //$drawTurntable //draw a LevelXing (pass in widget) function $drawLevelXing($widget) { //if set to hidden, don't draw anything if ($widget.hidden == "yes") { return; } //set colors and widths based on connected segments and blocks var $colorAC = $getLegColor($gWidgets[$widget.connectaname], $widget.blocknameac); var $colorBD = $getLegColor($gWidgets[$widget.connectbname], $widget.blocknamebd); var $widthAC = $getLegWidth($gWidgets[$widget.connectaname], $widget.blocknameac); var $widthBD = $getLegWidth($gWidgets[$widget.connectbname], $widget.blocknamebd); //retrieve the points var cen = [$widget.xcen, $widget.ycen]; var a = $getPoint($widget.ident + LEVEL_XING_A); var b = $getPoint($widget.ident + LEVEL_XING_B); var c = $getPoint($widget.ident + LEVEL_XING_C); var d = $getPoint($widget.ident + LEVEL_XING_D); //levelxing A // D-+-B // C $drawLineP(a, c, $colorAC, $widthAC); //A to C $drawLineP(b, d, $colorBD, $widthBD); //B to D } //draw a Turnout (pass in widget) // see LayoutTurnout.draw() // colors and widths based on side vs main then block color, turnout can be all one block, or several blocks function $drawTurnout($widget) { //if set to hidden, don't draw anything if ($widget.hidden == "yes") { return; } //set erase color and width var $eraseColor = $gPanel.backgroundcolor; var $eraseWidth = $gPanel.mainlinetrackwidth; //set colors and widths based on connected segments and blocks var $colorA = $getLegColor($gWidgets[$widget.connectaname], $widget.blockname); var $colorB = $getLegColor($gWidgets[$widget.connectbname], ($widget.blockbname ? $widget.blockbname : $widget.blockname)); //use bname if set var $colorC = $getLegColor($gWidgets[$widget.connectcname], ($widget.blockcname ? $widget.blockcname : $widget.blockname)); //use cname if set var $colorD = $getLegColor($gWidgets[$widget.connectdname], ($widget.blockdname ? $widget.blockdname : $widget.blockname)); //use dname if set var $widthA = $getLegWidth($gWidgets[$widget.connectaname], $widget.blockname); var $widthB = $getLegWidth($gWidgets[$widget.connectbname], ($widget.blockbname ? $widget.blockbname : $widget.blockname)); //use bname if set var $widthC = $getLegWidth($gWidgets[$widget.connectcname], ($widget.blockcname ? $widget.blockcname : $widget.blockname)); //use cname if set var $widthD = $getLegWidth($gWidgets[$widget.connectdname], ($widget.blockdname ? $widget.blockdname : $widget.blockname)); //use dname if set var cen = [$widget.xcen * 1, $widget.ycen * 1] var a = $getPoint($widget.ident + ".TURNOUT_A"); var b = $getPoint($widget.ident + ".TURNOUT_B"); var c = $getPoint($widget.ident + ".TURNOUT_C"); var ab = $point_midpoint(a, b); //turnout A--+--B // \-C if ($widget.type == LH_TURNOUT || $widget.type == RH_TURNOUT || $widget.type == WYE_TURNOUT) { //always draw from a to cen $drawLineP(a, cen, $colorA, $widthA); //a to cen //if closed or thrown, draw the selected leg and erase the other one if ($widget.state == CLOSED || $widget.state == THROWN) { if ($widget.state == $widget.continuing) { $drawLineP(cen, c, $eraseColor, $widthC+1); //erase center to C (diverging leg) if ($gPanel.turnoutdrawunselectedleg == 'yes') { $drawLineP(c, $point_midpoint(cen, c), $colorC, $widthC); //C to midC (diverging leg) } $drawLineP(cen, b, $colorB, $widthB); //center to B (straight leg) } else { $drawLineP(cen, b, $eraseColor, $widthB+1); //erase center to B (straight leg) if ($gPanel.turnoutdrawunselectedleg == 'yes') { $drawLineP(b, $point_midpoint(cen, b), $colorB, $widthB); //B to midB (straight leg) } $drawLineP(cen, c, $colorC, $widthC); //center to C (diverging leg) } } else { //if state is undefined, draw both legs $drawLineP(cen, b, $colorB, $widthB); //center to B (straight leg) $drawLineP(cen, c, $colorC, $widthC); //center to C (diverging leg) } // xover A--B // D--C } else if ($widget.type == LH_XOVER || $widget.type == RH_XOVER || $widget.type == DOUBLE_XOVER) { var d = $getPoint($widget.ident + ".TURNOUT_D"); var ab = $point_midpoint(a, b); var cd = $point_midpoint(c, d); if ($widget.state == CLOSED || $widget.state == THROWN) { $drawLineP(a, b, $eraseColor, $eraseWidth); //erase A to B $drawLineP(c, d, $eraseColor, $eraseWidth); //erase C to D $drawLineP(ab, cd, $eraseColor, $eraseWidth); //erase midAB to midDC $drawLineP(a, c, $eraseColor, $eraseWidth); //erase A to C $drawLineP(b, d, $eraseColor, $eraseWidth); //erase B to D if ($widget.state == $widget.continuing) { //draw closed legs $drawLineP(a, ab, $colorA, $widthA); //A to mid ab $drawLineP(b, ab, $colorB, $widthB); //B to mid ab $drawLineP(c, cd, $colorC, $widthC); //C to mid cd $drawLineP(d, cd, $colorD, $widthD); //D to mid cd //draw open legs if ($widget.type == DOUBLE_XOVER) { var acen = $point_midpoint(a, cen); var bcen = $point_midpoint(b, cen); var ccen = $point_midpoint(c, cen); var dcen = $point_midpoint(d, cen); $drawLineP(acen, cen, $colorA, $widthA); //mid a cen to cen $drawLineP(bcen, cen, $colorB, $widthB); //mid b cen to cen $drawLineP(ccen, cen, $colorC, $widthC); //mid c cen to cen $drawLineP(dcen, cen, $colorD, $widthD); //mid d cen to cen } else if ($widget.type == RH_XOVER) { $drawLineP($point_midpoint(ab, cen), cen, $colorA, $widthA); $drawLineP($point_midpoint(cd, cen), cen, $colorC, $widthC); } else if ($widget.type == LH_XOVER) { $drawLineP($point_midpoint(ab, cen), cen, $colorB, $widthB); $drawLineP($point_midpoint(cd, cen), cen, $colorD, $widthD); } } else { var aab = $point_midpoint(a, ab); var abb = $point_midpoint(ab, b); var ccd = $point_midpoint(c, cd); var cdd = $point_midpoint(cd, d); if ($widget.type == DOUBLE_XOVER) { //draw open legs $drawLineP(ab, aab, $colorA, $widthA); $drawLineP(ab, abb, $colorB, $widthB); $drawLineP(cd, ccd, $colorC, $widthC); $drawLineP(cd, cdd, $colorD, $widthD); //draw closed legs $drawLineP(a, cen, $colorA, $widthA); $drawLineP(b, cen, $colorB, $widthB); $drawLineP(c, cen, $colorC, $widthC); //C to cen $drawLineP(d, cen, $colorD, $widthD); //D to cen } else if ($widget.type == RH_XOVER) { //draw open legs $drawLineP(b, abb, $colorB, $widthB); $drawLineP(d, cdd, $colorD, $widthD); //draw closed legs $drawLineP(a, ab, $colorA, $widthA); //A to mid ab $drawLineP(ab, cen, $colorA, $widthA); //midAB to cen $drawLineP(cen, cd, $colorC, $widthC); //cen to midDC $drawLineP(c, cd, $colorC, $widthC); //C to mid cd } else { //LH_XOVER //draw open legs $drawLineP(a, aab, $colorA, $widthA); $drawLineP(c, ccd, $colorC, $widthC); //draw closed legs $drawLineP(b, ab, $colorB, $widthB); //B to mid ab $drawLineP(ab, cen, $colorB, $widthB); //midAB to cen $drawLineP(cen, cd, $colorD, $widthD); //cen to midDC $drawLineP(d, cd, $colorD, $widthD); //D to mid cd } } } else { //if state is undefined, draw all legs $drawLineP(a, ab, $colorA, $widthA); //A to mid ab $drawLineP(b, ab, $colorB, $widthB); //B to mid ab $drawLineP(c, cd, $colorC, $widthC); //C to mid cd $drawLineP(d, cd, $colorD, $widthD); //D to mid cd if ($widget.type == DOUBLE_XOVER) { $drawLineP(a, cen, $colorA, $widthA); //A to cen $drawLineP(b, cen, $colorB, $widthB); //B to cen $drawLineP(c, cen, $colorC, $widthC); //C to cen $drawLineP(d, cen, $colorD, $widthD); //D to cen } else if ($widget.type == RH_XOVER) { $drawLineP(ab, cen, $colorA, $widthA); //midAB to cen $drawLineP(cen, cd, $colorC, $widthC); //cen to midDC } else { //LH_XOVER $drawLineP(ab, cen, $colorB, $widthB); //midAB to cen $drawLineP(cen, cd, $colorD, $widthD); //cen to midDC } } } // erase and draw turnout circles if enabled, including occupancy check if (($gPanel.turnoutcircles == "yes") && ($gPanel.controlling == "yes") && ($widget.disabled !== "yes")) { $drawCircle($widget.xcen, $widget.ycen, $gPanel.turnoutcirclesize * SIZE, $eraseColor, 1); if (($widget.disableWhenOccupied !== "yes") || ($widget.occupancystate != ACTIVE)) { var $color = $gPanel.turnoutcirclecolor; if ($widget.state != CLOSED) { $color = $gPanel.turnoutcirclethrowncolor; } if ($gPanel.turnoutfillcontrolcircles == "yes") { $fillCircle($widget.xcen, $widget.ycen, $gPanel.turnoutcirclesize * SIZE, $color, 1); } else { $drawCircle($widget.xcen, $widget.ycen, $gPanel.turnoutcirclesize * SIZE, $color, 1); } } // if disableWhenOccupied requested, disable click if enabled and active if ($widget.disableWhenOccupied == "yes") { if ($widget.occupancystate == ACTIVE) { $('#'+$widget.id).removeClass("clickable"); $('#'+$widget.id).unbind(UPEVENT, $handleClick); } else { $('#'+$widget.id).addClass("clickable"); $('#'+$widget.id).bind(UPEVENT, $handleClick); } } } } // function $drawTurnout($widget) // compute width of turnout leg based on connected segment, then block type function $getLegWidth(cs, bn) { var width = $gPanel.sidelinetrackwidth; if (isDefined(cs)) { if (cs.mainline == "yes") { width = $gPanel.mainlinetrackwidth; } var blk = $gBlks[bn]; if (isDefined(blk)) { if (cs.mainline=="yes") { width = $gPanel.mainlineblockwidth; } else { width = $gPanel.sidelineblockwidth; } } } return width*1.0; //insure numeric } // compute color of turnout leg based on connected segment, then its block color function $getLegColor(cs, bn) { var color = $gPanel.defaulttrackcolor; if (isDefined(cs)) { if (isDefined($gPanel.mainRailColor) && (cs.mainline == "yes")) { color = $gPanel.mainRailColor; } else { if (isDefined($gPanel.sideRailColor)) { color = $gPanel.sideRailColor; } } var blk = $gBlks[bn]; if (isDefined(blk)) { color = blk.blockcolor; } } return color; } // function $getLegColor() //set trackcolor by default, then main/side var $getTrackColor = function(e) { var color = $gPanel.defaulttrackcolor; if (isDefined($gPanel.mainRailColor) && (e.mainline == "yes")) { color = $gPanel.mainRailColor; } if (isDefined($gPanel.sideRailColor) && (e.mainline != "yes")) { color = $gPanel.sideRailColor; } return color; } //draw a Slip (pass in widget) // see LayoutSlip.draw() function $drawSlip($widget) { //if set to hidden, don't draw anything if ($widget.hidden == "yes") { return; } if (jmri_logging) { log.log("$drawSlip(" + $widget.id + "): state: " + $widget.state); } var $mainWidth = $gPanel.mainlinetrackwidth; var $sideWidth = $gPanel.sidelinetrackwidth; var $widthA = $sideWidth; if (isDefined($gWidgets[$widget.connectaname])) { if ($gWidgets[$widget.connectaname].mainline == "yes") { $widthA = $mainWidth; } } var $widthB = $sideWidth; if (isDefined($gWidgets[$widget.connectbname])) { if ($gWidgets[$widget.connectbname].mainline == "yes") { $widthB = $mainWidth; } } var $widthC = $sideWidth; if (isDefined($gWidgets[$widget.connectcname])) { if ($gWidgets[$widget.connectcname].mainline == "yes") { $widthC = $mainWidth; } } var $widthD = $sideWidth; if (isDefined($gWidgets[$widget.connectdname])) { if ($gWidgets[$widget.connectdname].mainline == "yes") { $widthD = $mainWidth; } } var cen = [$widget.xcen * 1, $widget.ycen * 1] var a = $getPoint($widget.ident + SLIP_A); var b = $getPoint($widget.ident + SLIP_B); var c = $getPoint($widget.ident + SLIP_C); var d = $getPoint($widget.ident + SLIP_D); var $eraseColor = $gPanel.backgroundcolor; var $trackColor = $getTrackColor($widget); var $blkA = $gBlks[$widget.blockname]; var $colorA = isDefined($blkA) ? $blkA.blockcolor : $trackColor; var $colorAt = isDefined($blkA) ? $blkA.trackcolor : $trackColor; var $blkB = $gBlks[$widget.blockbname]; var $colorB = isDefined($blkB) ? $blkB.blockcolor : $colorA; var $colorBt = isDefined($blkB) ? $blkB.trackcolor : $colorAt; var $blkC = $gBlks[$widget.blockcname]; var $colorC = isDefined($blkC) ? $blkC.blockcolor : $colorA; var $colorCt = isDefined($blkC) ? $blkC.trackcolor : $colorAt; var $blkD = $gBlks[$widget.blockdname]; var $colorD = isDefined($blkD) ? $blkD.blockcolor : $colorA; var $colorDt = isDefined($blkD) ? $blkD.trackcolor : $colorAt; //slip A==-==D // \\ // // X // // \\ // B==-==C // var STATE_AC = 0x02; // var STATE_BD = 0x04; // var STATE_AD = 0x06; // var STATE_BC = 0x08; // ERASE EVERYTHING FIRST var acen3rd = $point_third(a, cen); var bcen3rd = $point_third(b, cen); var ccen3rd = $point_third(c, cen); var dcen3rd = $point_third(d, cen); var ad3rd = $point_midpoint(acen3rd, dcen3rd); var bc3rd = $point_midpoint(bcen3rd, ccen3rd); if ($widget.state != STATE_AC) { $drawLineP(a, acen3rd, $eraseColor, $mainWidth); $drawLineP(acen3rd, ccen3rd, $eraseColor, $mainWidth); //erase AC $drawLineP(ccen3rd, c, $eraseColor, $mainWidth); } if ($widget.state != STATE_BD) { $drawLineP(b, bcen3rd, $eraseColor, $mainWidth); $drawLineP(bcen3rd, dcen3rd, $eraseColor, $mainWidth); //erase BD $drawLineP(dcen3rd, d, $eraseColor, $mainWidth); } if ($widget.state != STATE_AD) { $drawLineP(a, acen3rd, $eraseColor, $mainWidth); $drawLineP(acen3rd, dcen3rd, $eraseColor, $mainWidth); //erase AD $drawLineP(dcen3rd, d, $eraseColor, $mainWidth); } if ($widget.slipType == DOUBLE_SLIP) { if ($widget.state != STATE_BC) { $drawLineP(b, bcen3rd, $eraseColor, $mainWidth); $drawLineP(bcen3rd, ccen3rd, $eraseColor, $mainWidth); //erase BC $drawLineP(ccen3rd, c, $eraseColor, $mainWidth); } } // THEN DRAW ROUTE var forceUnselected = false; if ($widget.state == STATE_AD) { // draw A<===>D $drawLineP(a, acen3rd, $colorA, $widthA); $drawLineP(acen3rd, ad3rd, $colorA, $widthA); $drawLineP(d, dcen3rd, $colorD, $widthD); $drawLineP(dcen3rd, ad3rd, $colorD, $widthD); } else if ($widget.state == STATE_AC) { // draw A<===>C $drawLineP(a, acen3rd, $colorA, $widthA); $drawLineP(acen3rd, cen, $colorA, $widthA); $drawLineP(c, ccen3rd, $colorC, $widthC); $drawLineP(ccen3rd, cen, $colorC, $widthC); } else if ($widget.state == STATE_BD) { // draw B<===>D $drawLineP(b, bcen3rd, $colorB, $widthB); $drawLineP(bcen3rd, cen, $colorB, $widthB); $drawLineP(d, dcen3rd, $colorD, $widthD); $drawLineP(dcen3rd, cen, $colorD, $widthD); } else if ($widget.state == STATE_BC) { if ($widget.slipType == DOUBLE_SLIP) { // draw B<===>C $drawLineP(b, bcen3rd, $colorB, $widthB); $drawLineP(bcen3rd, bc3rd, $colorB, $widthB); $drawLineP(c, ccen3rd, $colorC, $widthC); $drawLineP(ccen3rd, bc3rd, $colorC, $widthC); } // DOUBLE_SLIP } else { forceUnselected = true; // if not valid state force drawing unselected } if (forceUnselected || ($gPanel.turnoutdrawunselectedleg == 'yes')) { if ($widget.state == STATE_AC) { $drawLineP(b, bcen3rd, $colorBt, $widthB); $drawLineP(d, dcen3rd, $colorDt, $widthD); } else if ($widget.state == STATE_BD) { $drawLineP(a, acen3rd, $colorAt, $widthA); $drawLineP(c, ccen3rd, $colorCt, $widthC); } else if ($widget.state == STATE_AD) { $drawLineP(b, bcen3rd, $colorBt, $widthB); $drawLineP(c, ccen3rd, $colorCt, $widthC); } else if ($widget.state == STATE_BC) { $drawLineP(a, acen3rd, $colorAt, $widthA); $drawLineP(d, dcen3rd, $colorDt, $widthD); } else { $drawLineP(a, acen3rd, $colorAt, $widthA); $drawLineP(b, bcen3rd, $colorBt, $widthB); $drawLineP(c, ccen3rd, $colorCt, $widthC); $drawLineP(d, dcen3rd, $colorDt, $widthD); } } if (($gPanel.turnoutcircles == "yes") && ($gPanel.controlling == "yes") && ($widget.disabled !== "yes")) { //draw the two control circles var $cr = $gPanel.turnoutcirclesize * SIZE; //turnout circle radius // center var cen = [$widget.xcen, $widget.ycen]; // left center var lcen = $point_midpoint(a, b); var ldelta = $point_subtract(cen, lcen); // left fraction var lf = $cr / Math.hypot(ldelta[0], ldelta[1]); // left circle var lcc = $point_lerp(cen, lcen, lf); $drawCircleP(lcc, $cr, $gPanel.turnoutcirclecolor, 1); // right center var rcen = $point_midpoint(c, d); var rdelta = $point_subtract(cen, rcen); // right fraction var rf = $cr / Math.hypot(rdelta[0], rdelta[1]); // right circle var rcc = $point_lerp(cen, rcen, rf); $drawCircleP(rcc, $cr, $gPanel.turnoutcirclecolor, 1); } } // function $drawSlip($widget) function $drawPositionableRoundRect($widget) { //log.log("drawing PositionableRoundRect") createPanelCanvas(); //insure canvas layer is available for drawing $gCtx.save(); // save current line width and color if (isDefined($widget.lineColor)) { $gCtx.strokeStyle = $widget.lineColor; } if (isDefined($widget.fillColor)) { $gCtx.fillStyle = $widget.fillColor; } if (isDefined($widget.lineWidth)) { $gCtx.lineWidth = $widget.lineWidth; } $gCtx.beginPath(); // $gCtx.rotate($toRadians($widget.degrees)); $gCtx.roundRect($widget.x, $widget.y, $widget.width, $widget.height, $widget.cornerRadius); $gCtx.stroke() $gCtx.fill() $gCtx.restore(); // restore color and width back to default } // function $drawPositionableRoundRect($widget) function $drawPositionableEllipse($widget) { //log.log("drawing PositionableEllipse"); createPanelCanvas(); //insure canvas layer is available for drawing $gCtx.save(); // save current line width and color if (isDefined($widget.lineColor)) { $gCtx.strokeStyle = $widget.lineColor; } if (isDefined($widget.fillColor)) { $gCtx.fillStyle = $widget.fillColor; } if (isDefined($widget.lineWidth)) { $gCtx.lineWidth = $widget.lineWidth; } rw = $widget.width/2; rh = $widget.height/2; x = $widget.x * 1.0; y = $widget.y * 1.0; $gCtx.beginPath(); $gCtx.ellipse(x + rw, y + rh, rw, rh, $toRadians($widget.degrees), 0, 2 * Math.PI); $gCtx.stroke() $gCtx.fill() $gCtx.restore(); // restore color and width back to default } // function $drawPositionableEllipse($widget) function $drawLayoutShape($widget) { var $pts = $widget.points; // get the points var len = $pts.length; if (len > 0) { $gCtx.save(); // save current line width and color if (isDefined($widget.lineColor)) { $gCtx.strokeStyle = $widget.lineColor; } if (isDefined($widget.fillColor)) { $gCtx.fillStyle = $widget.fillColor; } if (isDefined($widget.linewidth)) { $gCtx.lineWidth = $widget.linewidth; //TODO: check case on this } $gCtx.beginPath(); var shapeType = $widget.type; $pts.each(function(idx, $lsp) { //loop thru points // this point var p = $getLayoutPoint($lsp); // left point var idxL = $wrapValue(idx - 1, 0, len); var $lspL = $pts[idxL]; var pL = $getLayoutPoint($lspL); var midL = $point_midpoint(pL, p); // right point var idxR = $wrapValue(idx + 1, 0, len); var $lspR = $pts[idxR]; var pR = $getLayoutPoint($lspR); var midR = $point_midpoint(p, pR); var lspt = $lsp.attributes.type.value; // Straight or Curve // if this is an open shape... if (shapeType == "eOpen") { // and this is first or last point... if ((idx == 0) || (idxR == 0)) { // then force straight shape point type lspt = "Straight"; } } if (lspt == "Straight") { if (idx == 0) { // if this is the first point... // ...and our shape is open... if (shapeType == "Open") { $gCtx.moveTo(p[0], p[1]); // then start here } else { // otherwise $gCtx.moveTo(midL[0], midL[1]); //start here $gCtx.lineTo(p[0], p[1]); //draw to here } } else { $gCtx.lineTo(midL[0], midL[1]); //start here $gCtx.lineTo(p[0], p[1]); //draw to here } // if this is not the last point... // ...or our shape isn't open if ((idxR != 0) || (shapeType == "Open")) { $gCtx.lineTo(midR[0], midR[1]); // draw to here } } else if (lspt == "Curve") { if (idx == 0) { // if this is the first point $gCtx.moveTo(midL[0], midL[1]); // then start here } $gCtx.quadraticCurveTo(p[0], p[1], midR[0], midR[1]); } else { log.error("ERROR: unexpected LayoutShape point type '" + lspt + "' for " + $widget.ide); } }); // $pts.each(function(idx, $lsp) if (shapeType == "Filled") { $gCtx.fill(); } $gCtx.stroke(); $gCtx.restore(); // restore color and width back to default } // if (len > 0) } function $getLayoutPoint($p) { return [Number($p.attributes.x.value), Number($p.attributes.y.value)]; } // wrap inValue around between minVal and maxVal function $wrapValue(inValue, minVal, maxVal) { var range = maxVal - minVal; return ((inValue % range) + range) % range; } function $lerp(value1, value2, amount) { return ((1 - amount) * value1) + (amount * value2); } function $half(value1, value2) { return $lerp(value1, value2, 1 / 2); } function $third(value1, value2) { return $lerp(value1, value2, 1 / 3); } function $store_occupancysensor(id, sensor) { if (id && sensor) { if (!(sensor in occupancyNames)) { occupancyNames[sensor] = new Array(); } occupancyNames[sensor][occupancyNames[sensor].length] = id; //console.log("sensor " + sensor + " stored with widget " + id); } } function $store_occupancyblock(id, oblock) { if (id && oblock) { if (!(oblock in $oblockNames)) { $oblockNames[oblock] = new Array(); } $oblockNames[oblock][$oblockNames[oblock].length] = id; // id = widgetId //console.log("oblock " + oblock + " stored with widget " + id); } } //store the various points defined with a Turnout (pass in widget) //see jmri.jmrit.display.layoutEditor.LayoutTurnout.java for background function $storeTurnoutPoints($widget) { var $t = []; $t['ident'] = $widget.ident + ".TURNOUT_B"; //store B endpoint $t['x'] = $widget.xb * 1; $t['y'] = $widget.yb * 1; $gPts[$t.ident] = $t; $t = []; $t['ident'] = $widget.ident + ".TURNOUT_C"; //store C endpoint $t['x'] = $widget.xc * 1; $t['y'] = $widget.yc * 1; $gPts[$t.ident] = $t; if ($widget.type == LH_TURNOUT || $widget.type == RH_TURNOUT) { $t = []; $t['ident'] = $widget.ident + ".TURNOUT_A"; //calculate and store A endpoint (mirror of B for these) $t['x'] = $widget.xcen - ($widget.xb - $widget.xcen); $t['y'] = $widget.ycen - ($widget.yb - $widget.ycen); $gPts[$t.ident] = $t; } else if ($widget.type == WYE_TURNOUT) { $t = []; $t['ident'] = $widget.ident + ".TURNOUT_A"; //store A endpoint $t['x'] = $widget.xa * 1; $t['y'] = $widget.ya * 1; $gPts[$t.ident] = $t; } else if ($widget.type == LH_XOVER || $widget.type == RH_XOVER || $widget.type == DOUBLE_XOVER) { $t = []; $t['ident'] = $widget.ident + ".TURNOUT_A"; //calculate and store A endpoint (mirror of C for these) $t['x'] = $widget.xcen - ($widget.xc - $widget.xcen); $t['y'] = $widget.ycen - ($widget.yc - $widget.ycen); $gPts[$t.ident] = $t; $t = []; $t['ident'] = $widget.ident + ".TURNOUT_D"; //calculate and store D endpoint (mirror of B for these) $t['x'] = $widget.xcen - ($widget.xb - $widget.xcen); $t['y'] = $widget.ycen - ($widget.yb - $widget.ycen); $gPts[$t.ident] = $t; } } //store the various points defined with a Slip (pass in widget) //see jmri.jmrit.display.layoutEditor.LayoutSlip.java for background function $storeSlipPoints($widget) { var $t = []; $t['ident'] = $widget.ident + SLIP_A; //store A endpoint $t['x'] = $widget.xa * 1; $t['y'] = $widget.ya * 1; $gPts[$t.ident] = $t; $t = []; $t['ident'] = $widget.ident + SLIP_B; //store B endpoint $t['x'] = $widget.xb * 1; $t['y'] = $widget.yb * 1; $gPts[$t.ident] = $t; $t = []; $t['ident'] = $widget.ident + SLIP_C; //calculate and store C endpoint (mirror of A for these) $t['x'] = $widget.xcen - ($widget.xa - $widget.xcen); $t['y'] = $widget.ycen - ($widget.ya - $widget.ycen); $gPts[$t.ident] = $t; $t = []; $t['ident'] = $widget.ident + SLIP_D; //calculate and store D endpoint (mirror of B for these) $t['x'] = $widget.xcen - ($widget.xb - $widget.xcen); $t['y'] = $widget.ycen - ($widget.yb - $widget.ycen); $gPts[$t.ident] = $t; } //store the various points defined with a LevelXing (pass in widget) //see jmri.jmrit.display.layoutEditor.LevelXing.java for background function $storeLevelXingPoints($widget) { var $t = []; $t['ident'] = $widget.ident + LEVEL_XING_A; //store A endpoint $t['x'] = $widget.xa * 1; $t['y'] = $widget.ya * 1; $gPts[$t.ident] = $t; $t = []; $t['ident'] = $widget.ident + LEVEL_XING_B; //store B endpoint $t['x'] = $widget.xb * 1; $t['y'] = $widget.yb * 1; $gPts[$t.ident] = $t; $t = []; $t['ident'] = $widget.ident + LEVEL_XING_C; //calculate and store A endpoint (mirror of A for these) $t['x'] = $widget.xcen - ($widget.xa - $widget.xcen); $t['y'] = $widget.ycen - ($widget.ya - $widget.ycen); $gPts[$t.ident] = $t; $t = []; $t['ident'] = $widget.ident + LEVEL_XING_D; //calculate and store D endpoint (mirror of B for these) $t['x'] = $widget.xcen - ($widget.xb - $widget.xcen); $t['y'] = $widget.ycen - ($widget.yb - $widget.ycen); $gPts[$t.ident] = $t; } //drawLine, passing in values from xml function $drawLine($p1x, $p1y, $p2x, $p2y, $color, $width, dashArray) { $gCtx.save(); // save current line width and color if (isDefined($color)) { $gCtx.strokeStyle = $color; } if (isDefined($width)) { $gCtx.lineWidth = $width; } $gCtx.beginPath(); if (isDefined(dashArray)) { $gCtx.setLineDash(dashArray); } $gCtx.moveTo($p1x, $p1y); $gCtx.lineTo($p2x, $p2y); $gCtx.stroke(); if (isDefined(dashArray)) { $gCtx.setLineDash([]); } $gCtx.restore(); // restore color and width back to default } function $drawLineP($p1, $p2, $color, $width) { $drawLine($p1[0], $p1[1], $p2[0], $p2[1], $color, $width); } //drawLine, passing in values from xml function $drawDashedLine($p1x, $p1y, $p2x, $p2y, $color, $width, dashArray) { $drawLine($p1x, $p1y, $p2x, $p2y, $color, $width, dashArray); } // function $drawDashedLineP($p1, $p2, $color, $width, dashArray) { // $drawDashedLine($p1[0], $p1[1], $p2[0], $p2[1], $color, $width, dashArray); // } //draw a Circle (color and width are optional) function $drawCircleP($p, $radius, $color, $width) { $drawCircle($p[0], $p[1], $radius, $color, $width); } function $drawCircle($px, $py, $radius, $color, $width) { $gCtx.save(); // save current line width and color // set color and width if (isDefined($color)) { $gCtx.strokeStyle = $color; } if (isDefined($width)) { $gCtx.lineWidth = $width; } $gCtx.beginPath(); $gCtx.arc($px, $py, $radius, 0, 2 * Math.PI, false); $gCtx.stroke(); $gCtx.restore(); // restore color and width back to default } //draw a Circle (color and width are optional) function $fillCircleP($p, $radius, $color, $width) { $fillCircle($p[0], $p[1], $radius, $color, $width); } function $fillCircle($px, $py, $radius, $color, $width) { $gCtx.save(); // save current line width and color // set color and width if (isDefined($color)) { $gCtx.fillStyle = $color; } if (isDefined($width)) { $gCtx.lineWidth = $width; } $gCtx.beginPath(); $gCtx.arc($px, $py, $radius, 0, 2 * Math.PI, false); $gCtx.fill(); $gCtx.restore(); // restore color and width back to default } //drawArc, passing in values from xml function $drawArc(pt1x, pt1y, pt2x, pt2y, degrees, $color, $width) { // Compute arc's chord var a = pt2x - pt1x; var o = pt2y - pt1y; var chord = Math.hypot(a, o); //in pixels if (chord > 0) { //don't bother if no length $gCtx.save(); // save current line width and color // set color and width if (isDefined($color)) { $gCtx.strokeStyle = $color; } if (isDefined($width)) { $gCtx.lineWidth = $width; } var halfAngleRAD = $toRadians(degrees / 2); var radius = (chord / 2) / (Math.sin(halfAngleRAD)); //in pixels var startRAD = Math.atan2(a, o) - halfAngleRAD; //in radians // calculate center of circle var cx = (pt2x * 1.0) - Math.cos(startRAD) * radius; var cy = (pt2y * 1.0) + Math.sin(startRAD) * radius; //calculate start and end angle var startAngleRAD = Math.atan2(pt1y - cy, pt1x - cx); //in radians var endAngleRAD = Math.atan2(pt2y - cy, pt2x - cx); //in radians $gCtx.beginPath(); $gCtx.arc(cx, cy, radius, startAngleRAD, endAngleRAD, false); $gCtx.stroke(); $gCtx.restore(); // restore color and width back to default } } function $drawArcP(pt1, pt2, degrees) { $drawArc(pt1[0], pt1[1], pt2[0], pt2[1], degrees); } function $drawEllipse(x, y, rw, rh, startAngleRAD, stopAngleRAD) { $gCtx.beginPath(); $gCtx.ellipse(x, y, rw, rh, 0, startAngleRAD, stopAngleRAD); $gCtx.stroke(); } // $drawBezier var bezier1st = true; function $drawBezier(points, $color, $width, displacement) { $gCtx.save(); // save current line width and color $gCtx.strokeStyle = $color; $gCtx.lineWidth = $width; try { bezier1st = true; $gCtx.beginPath(); $plotBezier(points, 0, displacement); $gCtx.stroke(); } catch (e) { if (jmri_logging) { log.log("$plotBezier exception: " + e); var vDebug = ""; for (var prop in e) { vDebug += " ["+ prop+ "]: '"+ e[prop]+ "'\n"; } vDebug += "toString(): " + " value: [" + e.toString() + "]"; log.log(vDebug); } } $gCtx.restore(); // restore color and width back to default } // //plotBezier - recursive function to draw bezier curve // function $plotBezier(points, depth, displacement) { var len = points.length, idx, jdx; // calculate flatness to determine if we need to recurse... var outer_distance = 0; for (var idx = 1; idx < len; idx++) { outer_distance += $point_distance(points[idx - 1], points[idx]); } var inner_distance = $point_distance(points[0], points[len - 1]); var flatness = outer_distance / inner_distance; // depth prevents stack overflow // (I picked 12 because 2^12 = 2048 is larger than most monitors ;-) // the flatness comparison value is somewhat arbitrary. // (I just kept moving it closer to 1 until I got good results. ;-) if ((depth > 12) || (flatness <= 1.001)) { var p0 = points[0], pN = points[len - 1]; var vO = $point_normalizeTo($point_orthogonal($point_subtract(pN, p0)), displacement); //$point_log("vO", vO); if (bezier1st) { var p0P = $point_add(p0, vO); //$point_log("p0P", p0P); $gCtx.moveTo(p0P[0], p0P[1]); bezier1st = false; } var pNP = $point_add(pN, vO); $gCtx.lineTo(pNP[0], pNP[1]); } else { // calculate (len - 1) order of points // (zero'th order are the input points) var orderPoints = []; for (idx = 0; idx < len - 1; idx++) { var nthOrderPoints = []; for (jdx = 0; jdx < len - 1 - idx; jdx++) { if (idx == 0) { nthOrderPoints.push($point_midpoint(points[jdx], points[jdx + 1])); } else { nthOrderPoints.push($point_midpoint(orderPoints[idx - 1][jdx], orderPoints[idx - 1][jdx + 1])); } } orderPoints.push(nthOrderPoints); } // collect left points var leftPoints = []; leftPoints.push(points[0]); for (idx = 0; idx < len - 1; idx++) { leftPoints.push(orderPoints[idx][0]); } // draw left side Bezier $plotBezier(leftPoints, depth + 1, displacement); // collect right points var rightPoints = []; for (idx = 0; idx < len - 1; idx++) { rightPoints.push(orderPoints[len - 2 - idx][idx]); } rightPoints.push(points[len - 1]); // draw right side Bezier $plotBezier(rightPoints, depth + 1, displacement); } } function $point_log(prefix, p) { log.log(prefix + ": {" + p[0] + ", " + p[1] + "}"); } function $getPoint(name) { var point$ = $gPts[name]; return [Number(point$.x), Number(point$.y)]; } function $point_length(p) { var dx = p[0]; var dy = p[1]; return Math.hypot(dx, dy); } function $point_add(p1, p2) { return [p1[0] + p2[0], p1[1] + p2[1]]; } function $point_subtract(p1, p2) { return [p1[0] - p2[0], p1[1] - p2[1]]; } function $point_distance(p1, p2) { var delta = $point_subtract(p1, p2); return Math.hypot(delta[0], delta[1]); } function $point_midpoint(p1, p2) { return [$half(p1[0], p2[0]), $half(p1[1], p2[1])]; } function $point_normalizeTo(p, new_length) { var m = new_length / $point_length(p); return [p[0] * m, p[1] * m]; } function $point_orthogonal(p) { return [-p[1],p[0]]; } function $computeAngleRAD(v) { return Math.atan2(v[0], v[1]); } function $computeAngleRAD2(p1, p2) { return $computeAngleRAD($point_subtract(p1, p2)); } // Converts from degrees to radians. function $toRadians(degrees) { return degrees * Math.PI / 180; }; // Converts from radians to degrees. function $toDegrees(radians) { return radians * 180 / Math.PI; }; // rotate a point vector function $point_rotate(point, angleRAD) { var sinA = Math.sin(angleRAD), cosA = Math.cos(angleRAD); var x = point[0], y = point[1]; return [cosA * x - sinA * y, sinA * x + cosA * y]; } function $point_lerp(p1, p2, amount) { return [$lerp(p1[0], p2[0], amount), $lerp(p1[1], p2[1], amount)] } function $point_third(p1, p2) { return $point_lerp(p1, p2, 1.0/3.0); } //set object attributes from xml attributes, returning object var $getObjFromXML = function(e) { var $widget = {}; $(e.attributes).each(function() { $widget[this.name] = this.value; }); return $widget; }; //redraw all "drawn" elements for given block (called after color change) function $redrawBlock(blockName) { //log.log("redrawing all tracks for block " + blockName); //loop thru widgets, if block matches, redraw widget by proper method jQuery.each($gWidgets, function($id, $widget) { $logProperties($widget); if (($widget.blockname == blockName) || ($widget.blocknameac == blockName) || ($widget.blocknamebd == blockName) || ($widget.blockbname == blockName) || ($widget.blockcname == blockName) || ($widget.blockdname == blockName)) { switch ($widget.widgetType) { case 'layoutturnout' : $drawTurnout($widget); break; case 'layoutSlip' : $drawSlip($widget); break; case 'tracksegment' : $drawTrackSegment($widget); break; case 'levelxing' : $drawLevelXing($widget); break; } } if ($widget.widgetType == 'layoutSlip') { if ((isDefined($widget.connectaname) && ($gWidgets[$widget.connectaname].blockname == blockName)) || isDefined($widget.connectbname) && ($gWidgets[$widget.connectbname].blockname == blockName) || isDefined($widget.connectcname) && ($gWidgets[$widget.connectcname].blockname == blockName) || isDefined($widget.connectdname) && ($gWidgets[$widget.connectdname].blockname == blockName)){ $drawSlip($widget); } } }); }; //redraw all "drawn" elements to overcome some bidirectional dependencies in the xml var $drawAllDrawnWidgets = function() { //loop thru widgets, redrawing each visible widget by proper method jQuery.each($gWidgets, function($id, $widget) { switch ($widget.widgetType) { case 'layoutturnout' : $drawTurnout($widget); break; case 'layoutSlip' : $drawSlip($widget); break; case 'tracksegment' : $drawTrackSegment($widget); break; case 'levelxing' : $drawLevelXing($widget); break; } }); }; // redraw all "icon" Control Panel elements. Called after a delay to allow loading of images. var $drawAllIconWidgets = function() { //loop thru widgets, repositioning each icon widget jQuery.each($gWidgets, function($id, $widget) { switch ($widget.widgetFamily) { case 'icon' : $setWidgetPosition($("#panel-area > #" + $widget.id)); break; } }); }; // draw all beanswitch icons first time var $drawAllSwitchIcons = function() { jQuery.each($gWidgets, function($id, $widget) { switch ($widget.widgetFamily) { case 'switch' : if (isDefined($widget['shape']) && ($widget.shape != "button")) { $drawWidgetSymbol($id, UNKNOWN); // draw first time UNKNOWN = 0 } break; } }); }; function createPanelCanvas() { if ($gCtx == undefined) { //create canvas if not already created $("#panel-area").prepend(""); var canvas = document.getElementById("panelCanvas"); $gCtx = canvas.getContext("2d"); $gCtx.strokeStyle = $gPanel.defaulttrackcolor; $gCtx.lineWidth = $gPanel.sidelinetrackwidth; //set background color from panel attribute (single hex value) $("#panel-area").css({'background-color': $gPanel.backgroundcolor}); } }; function updateWidgets(name, state, data) { // update all widgets based on the element that changed, using systemname if (whereUsed[name]) { //log.log("updateWidgets(" + name + ", " + state + ")"); $.each(whereUsed[name], function(index, widgetId) { $setWidgetState(widgetId, state, data); }); } //update all widgets based on the element that changed, using username if (isDefined(data.userName) && whereUsed[data.userName]) { //log.log("updateWidgets by username (" + data.userName + "), " + state); $.each(whereUsed[data.userName], function(index, widgetId) { $setWidgetState(widgetId, state, data); }); } } function updateOccupancy(sensorName, state, data) { // handle occupancy sensors by systemname if (occupancyNames[sensorName]) { updateOccupancySub(sensorName, state); } // handle occupancy sensors by username if (occupancyNames[data.userName]) { updateOccupancySub(data.userName, state); } } function updateOccupancySub(sensorName, state) { if (occupancyNames[sensorName]) { $.each(occupancyNames[sensorName], function(index, widgetId) { $widget = $gWidgets[widgetId]; updateBlockSensorState($widget.blockname, sensorName, state); updateBlockSensorState($widget.blocknameac, sensorName, state); updateBlockSensorState($widget.blocknamebd, sensorName, state); updateBlockSensorState($widget.blockbname, sensorName, state); updateBlockSensorState($widget.blockcname, sensorName, state); updateBlockSensorState($widget.blockdname, sensorName, state); $widget.occupancystate = state; // set occupancy for the widget to the newstate switch ($widget.widgetType) { case 'layoutturnout' : $drawTurnout($widget); break; case 'layoutSlip' : $drawSlip($widget); break; case 'indicatortrackicon' : case 'indicatorturnouticon' : $reDrawIcon($widget) //console.log("IT(O)I sensor change"); break; default : break; } }); } } function updateBlockSensorState(blockName, sensorName, sensorState) { if (isDefined(blockName)) { var $blk = $gBlks[blockName]; if (isDefined($blk)) { if (isDefined($blk.occupancysensor) && ($blk.occupancysensor == sensorName)) { $blk.state = sensorState; } } } } function setBlockColor(blockName, newColor) { //log.log("setBlockColor(" + blockName + ", " + newColor + ");"); var $blk = $gBlks[blockName]; if (isDefined($blk)) { $gBlks[blockName].blockcolor = newColor; } else { log.error("ERROR: block " + blockName + " not found for color " + newColor); } $redrawBlock(blockName); } function updateOblocks(oblockName, status) { // based on updateOccupancy() // all oblocks are handled by their systemname if ($oblockNames[oblockName]) { $.each($oblockNames[oblockName], function(index, widgetId) { $widget = $gWidgets[widgetId]; switch ($widget.widgetType) { case 'indicatortrackicon' : case 'indicatorturnouticon' : // does not receive turnout state via oblock //console.log("updateOblocks UNFILTERED " + oblockName + " on widget " + $widget.id + " status=" + status); if (status < 0x16) { // ignore (un)occupied // pass on as is } else if ((status & TRACK_ERROR) == TRACK_ERROR) { // ErrorTrack, swallow DontUse, Allocated 0x80 status = (status & 0x86); } else if ((status & OUT_OF_SERVICE) == OUT_OF_SERVICE) { // DontUseTrack, swallow Allocated, ignore Occupied 0x40 status = (status & 0x40); } else if ((status & RUNNING) == RUNNING) { // Running = occupied by train (via oblock) 0x20 status = (status & 0x22); // keep Occupied bit } else if ((status & 0x12) == 0x2) { // Occupied, swallow Allocated status = (status & 0x2); } else if ((status & ALLOCATED) == ALLOCATED) { // Allocated 0x10 status = (status & 0x12); // only keep Occupied bit, it should overrule ALLOCATED } $widget.occupancystate = status; // set occupancy for the widget to the new occ.status //console.log("updateOblocks FILTERED FOR " + oblockName + " on widget " + $widget.id + " status=" + $widget.occupancystate); // enable/disable turnout click handling if (status == OUT_OF_SERVICE) { $('#'+$widget.id).removeClass("clickable"); $('#'+$widget.id).unbind(UPEVENT, $handleClick); } else { $('#'+$widget.id).addClass("clickable"); $('#'+$widget.id).bind(UPEVENT, $handleClick); } $reDrawIcon($widget); break; default: break; // shouldn't get here } }); } } // convert turnout state to string function turnoutStateToString(state) { result = "UKNOWN" switch (state) { case 2: result = "CLOSED"; break; case 4: result = "THROWN"; break; case 8: result = "INCONSISTENT"; break; } return result; } // convert slip state to string function slipStateToString(state) { result = "UNKNOWN"; switch (state) { case STATE_AC: result = "STATE_AC"; break; case STATE_AD: result = "STATE_AD"; break; case STATE_BC: result = "STATE_BC"; break; case STATE_BD: result = "STATE_BD"; break; } return result; } function getTurnoutStatesForSlipState(slipWidget, slipState) { var results = [UNKNOWN, UNKNOWN]; if (isDefined(slipWidget)) { if (slipWidget.widgetType == 'layoutSlip') { switch (slipState) { case STATE_AC: results = [slipWidget.turnoutA_AC, slipWidget.turnoutB_AC]; break; case STATE_AD: results = [slipWidget.turnoutA_AD, slipWidget.turnoutB_AD]; break; case STATE_BC: results = [slipWidget.turnoutA_BC, slipWidget.turnoutB_BC]; break; case STATE_BD: results = [slipWidget.turnoutA_BD, slipWidget.turnoutB_BD]; break; } } } return results; } function getTurnoutStatesForSlip(slipWidget) { return getTurnoutStatesForSlipState(slipWidget, slipWidget.state); } function getSlipStateForTurnoutStatesClosest(slipWidget, stateA, stateB, useClosest) { var result = UNKNOWN; if ((stateA == slipWidget.turnoutA_AC) && (stateB == slipWidget.turnoutB_AC)) { result = STATE_AC; } else if ((stateA == slipWidget.turnoutA_AD) && (stateB == slipWidget.turnoutB_AD)) { result = STATE_AD; } else if ((slipWidget.slipType == DOUBLE_SLIP) && (stateA == slipWidget.turnoutA_BC) && (stateB == slipWidget.turnoutB_BC)) { result = STATE_BC; } else if ((stateA == slipWidget.turnoutA_BD) && (stateB == slipWidget.turnoutB_BD)) { result = STATE_BD; } else if (useClosest) { if ((stateA == slipWidget.turnoutA_AC) || (stateB == slipWidget.turnoutB_AC)) { result = STATE_AC; } else if ((stateA == slipWidget.turnoutA_AD) || (stateB == slipWidget.turnoutB_AD)) { result = STATE_AD; } else if ((slipWidget.slipType == DOUBLE_SLIP) && (stateA == slipWidget.turnoutA_BC) || (stateB == slipWidget.turnoutB_BC)) { result = STATE_BC; } else if ((stateA == slipWidget.turnoutA_BD) || (stateB == slipWidget.turnoutB_BD)) { result = STATE_BD; } else { result = STATE_AD; } } return result; } function getSlipStateForTurnoutStates(slipWidget, stateA, stateB) { return getSlipStateForTurnoutStatesClosest(slipWidget, stateA, stateB, false) } //slip A==-==D // \\ // // X // // \\ // B==-==C // var STATE_AC = 0x02; // var STATE_BD = 0x04; // var STATE_AD = 0x06; // var STATE_BC = 0x08; // var CLOSED = '2'; // var THROWN = '4'; function getNextSlipState(slipWidget) { var result = UNKNOWN; // log.log("****************************"); // log.log("slipWidget.side:" + slipWidget.side); // log.log(" slipWidget.state:" + slipWidget.state); switch (slipWidget.side) { case 'left': { switch (slipWidget.state) { case STATE_AC: if (slipWidget.slipType == SINGLE_SLIP) { result = STATE_BD; } else { result = STATE_BC; } break; case STATE_AD: result = STATE_BD; break; case STATE_BC: default: result = STATE_AC; break; case STATE_BD: result = STATE_AD; break; } break; } case 'right': { switch (slipWidget.state) { case STATE_AC: result = STATE_AD; break; case STATE_AD: result = STATE_AC; break; case STATE_BC: default: result = STATE_BD; break; case STATE_BD: if (slipWidget.slipType == SINGLE_SLIP) { result = STATE_AC; } else { result = STATE_BC; } break; } break; } default: { log.log("getNextSlipState($widget): unknown $widget.side: " + slipWidget.side); break; } } return result; } // ======= End of Layout Editor functions ======= /****************************************************************** * ======= Layout Editor Decoration classes ======= */ class Decoration { constructor($widget) { //log.log("Decoration.constructor(...)"); $logProperties(this.$widget); this.$widget = $widget; } getEndPoints() { [this.ep1, this.ep2] = $getEndPoints(this.$widget); //log.log("ep1 = {" + this.ep1[0] + "," + this.ep1[1] + "}, ep2 = {" + this.ep2[0] + "," + this.ep2[1] + "}"); } getAngles() { var $widget = this.$widget; if ($widget.bezier == "yes") { this.getBezierAngles(); } else if ($widget.circle == "yes") { this.getCircleAngles(); } else if ($widget.arc == "yes") { this.getArcAngles(); } else { this.startAngleRAD = (Math.PI / 2) - $computeAngleRAD2(this.ep2, this.ep1); this.stopAngleRAD = this.startAngleRAD; } //log.log("startAngleDEG: " + $toDegrees(this.startAngleRAD) + ", stopAngleDEG: " + $toDegrees(this.stopAngleRAD) + "."); } getBezierAngles() { var $widget = this.$widget; var $cps = $widget.controlpoints; // get the control points var $cp0 = $cps[0]; var $cpN = $cps[$cps.length - 1]; var cp0 = $getLayoutPoint($cp0); var cpN = $getLayoutPoint($cpN); this.startAngleRAD = (Math.PI / 2) - $computeAngleRAD2(cp0, this.ep1); this.stopAngleRAD = (Math.PI / 2) - $computeAngleRAD2(this.ep2, cpN); } getCircleAngles() { var $widget = this.$widget; var extentAngleDEG = $widget.angle; if (extentAngleDEG == 0) { extentAngleDEG = 90; } var startAngleRAD, stopAngleRAD; // Convert angle to radiants in order to speed up math var halfAngleRAD = $toRadians(extentAngleDEG) / 2; // Compute arc's chord var a = this.ep2[0] - this.ep1[0]; var o = this.ep2[1] - this.ep1[1]; var chord = Math.hypot(a, o); // Make sure chord is not null // In such a case (ep1 == ep2), there is no arc to draw if (chord > 0) { var midAngleRAD = Math.atan2(a, o); startAngleRAD = (Math.PI / 2) - (midAngleRAD + halfAngleRAD); stopAngleRAD = (Math.PI / 2) - (midAngleRAD - halfAngleRAD); } this.startAngleRAD = startAngleRAD; this.stopAngleRAD = stopAngleRAD; } getArcAngles() { var startAngleRAD, stopAngleRAD; if (this.ep1[0] < this.ep2[0]) { if (this.ep1[1] < this.ep2[1]) { //log.log("#### QUAD ONE ####"); startAngleRAD = 0; stopAngleRAD = Math.PI / 2; } else { //log.log("#### QUAD TWO ####"); startAngleRAD = -Math.PI / 2; stopAngleRAD = 0; } } else { if (this.ep1[1] < this.ep2[1]) { //log.log("#### QUAD THREE ####"); startAngleRAD = Math.PI / 2; stopAngleRAD = Math.PI; } else { //log.log("#### QUAD FOUR ####"); startAngleRAD = Math.PI; stopAngleRAD = -Math.PI / 2; } } this.startAngleRAD = startAngleRAD; this.stopAngleRAD = stopAngleRAD; } draw() { this.getEndPoints(); this.getAngles(); } getArcParams(rw, rh, tp1, tp2) { var x, y; if (rw < 0) { rw = -rw; if (rh < 0) { //log.log("**** QUAD ONE ****"); x = tp1[0]; y = tp2[1]; rh = -rh; } else { //log.log("**** QUAD TWO ****"); x = tp2[0]; y = tp1[1]; } } else { if (rh < 0) { //log.log("**** QUAD THREE ****"); x = tp2[0]; y = tp1[1]; rh = -rh; } else { //log.log("**** QUAD FOUR ****"); x = tp1[0]; y = tp2[1]; } } return [x, y, rw, rh]; } } // class Decoration class ArrowDecoration extends Decoration { constructor($widget, $arrow) { super($widget); // this.style = Number($arrow.attr('style')); this.end = $arrow.attr('end'); this.direction = $arrow.attr('direction'); this.color = $arrow.attr('color'); this.linewidth = Number($arrow.attr('linewidth')); this.length = Number($arrow.attr('length')); this.gap = Number($arrow.attr('gap')); //log.log("arrow: {end:" + this.end + ", dir: " + this.direction + "}"); } draw() { super.draw(); $gCtx.save(); // save current line width and color // set color and width $gCtx.strokeStyle = this.color; $gCtx.fillStyle = this.color; $gCtx.lineWidth = this.linewidth; this.drawArrowStart(); this.drawArrowStop(); $gCtx.restore(); // restore color and width back to default } drawArrowStart() { var angleRAD = this.startAngleRAD; if (this.$widget.flip == "yes") { angleRAD = this.stopAngleRAD; } this.offset = 1; // draw the start arrows if ((this.end == "start") || (this.end == "both")) { if ((this.direction == "in") || (this.direction == "both")) { this.drawArrowIn(this.ep1, Math.PI + angleRAD); } if ((this.direction == "out") || (this.direction == "both")) { this.drawArrowOut(this.ep1, Math.PI + angleRAD); } } } drawArrowStop() { var angleRAD = this.stopAngleRAD; if (this.$widget.flip == "yes") { angleRAD = this.startAngleRAD; } this.offset = 1; // draw the stop arrows if ((this.end == "stop") || (this.end == "both")) { if ((this.direction == "in") || (this.direction == "both")) { this.drawArrowIn(this.ep2, angleRAD); } if ((this.direction == "out") || (this.direction == "both")) { this.drawArrowOut(this.ep2, angleRAD); } } } drawArrowIn(ep, angleRAD) { $gCtx.save(); $gCtx.translate(ep[0], ep[1]); $gCtx.rotate(angleRAD); switch (this.style) { default: this.style = 0; case 0: break; case 1: this.drawArrow1In(); break; case 2: this.drawArrow2In(); break; case 3: this.drawArrow3In(); break; case 4: this.drawArrow4In(); break; case 5: this.drawArrow5In(); } $gCtx.restore(); } // drawArrowIn drawArrowOut(ep, angleRAD) { $gCtx.save(); $gCtx.translate(ep[0], ep[1]); $gCtx.rotate(angleRAD); switch (this.style) { default: this.style = 0; case 0: break; case 1: this.drawArrow1Out(); break; case 2: this.drawArrow2Out(); break; case 3: this.drawArrow3Out(); break; case 4: this.drawArrow4Out(); break; case 5: this.drawArrow5Out(); } $gCtx.restore(); } // drawArrowIn drawArrow1In() { var p1 = [this.offset + this.length, -this.length]; var p2 = [this.offset, 0]; var p3 = [this.offset + this.length, +this.length]; $drawLineP(p1, p2); $drawLineP(p2, p3); this.offset += this.length + this.gap; } drawArrow1Out() { var p1 = [this.offset, -this.length]; var p2 = [this.offset + this.length, 0]; var p3 = [this.offset, +this.length]; $drawLineP(p1, p2); $drawLineP(p2, p3); this.offset += this.length + this.gap; } drawArrow2In() { var p1 = [this.offset + this.length, -this.length]; var p2 = [this.offset, 0]; var p3 = [this.offset + this.length, +this.length]; var p4 = [this.offset + this.linewidth + this.gap + this.length, -this.length]; var p5 = [this.offset + this.linewidth + this.gap, 0]; var p6 = [this.offset + this.linewidth + this.gap + this.length, +this.length]; $drawLineP(p1, p2); $drawLineP(p2, p3); $drawLineP(p4, p5); $drawLineP(p5, p6); this.offset += this.length + (2 * (this.linewidth + this.gap)); } drawArrow2Out() { var p1 = [this.offset, -this.length]; var p2 = [this.offset + this.length, 0]; var p3 = [this.offset, +this.length]; var p4 = [this.offset + this.linewidth + this.gap, -this.length]; var p5 = [this.offset + this.linewidth + this.gap + this.length, 0]; var p6 = [this.offset + this.linewidth + this.gap, +this.length]; $drawLineP(p1, p2); $drawLineP(p2, p3); $drawLineP(p4, p5); $drawLineP(p5, p6); this.offset += this.length + (2 * (this.linewidth + this.gap)); } drawArrow3In() { var p1 = [this.offset + this.length, -this.length]; var p2 = [this.offset, 0]; var p3 = [this.offset + this.length, +this.length]; $gCtx.beginPath(); $gCtx.moveTo(p1[0], p1[1]); $gCtx.lineTo(p2[0], p2[1]); $gCtx.lineTo(p3[0], p3[1]); $gCtx.closePath(); if (this.linewidth > 1) { $gCtx.fill(); } else { $gCtx.stroke(); } this.offset += this.length + this.gap; } drawArrow3Out() { var p1 = [this.offset, -this.length]; var p2 = [this.offset + this.length, 0]; var p3 = [this.offset, +this.length]; $gCtx.beginPath(); $gCtx.moveTo(p1[0], p1[1]); $gCtx.lineTo(p2[0], p2[1]); $gCtx.lineTo(p3[0], p3[1]); $gCtx.closePath(); if (this.linewidth > 1) { $gCtx.fill(); } else { $gCtx.stroke(); } this.offset += this.length + this.gap; } drawArrow4In() { var p1 = [this.offset, 0]; var p2 = [this.offset + (4 * this.length), -this.length]; var p3 = [this.offset + (3 * this.length), 0]; var p4 = [this.offset + (4 * this.length), +this.length]; $drawLineP(p1, p3); $drawLineP(p2, p3); $drawLineP(p3, p4); this.offset += (3 * this.length) + this.gap; } drawArrow4Out() { var p1 = [this.offset, 0]; var p2 = [this.offset + (2 * this.length), -this.length]; var p3 = [this.offset + (3 * this.length), 0]; var p4 = [this.offset + (2 * this.length), +this.length]; $drawLineP(p1, p3); $drawLineP(p2, p3); $drawLineP(p3, p4); this.offset += (3 * this.length) + this.gap; } drawArrow5In() { var p1 = [this.offset, 0]; var p2 = [this.offset + (4 * this.length), -this.length]; var p3 = [this.offset + (3 * this.length), 0]; var p4 = [this.offset + (4 * this.length), +this.length]; $gCtx.beginPath(); $gCtx.moveTo(p4[0], p4[1]); $gCtx.lineTo(p2[0], p2[1]); $gCtx.lineTo(p3[0], p3[1]); $gCtx.closePath(); if (this.linewidth > 1) { $gCtx.fill(); } else { $gCtx.stroke(); } $drawLineP(p1, p3); this.offset += (3 * this.length) + this.gap; } drawArrow5Out() { var p1 = [this.offset, 0]; var p2 = [this.offset + (2 * this.length), -this.length]; var p3 = [this.offset + (3 * this.length), 0]; var p4 = [this.offset + (2 * this.length), +this.length]; $gCtx.beginPath(); $gCtx.moveTo(p4[0], p4[1]); $gCtx.lineTo(p2[0], p2[1]); $gCtx.lineTo(p3[0], p3[1]); $gCtx.closePath(); if (this.linewidth > 1) { $gCtx.fill(); } else { $gCtx.stroke(); } $drawLineP(p1, p3); this.offset += (3 * this.length) + this.gap; } } // class ArrowDecoration class BridgeDecoration extends Decoration { constructor($widget, $bridge) { super($widget); // this.side = $bridge.attr('side'); this.end = $bridge.attr('end'); this.color = $bridge.attr('color'); this.linewidth = Number($bridge.attr('linewidth')); this.approachwidth = Number($bridge.attr('approachwidth')); this.deckwidth = Number($bridge.attr('deckwidth')); } draw() { super.draw(); var $widget = this.$widget; $gCtx.save(); // save current line width and color // set color and width $gCtx.strokeStyle = this.color; $gCtx.fillStyle = this.color; $gCtx.lineWidth = this.linewidth; if ($widget.circle == "yes") { this.drawBridgeCircle(); } else if ($widget.arc == "yes") { this.drawBridgeArc(); } else if ($widget.bezier == "yes") { this.drawBridgeBezier(); } else { this.drawBridgeStrait(); } this.drawBridgeEnds(); $gCtx.restore(); // restore color and width back to default } // draw() drawBridgeCircle() { var $widget = this.$widget; var halfWidth = this.deckwidth / 2; var ep1 = this.ep1, ep2 = this.ep2; var startAngleRAD = this.startAngleRAD, stopAngleRAD = this.stopAngleRAD; var v = [0, +halfWidth]; if ($widget.flip == "yes") { v = [0, -halfWidth]; [startAngleRAD, stopAngleRAD] = [stopAngleRAD, startAngleRAD]; } if ((this.side == "right") || (this.side == "both")) { var tp1 = $point_add(ep1, $point_rotate(v, startAngleRAD)); var tp2 = $point_add(ep2, $point_rotate(v, stopAngleRAD)); if ($widget.flip == "yes") { $drawArcP(tp2, tp1, $widget.angle); } else { $drawArcP(tp1, tp2, $widget.angle); } } if ((this.side == "left") || (this.side == "both")) { var tp1 = $point_subtract(ep1, $point_rotate(v, startAngleRAD)); var tp2 = $point_subtract(ep2, $point_rotate(v, stopAngleRAD)); if ($widget.flip == "yes") { $drawArcP(tp2, tp1, $widget.angle); } else { $drawArcP(tp1, tp2, $widget.angle); } } } drawBridgeArc() { //draw arc of ellipse var $widget = this.$widget; var tp1 = this.ep1, tp2 = this.ep2; var startAngleRAD = this.startAngleRAD, stopAngleRAD = this.stopAngleRAD; if ($widget.flip == "yes") { [tp1, tp2] = [tp2, tp1]; startAngleRAD += Math.PI; stopAngleRAD += Math.PI; } var halfWidth = this.deckwidth / 2; var x, y; var rw = tp2[0] - tp1[0], rh = tp2[1] - tp1[1]; [x, y, rw, rh] = this.getArcParams(rw, rh, tp1, tp2); rw -= halfWidth; rh -= halfWidth; if ((this.side == "right") || (this.side == "both")) { $drawEllipse(x, y, rw, rh, Math.PI + stopAngleRAD, startAngleRAD); } rw += this.deckwidth; rh += this.deckwidth; if ((this.side == "left") || (this.side == "both")) { $drawEllipse(x, y, rw, rh, Math.PI + stopAngleRAD, startAngleRAD); } } // drawBridgeArc() drawBridgeBezier() { var $widget = this.$widget; var ep1 = this.ep1, ep2 = this.ep2; var points = [[ep1[0], ep1[1]]]; // first point var $cps = $widget.controlpoints; // get the control points $cps.each(function( idx, elem ) { // control points points.push($getLayoutPoint(elem)); }); points.push([ep2[0], ep2[1]]); // last point var halfWidth = this.deckwidth / 2; if (((this.side == "left") || (this.side == "both"))) { $drawBezier(points, this.color, this.linewidth, -halfWidth); } if ((this.side == "right") || (this.side == "both")) { $drawBezier(points, this.color, this.linewidth, +halfWidth); } } drawBridgeStrait() { var $widget = this.$widget; var ep1 = this.ep1, ep2 = this.ep2; var halfWidth = this.deckwidth / 2; var vector = $point_orthogonal($point_normalizeTo($point_subtract(ep2, ep1), halfWidth)); if ((this.side == "right") || (this.side == "both")) { $drawLineP($point_add(ep1, vector), $point_add(ep2, vector)); } if (((this.side == "left") || (this.side == "both"))) { $drawLineP($point_subtract(ep1, vector), $point_subtract(ep2, vector)); } } drawBridgeEnds() { if ((this.end == "entry") || (this.end == "both")) { this.drawBridgeEntry(); } if ((this.end == "exit") || (this.end == "both")) { this.drawBridgeExit(); } } drawBridgeEntry() { var $widget = this.$widget; var ep1 = this.ep1; var startAngleRAD = this.startAngleRAD, stopAngleRAD = this.stopAngleRAD; var halfWidth = this.deckwidth / 2; var isRight = ((this.side == "right") || (this.side == "both")); var isLeft = ((this.side == "left") || (this.side == "both")); if ($widget.flip == "yes") { [isRight, isLeft] = [isLeft, isRight]; [startAngleRAD, stopAngleRAD] = [stopAngleRAD, startAngleRAD]; } var p1, p2; if (isRight) { p1 = [-this.approachwidth, +this.approachwidth + halfWidth]; p2 = [0, +halfWidth]; p1 = $point_add($point_rotate(p1, startAngleRAD), ep1); p2 = $point_add($point_rotate(p2, startAngleRAD), ep1); $drawLineP(p1, p2); } if (isLeft) { p1 = [-this.approachwidth, -this.approachwidth - halfWidth]; p2 = [0, -halfWidth]; p1 = $point_add($point_rotate(p1, startAngleRAD), ep1); p2 = $point_add($point_rotate(p2, startAngleRAD), ep1); $drawLineP(p1, p2); } } drawBridgeExit() { var $widget = this.$widget; var ep2 = this.ep2; var startAngleRAD = this.startAngleRAD, stopAngleRAD = this.stopAngleRAD; var halfWidth = this.deckwidth / 2; var isRight = ((this.side == "right") || (this.side == "both")); var isLeft = ((this.side == "left") || (this.side == "both")); if ($widget.flip == "yes") { [isRight, isLeft] = [isLeft, isRight]; [startAngleRAD, stopAngleRAD] = [stopAngleRAD, startAngleRAD]; } var p1, p2; if (isRight) { p1 = [+this.approachwidth, +this.approachwidth + halfWidth]; p2 = [0, +halfWidth]; p1 = $point_add($point_rotate(p1, stopAngleRAD), ep2); p2 = $point_add($point_rotate(p2, stopAngleRAD), ep2); $drawLineP(p1, p2); } if (isLeft) { p1 = [+this.approachwidth, -this.approachwidth - halfWidth]; p2 = [0, -halfWidth]; p1 = $point_add($point_rotate(p1, stopAngleRAD), ep2); p2 = $point_add($point_rotate(p2, stopAngleRAD), ep2); $drawLineP(p1, p2); } } } // BridgeDecoration class BumperDecoration extends Decoration { constructor($widget, $bumper) { super($widget); // this.end = $bumper.attr('end'); this.color = $bumper.attr('color'); this.linewidth = Number($bumper.attr('linewidth')); this.length = Number($bumper.attr('length')); } draw() { super.draw(); $gCtx.save(); // save current line width and color // set color and width $gCtx.strokeStyle = this.color; $gCtx.fillStyle = this.color; $gCtx.lineWidth = this.linewidth; var $widget = this.$widget; var startAngleRAD = this.startAngleRAD, stopAngleRAD = this.stopAngleRAD; if ($widget.flip == "yes") { [startAngleRAD, stopAngleRAD] = [stopAngleRAD, startAngleRAD]; } var bumperLength = this.length; var halfLength = bumperLength / 2; // common points if ((this.end == "start") || (this.end == "both")) { var p1 = [0, -halfLength], p2 = [0, +halfLength]; var p1 = $point_add($point_rotate(p1, startAngleRAD), this.ep1); var p2 = $point_add($point_rotate(p2, startAngleRAD), this.ep1); $drawLineP(p1, p2); // draw cross tie } if ((this.end == "stop") || (this.end == "both")) { var p1 = [0, -halfLength], p2 = [0, +halfLength]; var p1 = $point_add($point_rotate(p1, stopAngleRAD), this.ep2); var p2 = $point_add($point_rotate(p2, stopAngleRAD), this.ep2); $drawLineP(p1, p2); // draw cross tie } $gCtx.restore(); // restore color and width back to default } } // class BumperDecoration class TunnelDecoration extends Decoration { constructor($widget, $tunnel) { super($widget); // this.side = $tunnel.attr('side'); this.end = $tunnel.attr('end'); this.color = $tunnel.attr('color'); this.linewidth = Number($tunnel.attr('linewidth')); this.entrancewidth = Number($tunnel.attr('entrancewidth')); this.floorwidth = Number($tunnel.attr('floorwidth')); } draw() { super.draw(); var $widget = this.$widget; $gCtx.save(); // save current line width and color // set color and width $gCtx.strokeStyle = this.color; $gCtx.fillStyle = this.color; $gCtx.lineWidth = this.linewidth; $gCtx.setLineDash([6, 4]); if ($widget.circle == "yes") { this.drawTunnelCircle(); } else if ($widget.arc == "yes") { this.drawTunnelArc(); } else if ($widget.bezier == "yes") { this.drawTunnelBezier(); } else { this.drawTunnelStrait(); } $gCtx.setLineDash([]); this.drawTunnelEnds(); $gCtx.restore(); // restore color and width back to default } // draw() drawTunnelCircle() { var $widget = this.$widget; var halfWidth = this.floorwidth / 2; var ep1 = this.ep1, ep2 = this.ep2; var startAngleRAD = this.startAngleRAD, stopAngleRAD = this.stopAngleRAD; var v = [0, +halfWidth]; if ($widget.flip == "yes") { v = [0, -halfWidth]; [startAngleRAD, stopAngleRAD] = [stopAngleRAD, startAngleRAD]; } if ((this.side == "right") || (this.side == "both")) { var tp1 = $point_add(ep1, $point_rotate(v, startAngleRAD)); var tp2 = $point_add(ep2, $point_rotate(v, stopAngleRAD)); if ($widget.flip == "yes") { $drawArcP(tp2, tp1, $widget.angle); } else { $drawArcP(tp1, tp2, $widget.angle); } } if ((this.side == "left") || (this.side == "both")) { var tp1 = $point_subtract(ep1, $point_rotate(v, startAngleRAD)); var tp2 = $point_subtract(ep2, $point_rotate(v, stopAngleRAD)); if ($widget.flip == "yes") { $drawArcP(tp2, tp1, $widget.angle); } else { $drawArcP(tp1, tp2, $widget.angle); } } } drawTunnelArc() { //draw arc of ellipse var $widget = this.$widget; var tp1 = this.ep1, tp2 = this.ep2; var startAngleRAD = this.startAngleRAD, stopAngleRAD = this.stopAngleRAD; if ($widget.flip == "yes") { [tp1, tp2] = [tp2, tp1]; startAngleRAD += Math.PI; stopAngleRAD += Math.PI; } var halfWidth = this.floorwidth / 2; var x, y; var rw = tp2[0] - tp1[0], rh = tp2[1] - tp1[1]; [x, y, rw, rh] = this.getArcParams(rw, rh, tp1, tp2); rw -= halfWidth; rh -= halfWidth; if ((this.side == "right") || (this.side == "both")) { $drawEllipse(x, y, rw, rh, Math.PI + stopAngleRAD, startAngleRAD); } rw += this.floorwidth; rh += this.floorwidth; if ((this.side == "left") || (this.side == "both")) { $drawEllipse(x, y, rw, rh, Math.PI + stopAngleRAD, startAngleRAD); } } // drawTunnelArc() drawTunnelBezier() { var $widget = this.$widget; var ep1 = this.ep1, ep2 = this.ep2; var points = [[ep1[0], ep1[1]]]; // first point var $cps = $widget.controlpoints; // get the control points $cps.each(function( idx, elem ) { // control points points.push($getLayoutPoint(elem)); }); points.push([ep2[0], ep2[1]]); // last point var halfWidth = this.floorwidth / 2; if (((this.side == "left") || (this.side == "both"))) { $drawBezier(points, this.color, this.linewidth, -halfWidth); } if ((this.side == "right") || (this.side == "both")) { $drawBezier(points, this.color, this.linewidth, +halfWidth); } } drawTunnelStrait() { var $widget = this.$widget; var ep1 = this.ep1, ep2 = this.ep2; var halfWidth = this.floorwidth / 2; var vector = $point_orthogonal($point_normalizeTo($point_subtract(ep2, ep1), halfWidth)); if ((this.side == "right") || (this.side == "both")) { $drawLineP($point_add(ep1, vector), $point_add(ep2, vector)); } if (((this.side == "left") || (this.side == "both"))) { $drawLineP($point_subtract(ep1, vector), $point_subtract(ep2, vector)); } } drawTunnelEnds() { if ((this.end == "entry") || (this.end == "both")) { this.drawTunnelEntry(); } if ((this.end == "exit") || (this.end == "both")) { this.drawTunnelExit(); } } drawTunnelEntry() { var $widget = this.$widget; var ep1 = this.ep1; var angleRAD = this.startAngleRAD; var isRight = ((this.side == "right") || (this.side == "both")); var isLeft = ((this.side == "left") || (this.side == "both")); if ($widget.flip == "yes") { [isRight, isLeft] = [isLeft, isRight]; // swap left and right angleRAD = this.stopAngleRAD; } $gCtx.save(); $gCtx.translate(ep1[0], ep1[1]); $gCtx.rotate(angleRAD); if (isRight) { this.drawTunnelEntryRight(); } if (isLeft) { this.drawTunnelEntryLeft(); } $gCtx.restore(); } drawTunnelEntryRight() { var halfWidth = this.floorwidth / 2; var halfEntranceWidth = this.entrancewidth / 2; var halfFloorWidth = this.floorwidth / 2; var halfDiffWidth = halfEntranceWidth - halfFloorWidth; var p1, p2, p3, p4, p5, p6, p7; p1 = [0, 0]; p2 = [0, +halfFloorWidth]; p3 = [0, +halfEntranceWidth]; p4 = [-halfEntranceWidth - halfFloorWidth, +halfEntranceWidth]; p5 = [-halfEntranceWidth - halfFloorWidth, +halfEntranceWidth - halfDiffWidth]; p6 = [-halfFloorWidth, +halfEntranceWidth - halfDiffWidth]; p7 = [-halfDiffWidth, 0]; $gCtx.beginPath(); $gCtx.moveTo(p1[0], p1[1]); $gCtx.lineTo(p2[0], p2[1]); $gCtx.quadraticCurveTo(p3[0], p3[1], p4[0], p4[1]); $gCtx.lineTo(p5[0], p5[1]); $gCtx.quadraticCurveTo(p6[0], p6[1], p7[0], p7[1]); $gCtx.closePath(); $gCtx.stroke(); } drawTunnelEntryLeft() { var halfWidth = this.floorwidth / 2; var halfEntranceWidth = this.entrancewidth / 2; var halfFloorWidth = this.floorwidth / 2; var halfDiffWidth = halfEntranceWidth - halfFloorWidth; var p1, p2, p3, p4, p5, p6, p7; p1 = [0, 0]; p2 = [0, -halfFloorWidth]; p3 = [0, -halfEntranceWidth]; p4 = [-halfEntranceWidth - halfFloorWidth, -halfEntranceWidth]; p5 = [-halfEntranceWidth - halfFloorWidth, -halfEntranceWidth + halfDiffWidth]; p6 = [-halfFloorWidth, -halfEntranceWidth + halfDiffWidth]; p7 = [-halfDiffWidth, 0]; $gCtx.beginPath(); $gCtx.moveTo(p1[0], p1[1]); $gCtx.lineTo(p2[0], p2[1]); $gCtx.quadraticCurveTo(p3[0], p3[1], p4[0], p4[1]); $gCtx.lineTo(p5[0], p5[1]); $gCtx.quadraticCurveTo(p6[0], p6[1], p7[0], p7[1]); $gCtx.closePath(); $gCtx.stroke(); } drawTunnelExit() { var $widget = this.$widget; var ep2 = this.ep2; var angleRAD = this.stopAngleRAD; var isRight = ((this.side == "right") || (this.side == "both")); var isLeft = ((this.side == "left") || (this.side == "both")); if ($widget.flip == "yes") { [isRight, isLeft] = [isLeft, isRight]; angleRAD = this.startAngleRAD; } var halfWidth = this.floorwidth / 2; var halfEntranceWidth = this.entrancewidth / 2; var halfFloorWidth = this.floorwidth / 2; var halfDiffWidth = halfEntranceWidth - halfFloorWidth; var p1, p2, p3, p4, p5, p6, p7; $gCtx.save(); $gCtx.translate(ep2[0], ep2[1]); $gCtx.rotate(angleRAD); if (isRight) { this.drawTunnelExitRight(); } if (isLeft) { this.drawTunnelExitLeft(); } $gCtx.restore(); } drawTunnelExitRight() { var halfWidth = this.floorwidth / 2; var halfEntranceWidth = this.entrancewidth / 2; var halfFloorWidth = this.floorwidth / 2; var halfDiffWidth = halfEntranceWidth - halfFloorWidth; var p1, p2, p3, p4, p5, p6, p7; p1 = [0, 0]; p2 = [0, +halfFloorWidth]; p3 = [0, +halfEntranceWidth]; p4 = [halfEntranceWidth + halfFloorWidth, +halfEntranceWidth]; p5 = [halfEntranceWidth + halfFloorWidth, +halfEntranceWidth - halfDiffWidth]; p6 = [halfFloorWidth, +halfEntranceWidth - halfDiffWidth]; p7 = [halfDiffWidth, 0]; $gCtx.beginPath(); $gCtx.moveTo(p1[0], p1[1]); $gCtx.lineTo(p2[0], p2[1]); $gCtx.quadraticCurveTo(p3[0], p3[1], p4[0], p4[1]); $gCtx.lineTo(p5[0], p5[1]); $gCtx.quadraticCurveTo(p6[0], p6[1], p7[0], p7[1]); $gCtx.closePath(); $gCtx.stroke(); } drawTunnelExitLeft() { var halfWidth = this.floorwidth / 2; var halfEntranceWidth = this.entrancewidth / 2; var halfFloorWidth = this.floorwidth / 2; var halfDiffWidth = halfEntranceWidth - halfFloorWidth; var p1, p2, p3, p4, p5, p6, p7; p1 = [0, 0]; p2 = [0, -halfFloorWidth]; p3 = [0, -halfEntranceWidth]; p4 = [halfEntranceWidth + halfFloorWidth, -halfEntranceWidth]; p5 = [halfEntranceWidth + halfFloorWidth, -halfEntranceWidth + halfDiffWidth]; p6 = [halfFloorWidth, -halfEntranceWidth + halfDiffWidth]; p7 = [halfDiffWidth, 0]; $gCtx.beginPath(); $gCtx.moveTo(p1[0], p1[1]); $gCtx.lineTo(p2[0], p2[1]); $gCtx.quadraticCurveTo(p3[0], p3[1], p4[0], p4[1]); $gCtx.lineTo(p5[0], p5[1]); $gCtx.quadraticCurveTo(p6[0], p6[1], p7[0], p7[1]); $gCtx.closePath(); $gCtx.stroke(); } } // End of Layout Editor Decoration classes =======