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