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}