001package jmri.jmrit.operations.automation;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.*;
006
007import javax.swing.JComboBox;
008
009import org.jdom2.Element;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013import jmri.InstanceManager;
014import jmri.beans.PropertyChangeSupport;
015import jmri.jmrit.operations.automation.actions.Action;
016import jmri.jmrit.operations.automation.actions.HaltAction;
017import jmri.jmrit.operations.setup.Control;
018import jmri.jmrit.operations.trains.TrainManagerXml;
019
020/**
021 * Automation for operations
022 *
023 * @author Daniel Boudreau Copyright (C) 2016
024 */
025public class Automation extends PropertyChangeSupport implements PropertyChangeListener {
026
027    protected String _id = "";
028    protected String _name = "";
029    protected String _comment = "";
030    protected AutomationItem _currentAutomationItem = null;
031    protected AutomationItem _lastAutomationItem = null;
032    protected AutomationItem _gotoAutomationItem = null;
033    protected boolean _running = false;
034
035    // stores AutomationItems for this automation
036    protected HashMap<String, AutomationItem> _automationHashTable = new HashMap<>();
037    protected int _IdNumber = 0; // each item in a automation gets its own unique id
038
039    public static final String REGEX = "c"; // NOI18N
040
041    public static final String LISTCHANGE_CHANGED_PROPERTY = "automationListChange"; // NOI18N
042    public static final String CURRENT_ITEM_CHANGED_PROPERTY = "automationCurrentItemChange"; // NOI18N
043    public static final String RUNNING_CHANGED_PROPERTY = "automationRunningChange"; // NOI18N
044    public static final String DISPOSE = "automationDispose"; // NOI18N
045
046    public Automation(String id, String name) {
047        log.debug("New automation ({}) id: {}", name, id);
048        _name = name;
049        _id = id;
050    }
051
052    public String getId() {
053        return _id;
054    }
055
056    public void setName(String name) {
057        String old = _name;
058        _name = name;
059        if (!old.equals(name)) {
060            setDirtyAndFirePropertyChange("AutomationName", old, name); // NOI18N
061        }
062    }
063
064    // for combo boxes
065    @Override
066    public String toString() {
067        return getName();
068    }
069
070    public String getName() {
071        return _name;
072    }
073
074    public int getSize() {
075        return _automationHashTable.size();
076    }
077
078    public void setComment(String comment) {
079        String old = _comment;
080        _comment = comment;
081        if (!old.equals(comment)) {
082            setDirtyAndFirePropertyChange("AutomationComment", old, comment); // NOI18N
083        }
084    }
085
086    public String getComment() {
087        return _comment;
088    }
089
090    public String getCurrentActionString() {
091        if (getCurrentAutomationItem() != null && getCurrentAutomationItem().getAction() != null) {
092            return getCurrentAutomationItem().getId() + " " + getCurrentAutomationItem().getAction().getActionString();
093        }
094        return "";
095    }
096
097    public String getActionStatus() {
098        if (getCurrentAutomationItem() != null) {
099            return getCurrentAutomationItem().getStatus();
100        }
101        return "";
102    }
103
104    public String getMessage() {
105        if (getCurrentAutomationItem() != null && getCurrentAutomationItem().getAction() != null) {
106            return getCurrentAutomationItem().getAction().getFormatedMessage(getCurrentAutomationItem().getMessage());
107        }
108        return "";
109    }
110
111    public void setRunning(boolean running) {
112        boolean old = _running;
113        _running = running;
114        if (old != running) {
115            firePropertyChange(RUNNING_CHANGED_PROPERTY, old, running); // NOI18N
116        }
117    }
118
119    public boolean isRunning() {
120        return _running;
121    }
122
123    public boolean isActionRunning() {
124        for (AutomationItem item : getItemsBySequenceList()) {
125            if (item.isActionRunning()) {
126                return true;
127            }
128        }
129        return false;
130    }
131
132    /**
133     * Used to determine if automation is at the start of its sequence.
134     *
135     * @return true if the current action is the first action in the list.
136     */
137    public boolean isReadyToRun() {
138        return (getSize() > 0 && getCurrentAutomationItem() == getItemsBySequenceList().get(0));
139    }
140
141    public void run() {
142        if (getSize() > 0) {
143            log.debug("run automation ({})", getName());
144            _gotoAutomationItem = null;
145            setCurrentAutomationItem(getItemsBySequenceList().get(0));
146            setRunning(true);
147            step();
148        }
149    }
150
151    public void step() {
152        log.debug("step automation ({})", getName());
153        if (getCurrentAutomationItem() != null && getCurrentAutomationItem().getAction() != null) {
154            if (getCurrentAutomationItem().getAction().getClass().equals(HaltAction.class)
155                    && getCurrentAutomationItem().isActionRan()
156                    && getCurrentAutomationItem() != getItemsBySequenceList().get(0)) {
157                setNextAutomationItem();
158            }
159            if (getCurrentAutomationItem() == getItemsBySequenceList().get(0)) {
160                resetAutomationItems();
161            }
162            performAction(getCurrentAutomationItem());
163        }
164    }
165
166    private void performAction(AutomationItem item) {
167        if (item.isActionRunning()) {
168            log.debug("Action ({}) item id: {} already running", item.getAction().getName(), item.getId());
169        } else {
170            log.debug("Perform action ({}) item id: {}", item.getAction().getName(), item.getId());
171            item.getAction().removePropertyChangeListener(this);
172            item.getAction().addPropertyChangeListener(this);
173            Thread runAction = jmri.util.ThreadingUtil.newThread(() -> {
174                item.getAction().doAction();
175            });
176            runAction.setName("Run action item: " + item.getId()); // NOI18N
177            runAction.start();
178        }
179    }
180
181    public void stop() {
182        log.debug("stop automation ({})", getName());
183        if (getCurrentAutomationItem() != null && getCurrentAutomationItem().getAction() != null) {
184            setRunning(false);
185            cancelActions();
186        }
187    }
188
189    private void cancelActions() {
190        for (AutomationItem item : getItemsBySequenceList()) {
191            item.getAction().cancelAction();
192        }
193    }
194
195    public void resume() {
196        if (getSize() > 0) {
197            log.debug("resume automation ({})", getName());
198            setRunning(true);
199            step();
200        }
201    }
202
203    public void reset() {
204        stop();
205        if (getSize() > 0) {
206            setCurrentAutomationItem(getItemsBySequenceList().get(0));
207            resetAutomationItems();
208            resetAutomationItemsActon();
209        }
210    }
211
212    private void resetAutomationItems() {
213        resetAutomationItems(getCurrentAutomationItem());
214    }
215    
216    private void resetAutomationItemsActon() {
217        for (AutomationItem automationItem : getItemsBySequenceList()) {
218            if (automationItem.getAction() != null) {
219                automationItem.getAction().reset();
220            }
221        }
222    }
223
224    public void resetAutomationItems(AutomationItem item) {
225        boolean found = false;
226        for (AutomationItem automationItem : getItemsBySequenceList()) {
227            if (!found && automationItem != item) {
228                continue;
229            }
230            found = true;
231            automationItem.reset();
232        }
233    }
234
235    public void setNextAutomationItem() {
236        log.debug("set next automation ({})", getName());
237        if (getSize() > 0) {
238            // goto?
239            if (_gotoAutomationItem != null) {
240                getCurrentAutomationItem().setGotoBranched(true);
241                setCurrentAutomationItem(_gotoAutomationItem);
242                resetAutomationItems(_gotoAutomationItem);
243                _gotoAutomationItem = null;
244                return; // done with goto
245            }
246            List<AutomationItem> items = getItemsBySequenceList();
247            for (int index = 0; index < items.size(); index++) {
248                AutomationItem item = items.get(index);
249                if (item == getCurrentAutomationItem()) {
250                    if (index + 1 < items.size()) {
251                        item = items.get(index + 1);
252                        setCurrentAutomationItem(item);
253                        if (item.isActionRan()) {
254                            continue;
255                        }
256                    } else {
257                        setCurrentAutomationItem(getItemsBySequenceList().get(0));
258                        setRunning(false); // reached the end of the list
259                    }
260                    return; // done
261                }
262            }
263        }
264        setCurrentAutomationItem(null);
265    }
266
267    /*
268     * Returns the next automationItem in the sequence
269     */
270    private AutomationItem getNextAutomationItem(AutomationItem item) {
271        List<AutomationItem> items = getItemsBySequenceList();
272        for (int index = 0; index < items.size(); index++) {
273            if (item == items.get(index)) {
274                if (index + 1 < items.size()) {
275                    return items.get(index + 1);
276                } else {
277                    break;
278                }
279            }
280        }
281        return null;
282    }
283
284    public void setCurrentAutomationItem(AutomationItem item) {
285        _lastAutomationItem = _currentAutomationItem;
286        _currentAutomationItem = item;
287        if (_lastAutomationItem != item) {
288            setDirtyAndFirePropertyChange(CURRENT_ITEM_CHANGED_PROPERTY, _lastAutomationItem, item); // NOI18N
289        }
290    }
291
292    public AutomationItem getCurrentAutomationItem() {
293        return _currentAutomationItem;
294    }
295
296    public AutomationItem getLastAutomationItem() {
297        return _lastAutomationItem;
298    }
299
300    public boolean isLastActionSuccessful() {
301        if (getLastAutomationItem() != null) {
302            return getLastAutomationItem().isActionSuccessful();
303        }
304        return false;
305    }
306
307    public void dispose() {
308        firePropertyChange(DISPOSE, null, DISPOSE);
309    }
310
311    public AutomationItem addItem() {
312        _IdNumber++;
313        String id = getId() + REGEX + Integer.toString(_IdNumber);
314        log.debug("Adding new item to ({}) id: {}", getName(), id);
315        AutomationItem item = new AutomationItem(id);
316        _automationHashTable.put(item.getId(), item);
317        item.setSequenceId(getSize());
318
319        if (getCurrentAutomationItem() == null) {
320            setCurrentAutomationItem(item);
321        }
322        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, getSize() - 1, getSize());
323        return item;
324    }
325
326    /**
327     * Add a automation item at a specific place (sequence) in the automation
328     * Allowable sequence numbers are 0 to max size of automation. 0 = start of
329     * list.
330     *
331     * @param sequence where to add a new item in the automation
332     *
333     * @return automation item
334     */
335    public AutomationItem addNewItem(int sequence) {
336        AutomationItem item = addItem();
337        if (sequence < 0 || sequence > getSize()) {
338            return item;
339        }
340        for (int i = 0; i < getSize() - sequence - 1; i++) {
341            moveItemUp(item);
342        }
343        return item;
344    }
345
346    /**
347     * Remember a NamedBean Object created outside the manager.
348     *
349     * @param item the item to be added to this automation.
350     */
351    public void register(AutomationItem item) {
352        _automationHashTable.put(item.getId(), item);
353        // find last id created
354        String[] getId = item.getId().split(Automation.REGEX);
355        int id = Integer.parseInt(getId[1]);
356        if (id > _IdNumber) {
357            _IdNumber = id;
358        }
359        if (getCurrentAutomationItem() == null) {
360            setCurrentAutomationItem(item); // default is to load the first item saved.
361        }
362        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, getSize() - 1, getSize());
363    }
364
365    /**
366     * Delete a AutomationItem
367     *
368     * @param item The item to be deleted.
369     *
370     */
371    public void deleteItem(AutomationItem item) {
372        if (item != null) {
373            if (item.isActionRunning()) {
374                stop();
375            }
376            if (getCurrentAutomationItem() == item) {
377                setNextAutomationItem();
378            }
379            String id = item.getId();
380            item.dispose();
381            int old = getSize();
382            _automationHashTable.remove(id);
383            resequenceIds();
384            if (getSize() <= 0) {
385                setCurrentAutomationItem(null);
386            }
387            setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, getSize());
388        }
389    }
390
391    /**
392     * Reorder the item sequence numbers for this automation
393     */
394    private void resequenceIds() {
395        int i = 1; // start sequence numbers at 1
396        for (AutomationItem item : getItemsBySequenceList()) {
397            item.setSequenceId(i++);
398        }
399    }
400
401    /**
402     * Get a AutomationItem by id
403     *
404     * @param id The string id of the item.
405     *
406     * @return automation item
407     */
408    public AutomationItem getItemById(String id) {
409        return _automationHashTable.get(id);
410    }
411
412    private List<AutomationItem> getItemsByIdList() {
413        List<AutomationItem> out = new ArrayList<>();
414        _automationHashTable.keySet().stream().sorted().forEach((id) -> {
415            out.add(getItemById(id));
416        });
417        return out;
418    }
419
420    /**
421     * Get a list of AutomationItems sorted by automation order
422     *
423     * @return list of AutomationItems ordered by sequence
424     */
425    public List<AutomationItem> getItemsBySequenceList() {
426        List<AutomationItem> items = new ArrayList<>();
427        for (AutomationItem item : getItemsByIdList()) {
428            for (int j = 0; j < items.size(); j++) {
429                if (item.getSequenceId() < items.get(j).getSequenceId()) {
430                    items.add(j, item);
431                    break;
432                }
433            }
434            if (!items.contains(item)) {
435                items.add(item);
436            }
437        }
438        return items;
439    }
440
441    /**
442     * Gets a JComboBox loaded with automation items.
443     *
444     * @return JComboBox with a list of automation items.
445     */
446    public JComboBox<AutomationItem> getComboBox() {
447        JComboBox<AutomationItem> box = new JComboBox<>();
448        for (AutomationItem item : getItemsBySequenceList()) {
449            box.addItem(item);
450        }
451        return box;
452    }
453
454    /**
455     * Places a AutomationItem earlier in the automation
456     *
457     * @param item The item to move up one position in the automation.
458     *
459     */
460    public void moveItemUp(AutomationItem item) {
461        int sequenceId = item.getSequenceId();
462        if (sequenceId - 1 <= 0) {
463            item.setSequenceId(getSize() + 1); // move to the end of the list
464            resequenceIds();
465        } else {
466            // adjust the other item taken by this one
467            AutomationItem replaceSi = getItemBySequenceId(sequenceId - 1);
468            if (replaceSi != null) {
469                replaceSi.setSequenceId(sequenceId);
470                item.setSequenceId(sequenceId - 1);
471            } else {
472                resequenceIds(); // error the sequence number is missing
473            }
474        }
475        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, sequenceId);
476    }
477
478    /**
479     * Places a AutomationItem later in the automation.
480     *
481     * @param item The item to move later in the automation.
482     *
483     */
484    public void moveItemDown(AutomationItem item) {
485        int sequenceId = item.getSequenceId();
486        if (sequenceId + 1 > getSize()) {
487            item.setSequenceId(0); // move to the start of the list
488            resequenceIds();
489        } else {
490            // adjust the other item taken by this one
491            AutomationItem replaceSi = getItemBySequenceId(sequenceId + 1);
492            if (replaceSi != null) {
493                replaceSi.setSequenceId(sequenceId);
494                item.setSequenceId(sequenceId + 1);
495            } else {
496                resequenceIds(); // error the sequence number is missing
497            }
498        }
499        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, sequenceId);
500    }
501
502    public AutomationItem getItemBySequenceId(int sequenceId) {
503        for (AutomationItem item : getItemsByIdList()) {
504            if (item.getSequenceId() == sequenceId) {
505                return item;
506            }
507        }
508        return null;
509    }
510
511    /**
512     * Copies automation.
513     *
514     * @param automation the automation to copy
515     */
516    public void copyAutomation(Automation automation) {
517        if (automation != null) {
518            setComment(automation.getComment());
519            for (AutomationItem item : automation.getItemsBySequenceList()) {
520                addItem().copyItem(item);
521            }
522            // now adjust GOTOs to reference the new automation
523            for (AutomationItem item : getItemsBySequenceList()) {
524                if (item.getGotoAutomationItem() != null) {
525                    item.setGotoAutomationItem(getItemBySequenceId(item.getGotoAutomationItem().getSequenceId()));
526                }
527            }
528        }
529    }
530
531    /**
532     * Construct this Entry from XML. This member has to remain synchronized
533     * with the detailed DTD in operations-trains.dtd
534     *
535     * @param e Consist XML element
536     */
537    public Automation(Element e) {
538        org.jdom2.Attribute a;
539        if ((a = e.getAttribute(Xml.ID)) != null) {
540            _id = a.getValue();
541        } else {
542            log.warn("no id attribute in automation element when reading operations");
543        }
544        if ((a = e.getAttribute(Xml.NAME)) != null) {
545            _name = a.getValue();
546        }
547        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
548            _comment = a.getValue();
549        }
550        if (e.getChildren(Xml.ITEM) != null) {
551            List<Element> eAutomationItems = e.getChildren(Xml.ITEM);
552            log.debug("automation: {} has {} items", getName(), eAutomationItems.size());
553            for (Element eAutomationItem : eAutomationItems) {
554                register(new AutomationItem(eAutomationItem));
555            }
556        }
557        // get the current item after all of the items above have been loaded
558        if ((a = e.getAttribute(Xml.CURRENT_ITEM)) != null) {
559            _currentAutomationItem = getItemById(a.getValue());
560        }
561
562    }
563
564    /**
565     * Create an XML element to represent this Entry. This member has to remain
566     * synchronized with the detailed DTD in operations-trains.dtd.
567     *
568     * @return Contents in a JDOM Element
569     */
570    public Element store() {
571        Element e = new org.jdom2.Element(Xml.AUTOMATION);
572        e.setAttribute(Xml.ID, getId());
573        e.setAttribute(Xml.NAME, getName());
574        e.setAttribute(Xml.COMMENT, getComment());
575        if (getCurrentAutomationItem() != null) {
576            e.setAttribute(Xml.CURRENT_ITEM, getCurrentAutomationItem().getId());
577        }
578        for (AutomationItem item : getItemsBySequenceList()) {
579            e.addContent(item.store());
580        }
581        return e;
582    }
583
584    private void checkForActionPropertyChange(PropertyChangeEvent evt) {
585        if (evt.getPropertyName().equals(Action.ACTION_COMPLETE_CHANGED_PROPERTY)
586                || evt.getPropertyName().equals(Action.ACTION_HALT_CHANGED_PROPERTY)) {
587            Action action = (Action) evt.getSource();
588            action.removePropertyChangeListener(this);
589        }
590        // the following code causes multiple wait actions to run concurrently
591        if (evt.getPropertyName().equals(Action.ACTION_RUNNING_CHANGED_PROPERTY)) {
592            firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
593            // when new value is true the action is running
594            if ((boolean) evt.getNewValue()) {
595                Action action = (Action) evt.getSource();
596                log.debug("Action ({}) is running", action.getActionString());
597                if (action.isConcurrentAction()) {
598                    AutomationItem item = action.getAutomationItem();
599                    AutomationItem nextItem = getNextAutomationItem(item);
600                    if (nextItem != null && nextItem.getAction().isConcurrentAction()) {
601                        performAction(nextItem); // start this wait action
602                    }
603                }
604            }
605        }
606        if (getCurrentAutomationItem() != null && getCurrentAutomationItem().getAction() == evt.getSource()) {
607            if (evt.getPropertyName().equals(Action.ACTION_COMPLETE_CHANGED_PROPERTY)
608                    || evt.getPropertyName().equals(Action.ACTION_HALT_CHANGED_PROPERTY)) {
609                getCurrentAutomationItem().getAction().cancelAction();
610                if (evt.getPropertyName().equals(Action.ACTION_COMPLETE_CHANGED_PROPERTY)) {
611                    setNextAutomationItem();
612                    if (isRunning()) {
613                        step(); // continue running by doing the next action
614                    }
615                } else if (evt.getPropertyName().equals(Action.ACTION_HALT_CHANGED_PROPERTY)) {
616                    if ((boolean) evt.getNewValue() == true) {
617                        log.debug("User halted successful action");
618                        setNextAutomationItem();
619                    }
620                    stop();
621                }
622            }
623            if (evt.getPropertyName().equals(Action.ACTION_GOTO_CHANGED_PROPERTY)) {
624                // the old property value is used to control branch
625                // if old = null, then it is a unconditional branch
626                // if old = true, branch if success
627                // if old = false, branch if failure
628                if (evt.getOldValue() == null || (boolean) evt.getOldValue() == isLastActionSuccessful()) {
629                    _gotoAutomationItem = (AutomationItem) evt.getNewValue();
630                }
631            }
632        }
633    }
634
635    @Override
636    public void propertyChange(PropertyChangeEvent e) {
637        if (Control.SHOW_PROPERTY) {
638            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e
639                    .getNewValue());
640        }
641        checkForActionPropertyChange(e);
642    }
643
644    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
645        // set dirty
646        InstanceManager.getDefault(TrainManagerXml.class).setDirty(true);
647        firePropertyChange(p, old, n);
648    }
649
650    private final static Logger log = LoggerFactory.getLogger(Automation.class);
651
652}