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