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