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}