001package jmri.jmrit.beantable.routetable;
002
003import jmri.*;
004import jmri.implementation.DefaultConditionalAction;
005import jmri.util.FileUtil;
006
007import java.util.ArrayList;
008
009import javax.annotation.CheckForNull;
010import javax.annotation.Nonnull;
011
012/**
013 * Enable creation of a Logix from a Route.
014 *
015 * Split from {@link jmri.jmrit.beantable.RouteTableAction}
016 *
017 * @author Dave Duchamp Copyright (C) 2004
018 * @author Bob Jacobsen Copyright (C) 2007
019 * @author Simon Reader Copyright (C) 2008
020 * @author Pete Cressman Copyright (C) 2009
021 * @author Egbert Broerse Copyright (C) 2016
022 * @author Paul Bender Copyright (C) 2020
023 */
024public class RouteExportToLogix {
025
026    private final String logixSysName;
027    private final String conditionalSysPrefix;
028    private final String systemName;
029    private final RouteManager routeManager;
030    private final LogixManager logixManager;
031    private final ConditionalManager conditionalManager;
032
033    RouteExportToLogix(String systemName){
034        this(systemName,InstanceManager.getDefault(RouteManager.class),
035                InstanceManager.getDefault(LogixManager.class),
036                InstanceManager.getDefault(ConditionalManager.class));
037    }
038
039    RouteExportToLogix(String systemName, RouteManager routeManager,
040            LogixManager logixManager,ConditionalManager conditionalManager){
041        this.systemName = systemName;
042        this.routeManager = routeManager;
043        this.logixManager = logixManager;
044        this.conditionalManager = conditionalManager;
045
046        String logixPrefix = logixManager.getSystemNamePrefix();
047        logixSysName = logixPrefix + ":RTX:";
048        conditionalSysPrefix = logixSysName + "C";
049    }
050
051    public void export() {
052        String logixSystemName = logixSysName + systemName;
053        Route route = routeManager.getBySystemName(systemName);
054        if(route == null ){
055            log.error("Route {} does not exist",systemName);
056            return;
057        }
058        String uName = route.getUserName();
059        Logix logix = logixManager.getBySystemName(logixSystemName);
060        if (logix == null) {
061            logix = logixManager.createNewLogix(logixSystemName, uName);
062            if (logix == null) {
063                log.error("Failed to create Logix {}, {}", logixSystemName, uName);
064                return;
065            }
066        }
067        logix.deActivateLogix();
068
069        /////////////////// Construct output actions for change to true //////////////////////
070        ArrayList<ConditionalAction> actionList = getConditionalActions(route);
071
072        String file = route.getOutputSoundName();
073        if (file!=null && file.length() > 0) {
074            actionList.add(new DefaultConditionalAction(Conditional.ACTION_OPTION_ON_CHANGE_TO_TRUE,
075                Conditional.Action.PLAY_SOUND, "", -1, FileUtil.getPortableFilename(file)));
076        }
077        file = route.getOutputScriptName();
078        if (file!=null && file.length() > 0) {
079            actionList.add(new DefaultConditionalAction(Conditional.ACTION_OPTION_ON_CHANGE_TO_TRUE,
080                Conditional.Action.RUN_SCRIPT, "", -1, FileUtil.getPortableFilename(file)));
081        }
082
083        ///// Construct 'AND' clause from 'VETO' controls ////////
084        ArrayList<ConditionalVariable> vetoList = getVetoVariables(route);
085
086        removeOldConditionalNames(route,logix);
087
088        ///////////////// Make Trigger Conditionals //////////////////////
089        int numConds = 1; // passed through all these, with new value returned each time
090        numConds = makeSensorConditional(route.getRouteSensor(0),route.getRouteSensorMode(0),
091            numConds, false, actionList, vetoList, logix, logixSystemName, uName);
092        numConds = makeSensorConditional(route.getRouteSensor(1), route.getRouteSensorMode(1),
093            numConds, false, actionList, vetoList, logix, logixSystemName, uName);
094        numConds = makeSensorConditional(route.getRouteSensor(2), route.getRouteSensorMode(2),
095            numConds, false, actionList, vetoList, logix, logixSystemName, uName);
096        numConds = makeTurnoutConditional(route.getCtlTurnout(), route.getControlTurnoutState(),
097            numConds, false, actionList, vetoList, logix, logixSystemName, uName);
098
099        ////// Construct actions for false from the 'any change' controls ////////////
100        numConds = makeSensorConditional(route.getRouteSensor(0), route.getRouteSensorMode(0),
101            numConds, true, actionList, vetoList, logix, logixSystemName, uName);
102        numConds = makeSensorConditional(route.getRouteSensor(1), route.getRouteSensorMode(1),
103            numConds, true, actionList, vetoList, logix, logixSystemName, uName);
104        numConds = makeSensorConditional(route.getRouteSensor(2), route.getRouteSensorMode(2),
105            numConds, true, actionList, vetoList, logix, logixSystemName, uName);
106        numConds = makeTurnoutConditional(route.getCtlTurnout(), route.getControlTurnoutState(),
107            numConds, true, actionList, vetoList, logix, logixSystemName, uName);
108        log.debug("Final number of conditionals: {}", numConds);
109        addRouteAlignmentSensorToLogix(logixSystemName, route, uName, logix);
110
111        addRouteLockToLogix(logixSystemName, route, uName, logix);
112
113        logix.activateLogix();
114        routeManager.deleteRoute(route);
115    }
116
117    private void addRouteAlignmentSensorToLogix(String logixSystemName, Route route, String uName, Logix logix) {
118        String cUserName;
119        ArrayList<ConditionalAction> actionList;
120        ///////////////// Set up Alignment Sensor, if there is one //////////////////////////
121        Sensor sens = route.getTurnoutsAlgdSensor();
122        if (sens != null) {
123            String sensorDisplayName = sens.getDisplayName();
124            String cSystemName = logixSystemName + "1A"; // NOI18N
125            cUserName = sens.getDisplayName() + "A " + uName; // NOI18N
126
127            ArrayList<ConditionalVariable> variableList = new ArrayList<>();
128            for(int i=0;i<route.getNumOutputTurnouts();i++){
129                Turnout ot = route.getOutputTurnout(i);
130                if ( ot != null ) {
131                    String name = ot.getDisplayName();
132
133                    // exclude toggled outputs
134                    switch (route.getOutputTurnoutState(i)) {
135                        case Turnout.CLOSED:
136                            variableList.add(new ConditionalVariable(false, Conditional.Operator.AND,
137                                Conditional.Type.TURNOUT_CLOSED, name, true));
138                            break;
139                        case Turnout.THROWN:
140                            variableList.add(new ConditionalVariable(false, Conditional.Operator.AND,
141                                Conditional.Type.TURNOUT_THROWN, name, true));
142                            break;
143                        default:
144                            log.warn("Turnout {} was {}, neither CLOSED nor THROWN; not handled",
145                                name, route.getOutputTurnoutState(i)); // NOI18N
146                    }
147                }
148            }
149            actionList = new ArrayList<>();
150            actionList.add(new DefaultConditionalAction(Conditional.ACTION_OPTION_ON_CHANGE_TO_TRUE,
151                Conditional.Action.SET_SENSOR, sensorDisplayName, Sensor.ACTIVE, ""));
152            actionList.add(new DefaultConditionalAction(Conditional.ACTION_OPTION_ON_CHANGE_TO_FALSE,
153                Conditional.Action.SET_SENSOR, sensorDisplayName, Sensor.INACTIVE, ""));
154
155            Conditional c = conditionalManager.createNewConditional(cSystemName, cUserName);
156            c.setStateVariables(variableList);
157            c.setLogicType(Conditional.AntecedentOperator.ALL_AND, "");
158            c.setAction(actionList);
159            logix.addConditional(cSystemName, 0);
160            c.calculate(true, null);
161        }
162    }
163
164    private void removeOldConditionalNames(Route route,Logix logix) {
165        // remove old Conditionals for actions (ver 2.5.2 only -remove a bad idea)
166        char[] ch = route.getSystemName().toCharArray();
167        int hash = 0;
168        for (char value : ch) {
169            hash += value;
170        }
171        String cSystemName = conditionalSysPrefix + "T" + hash;
172        removeConditionals(cSystemName, logix);
173        cSystemName = conditionalSysPrefix + "F" + hash;
174        removeConditionals(cSystemName, logix);
175        cSystemName = conditionalSysPrefix + "A" + hash;
176        removeConditionals(cSystemName, logix);
177        cSystemName = conditionalSysPrefix + "L" + hash;
178        removeConditionals(cSystemName, logix);
179
180        int n = 0;
181        do {
182            n++;
183            cSystemName = logix.getSystemName() + n + "A";
184        } while (removeConditionals(cSystemName, logix));
185        n = 0;
186        do {
187            n++;
188            cSystemName = logix.getSystemName() + n + "T";
189        } while (removeConditionals(cSystemName, logix));
190        cSystemName = logix.getSystemName() + "L";
191        removeConditionals(cSystemName, logix);
192    }
193
194    private void addRouteLockToLogix(String logixSystemName, Route route, String uName, Logix logix) {
195        String cSystemName;
196        String cUserName;
197        ArrayList<ConditionalAction> actionList;
198        ///////////////// Set lock turnout information if there is any //////////////////////////
199        Turnout lockControlTurnout = route.getLockCtlTurnout();
200        if ( lockControlTurnout != null ) {
201            
202
203            // verify name (logix doesn't use "provideXXX")
204            cSystemName = logixSystemName + "1L"; // NOI18N
205            cUserName = lockControlTurnout.getSystemName() + "L " + uName; // NOI18N
206            ArrayList<ConditionalVariable> variableList = new ArrayList<>();
207            int mode = route.getLockControlTurnoutState();
208            Conditional.Type conditionalType = Conditional.Type.TURNOUT_CLOSED;
209            if (mode == Route.ONTHROWN) {
210                conditionalType = Conditional.Type.TURNOUT_THROWN;
211            }
212            variableList.add(new ConditionalVariable(false, Conditional.Operator.NONE, conditionalType,
213                lockControlTurnout.getSystemName(), true));
214
215            actionList = new ArrayList<>();
216            int option = Conditional.ACTION_OPTION_ON_CHANGE_TO_TRUE;
217            int type = Turnout.LOCKED;
218            if (mode == Route.ONCHANGE) {
219                option = Conditional.ACTION_OPTION_ON_CHANGE;
220                type = Route.TOGGLE;
221            }
222            for(int i=0;i<route.getNumOutputTurnouts();i++){
223                Turnout ot = route.getOutputTurnout(i);
224                if ( ot != null ) {
225                    actionList.add(new DefaultConditionalAction(option, Conditional.Action.LOCK_TURNOUT,
226                        ot.getDisplayName(), type, ""));
227                }
228            }
229            if (mode != Route.ONCHANGE) {
230                // add non-toggle actions on
231                option = Conditional.ACTION_OPTION_ON_CHANGE_TO_FALSE;
232                type = Turnout.UNLOCKED;
233                for(int i=0;i<route.getNumOutputTurnouts();i++){
234                    Turnout ot = route.getOutputTurnout(i);
235                    if ( ot != null ) {
236                        actionList.add(new DefaultConditionalAction(option, Conditional.Action.LOCK_TURNOUT,
237                            ot.getDisplayName(), type, ""));
238                    }
239                }
240            }
241
242            // add new Conditionals for action on 'locks'
243            Conditional c = conditionalManager.createNewConditional(cSystemName, cUserName);
244            c.setStateVariables(variableList);
245            c.setLogicType(Conditional.AntecedentOperator.ALL_AND, "");
246            c.setAction(actionList);
247            logix.addConditional(cSystemName, 0);
248            c.calculate(true, null);
249        }
250    }
251
252    private ArrayList<ConditionalVariable> getVetoVariables(Route route) {
253        ArrayList<ConditionalVariable> vetoList = new ArrayList<>();
254
255        ConditionalVariable cVar = makeCtrlSensorVar(route.getRouteSensor(0),route.getRouteSensorMode(0), true, false);
256        if (cVar != null) {
257            vetoList.add(cVar);
258        }
259        cVar = makeCtrlSensorVar(route.getRouteSensor(1),route.getRouteSensorMode(1), true, false);
260        if (cVar != null) {
261            vetoList.add(cVar);
262        }
263        cVar = makeCtrlSensorVar(route.getRouteSensor(2),route.getRouteSensorMode(2), true, false);
264        if (cVar != null) {
265            vetoList.add(cVar);
266        }
267        cVar = makeCtrlTurnoutVar(route.getCtlTurnout(), route.getControlTurnoutState(), true, false);
268        if (cVar != null) {
269            vetoList.add(cVar);
270        }
271        return vetoList;
272    }
273
274    private ArrayList<ConditionalAction> getConditionalActions(Route route) {
275        ArrayList<ConditionalAction> actionList = new ArrayList<>();
276
277        for(int i=0;i<route.getNumOutputSensors();i++){
278            Sensor sens = route.getOutputSensor(i);
279            if ( sens != null ) {
280                actionList.add(new DefaultConditionalAction(Conditional.ACTION_OPTION_ON_CHANGE_TO_TRUE,
281                    Conditional.Action.SET_SENSOR, sens.getDisplayName(),
282                    route.getOutputSensorState(i), ""));
283            }
284        }
285        for(int i=0;i<route.getNumOutputTurnouts();i++){
286            Turnout to = route.getOutputTurnout(i);
287            if ( to != null ) {
288                actionList.add(new DefaultConditionalAction(Conditional.ACTION_OPTION_ON_CHANGE_TO_TRUE,
289                    Conditional.Action.SET_TURNOUT, to.getDisplayName(),
290                    route.getOutputTurnoutState(i), ""));
291            }
292        }
293        log.debug("sensor actions {} turnout actions {} resulting Action List size {}",
294                route.getNumOutputSensors(),route.getNumOutputTurnouts(),actionList.size());
295        return actionList;
296    }
297
298    private boolean removeConditionals(String cSystemName, Logix logix) {
299        Conditional c = conditionalManager.getBySystemName(cSystemName);
300        if (c != null) {
301            logix.deleteConditional(cSystemName);
302            conditionalManager.deleteConditional(c);
303            return true;
304        }
305        return false;
306    }
307
308    /**
309     * Create a new sensor conditional.
310     *
311     * @param selectedSensor will be used to determine which sensor to make a conditional for
312     * @param sensorMode will be used to determine the mode for the conditional
313     * @param numConds   number of existing route conditionals
314     * @param onChange   ???
315     * @param actionList actions to take in conditional
316     * @param vetoList   conditionals that can veto an action
317     * @param logix      Logix to add the conditional to
318     * @param prefix     system prefix for conditional
319     * @param uName      user name for conditional
320     * @return number of conditionals after the creation
321     * @throws IllegalArgumentException if "user input no good"
322     */
323    private int makeSensorConditional(Sensor selectedSensor, int sensorMode, int numConds,
324            boolean onChange, ArrayList<ConditionalAction> actionList, ArrayList<ConditionalVariable> vetoList,
325            Logix logix, String prefix, String uName) {
326
327        int loop = numConds;
328        ConditionalVariable cVar = makeCtrlSensorVar(selectedSensor, sensorMode, false, onChange);
329        if (cVar != null) {
330            ArrayList<ConditionalVariable> varList = new ArrayList<>();
331            varList.add(cVar);
332            for (ConditionalVariable conditionalVariable : vetoList) {
333                varList.add(cloneVariable(conditionalVariable));
334            }
335            String cSystemName = prefix + loop + "T";
336            String cUserName = selectedSensor.getDisplayName() + loop + "C " + uName;
337            Conditional c;
338            try {
339                c = conditionalManager.createNewConditional(cSystemName, cUserName);
340            } catch (Exception ex) {
341                // throw without creating any
342                throw new IllegalArgumentException("user input no good");
343            }
344            c.setStateVariables(varList);
345            int option = onChange ? Conditional.ACTION_OPTION_ON_CHANGE : Conditional.ACTION_OPTION_ON_CHANGE_TO_TRUE;
346            c.setAction(cloneActionList(actionList, option));
347            c.setLogicType(Conditional.AntecedentOperator.ALL_AND, "");
348            logix.addConditional(cSystemName, 0);
349            c.calculate(true, null);
350            loop++;
351        }
352        return loop;
353    }
354
355    /**
356     * Create a new turnout conditional.
357     *
358     * @param turnout    will be used to determine which turnout to make a conditional for
359     * @param state      will be used to determine the mode for the conditional
360     * @param numConds   number of existing route conditionals
361     * @param onChange   ???
362     * @param actionList actions to take in conditional
363     * @param vetoList   conditionals that can veto an action
364     * @param logix      Logix to add the conditional to
365     * @param prefix     system prefix for conditional
366     * @param uName      user name for conditional
367     * @return number of conditionals after the creation
368     * @throws IllegalArgumentException if "user input no good"
369     */
370    private int makeTurnoutConditional(Turnout turnout, int state, int numConds, boolean onChange,
371        ArrayList<ConditionalAction> actionList, ArrayList<ConditionalVariable> vetoList, Logix logix,
372            String prefix, String uName) {
373
374        int loop = numConds;
375        ConditionalVariable cVar = makeCtrlTurnoutVar(turnout,state, false, onChange);
376        if (cVar != null) {
377            ArrayList<ConditionalVariable> varList = new ArrayList<>();
378            varList.add(cVar);
379            for (ConditionalVariable conditionalVariable : vetoList) {
380                varList.add(cloneVariable(conditionalVariable));
381            }
382            String cSystemName = prefix + loop + "T";
383            String cUserName = turnout.getDisplayName() + loop + "C " + uName;
384            Conditional c;
385            try {
386                c = conditionalManager.createNewConditional(cSystemName, cUserName);
387            } catch (Exception ex) {
388                // throw without creating any
389                throw new IllegalArgumentException("user input no good");
390            }
391            c.setStateVariables(varList);
392            int option = onChange ? Conditional.ACTION_OPTION_ON_CHANGE : Conditional.ACTION_OPTION_ON_CHANGE_TO_TRUE;
393            c.setAction(cloneActionList(actionList, option));
394            c.setLogicType(Conditional.AntecedentOperator.ALL_AND, "");
395            logix.addConditional(cSystemName, 0);
396            c.calculate(true, null);
397            loop++;
398        }
399        return loop;
400    }
401
402    @CheckForNull
403    private ConditionalVariable makeCtrlTurnoutVar(@CheckForNull Turnout turnout,
404            int mode, boolean makeVeto, boolean onChange) {
405
406        if (turnout == null) {
407            return null;
408        }
409        String devName = turnout.getDisplayName();
410        Conditional.Operator oper = Conditional.Operator.AND;
411        Conditional.Type type;
412        boolean negated = false;
413        boolean trigger = true;
414        switch (mode) {
415            case Route.ONCLOSED:    // route fires if turnout goes closed
416                if (makeVeto || onChange) {
417                    return null;
418                }
419                type = Conditional.Type.TURNOUT_CLOSED;
420                break;
421            case Route.ONTHROWN:  // route fires if turnout goes thrown
422                if (makeVeto || onChange) {
423                    return null;
424                }
425                type = Conditional.Type.TURNOUT_THROWN;
426                break;
427            case Route.ONCHANGE:    // route fires if turnout goes active or inactive
428                if (makeVeto || !onChange) {
429                    return null;
430                }
431                type = Conditional.Type.TURNOUT_CLOSED;
432                break;
433            case Route.VETOCLOSED:  // turnout must be closed for route to fire
434                if (!makeVeto || onChange) {
435                    return null;
436                }
437                type = Conditional.Type.TURNOUT_CLOSED;
438                trigger = false;
439                negated = true;
440                break;
441            case Route.VETOTHROWN:  // turnout must be thrown for route to fire
442                if (!makeVeto || onChange) {
443                    return null;
444                }
445                type = Conditional.Type.TURNOUT_THROWN;
446                trigger = false;
447                negated = true;
448                break;
449            default:
450                log.error("Control Turnout {} has bad mode= {}", devName, mode);
451                return null;
452        }
453        return new ConditionalVariable(negated, oper, type, devName, trigger);
454    }
455
456
457    private ConditionalVariable cloneVariable(@Nonnull ConditionalVariable v) {
458        return new ConditionalVariable(v.isNegated(), v.getOpern(), v.getType(), v.getName(), v.doTriggerActions());
459    }
460
461    private ArrayList<ConditionalAction> cloneActionList(@Nonnull ArrayList<ConditionalAction> actionList, int option) {
462        ArrayList<ConditionalAction> list = new ArrayList<>();
463        for (ConditionalAction action : actionList) {
464            ConditionalAction clone = new DefaultConditionalAction();
465            clone.setType(action.getType());
466            clone.setOption(option);
467            clone.setDeviceName(action.getDeviceName());
468            clone.setActionData(action.getActionData());
469            clone.setActionString(action.getActionString());
470            list.add(clone);
471        }
472        return list;
473    }
474
475    @CheckForNull
476    private ConditionalVariable makeCtrlSensorVar(@CheckForNull Sensor selectedSensor,
477            int mode, boolean makeVeto, boolean onChange) {
478
479        if (selectedSensor == null) {
480            return null;
481        }
482        String devName = selectedSensor.getDisplayName();
483        Conditional.Operator oper = Conditional.Operator.AND;
484        boolean trigger = true;
485        boolean negated = false;
486        Conditional.Type type;
487        switch (mode) {
488            case Route.ONACTIVE:    // route fires if sensor goes active
489                if (makeVeto || onChange) {
490                    return null;
491                }
492                type = Conditional.Type.SENSOR_ACTIVE;
493                break;
494            case Route.ONINACTIVE:  // route fires if sensor goes inactive
495                if (makeVeto || onChange) {
496                    return null;
497                }
498                type = Conditional.Type.SENSOR_INACTIVE;
499                break;
500            case Route.ONCHANGE:  // route fires if sensor goes active or inactive
501                if (makeVeto || !onChange) {
502                    return null;
503                }
504                type = Conditional.Type.SENSOR_ACTIVE;
505                break;
506            case Route.VETOACTIVE:  // sensor must be active for route to fire
507                if (!makeVeto || onChange) {
508                    return null;
509                }
510                type = Conditional.Type.SENSOR_ACTIVE;
511                negated = true;
512                trigger = false;
513                break;
514            case Route.VETOINACTIVE:
515                if (!makeVeto || onChange) {
516                    return null;
517                }
518                type = Conditional.Type.SENSOR_INACTIVE;
519                negated = true;
520                trigger = false;
521                break;
522            default:
523                log.error("Control Sensor {} has bad mode= {}", devName, mode);
524                return null;
525        }
526        return new ConditionalVariable(negated, oper, type, devName, trigger);
527    }
528
529    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RouteExportToLogix.class);
530}