001package jmri.jmrit.display; 002 003import java.awt.event.ActionEvent; 004import java.awt.event.ActionListener; 005import java.util.HashMap; 006import java.util.Hashtable; 007import java.util.Map.Entry; 008 009import javax.annotation.Nonnull; 010import javax.swing.JCheckBoxMenuItem; 011import javax.swing.JPopupMenu; 012 013import jmri.InstanceManager; 014import jmri.NamedBeanHandle; 015import jmri.Turnout; 016import jmri.NamedBean.DisplayOptions; 017import jmri.jmrit.catalog.NamedIcon; 018import jmri.jmrit.display.palette.TableItemPanel; 019import jmri.jmrit.picker.PickListModel; 020import jmri.util.swing.JmriMouseEvent; 021 022/** 023 * An icon to display a status of a turnout. 024 * <p> 025 * This responds to only KnownState, leaving CommandedState to some other 026 * graphic representation later. 027 * <p> 028 * A click on the icon will command a state change. Specifically, it will set 029 * the CommandedState to the opposite (THROWN vs CLOSED) of the current 030 * KnownState. 031 * <p> 032 * The default icons are for a left-handed turnout, facing point for east-bound 033 * traffic. 034 * 035 * @author Bob Jacobsen Copyright (c) 2002 036 * @author PeteCressman Copyright (C) 2010, 2011 037 */ 038public class TurnoutIcon extends PositionableIcon implements java.beans.PropertyChangeListener { 039 040 protected HashMap<Integer, NamedIcon> _iconStateMap; // state int to icon 041 protected HashMap<String, Integer> _name2stateMap; // name to state 042 protected HashMap<Integer, String> _state2nameMap; // state to name 043 044 public TurnoutIcon(Editor editor) { 045 // super ctor call to make sure this is an icon label 046 super(new NamedIcon("resources/icons/smallschematics/tracksegments/os-lefthand-east-closed.gif", 047 "resources/icons/smallschematics/tracksegments/os-lefthand-east-closed.gif"), editor); 048 _control = true; 049 setPopupUtility(null); 050 } 051 052 public TurnoutIcon(NamedIcon s, Editor editor) { 053 // super ctor call to make sure this is an icon label 054 super(s, editor); 055 setOpaque(false); 056 _control = true; 057 // setPopupUtility(new TurnoutPopupUtil(this, this)); 058 setPopupUtility(null); 059 } 060 061 @Override 062 public Positionable deepClone() { 063 TurnoutIcon pos = new TurnoutIcon(_editor); 064 return finishClone(pos); 065 } 066 067 protected Positionable finishClone(TurnoutIcon pos) { 068 pos.setTurnout(getNamedTurnout().getName()); 069 pos._iconStateMap = cloneMap(_iconStateMap, pos); 070 pos.setTristate(getTristate()); 071 pos.setMomentary(getMomentary()); 072 pos.setDirectControl(getDirectControl()); 073 pos._iconFamily = _iconFamily; 074 return super.finishClone(pos); 075 } 076 077 // the associated Turnout object 078 private NamedBeanHandle<Turnout> namedTurnout = null; 079 080 /** 081 * Attach a named turnout to this display item. 082 * 083 * @param pName Used as a system/user name to lookup the turnout object 084 */ 085 public void setTurnout(String pName) { 086 if (InstanceManager.getNullableDefault(jmri.TurnoutManager.class) != null) { 087 try { 088 Turnout turnout = InstanceManager.turnoutManagerInstance().provideTurnout(pName); 089 setTurnout(InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, turnout)); 090 } catch (IllegalArgumentException ex) { 091 log.error("Turnout '{}' not available, icon won't see changes", pName); 092 } 093 } else { 094 log.error("No TurnoutManager for this protocol, icon won't see changes"); 095 } 096 } 097 098 public void setTurnout(NamedBeanHandle<Turnout> to) { 099 if (namedTurnout != null) { 100 getTurnout().removePropertyChangeListener(this); 101 } 102 namedTurnout = to; 103 if (namedTurnout != null) { 104 _iconStateMap = new HashMap<>(); 105 _name2stateMap = new HashMap<>(); 106 _name2stateMap.put("BeanStateUnknown", Turnout.UNKNOWN); 107 _name2stateMap.put("BeanStateInconsistent", Turnout.INCONSISTENT); 108 _name2stateMap.put("TurnoutStateClosed", Turnout.CLOSED); 109 _name2stateMap.put("TurnoutStateThrown", Turnout.THROWN); 110 _state2nameMap = new HashMap<>(); 111 _state2nameMap.put(Turnout.UNKNOWN, "BeanStateUnknown"); 112 _state2nameMap.put(Turnout.INCONSISTENT, "BeanStateInconsistent"); 113 _state2nameMap.put(Turnout.CLOSED, "TurnoutStateClosed"); 114 _state2nameMap.put(Turnout.THROWN, "TurnoutStateThrown"); 115 displayState(turnoutState()); 116 getTurnout().addPropertyChangeListener(this, namedTurnout.getName(), "Panel Editor Turnout Icon"); 117 } 118 } 119 120 public Turnout getTurnout() { 121 return namedTurnout.getBean(); 122 } 123 124 public NamedBeanHandle<Turnout> getNamedTurnout() { 125 return namedTurnout; 126 } 127 128 @Override 129 public jmri.NamedBean getNamedBean() { 130 return getTurnout(); 131 } 132 133 /** 134 * Place icon by its localized bean state name. 135 * 136 * @param name the state name 137 * @param icon the icon to place 138 */ 139 public void setIcon(String name, NamedIcon icon) { 140 if (log.isDebugEnabled()) { 141 log.debug("setIcon for name \"{}\" state= {}", name, _name2stateMap.get(name)); 142 } 143 _iconStateMap.put(_name2stateMap.get(name), icon); 144 displayState(turnoutState()); 145 } 146 147 /** 148 * Get icon by its localized bean state name. 149 */ 150 @Override 151 public NamedIcon getIcon(String state) { 152 return _iconStateMap.get(_name2stateMap.get(state)); 153 } 154 155 public NamedIcon getIcon(int state) { 156 return _iconStateMap.get(state); 157 } 158 159 @Override 160 public int maxHeight() { 161 int max = 0; 162 if (_iconStateMap != null) { 163 for (NamedIcon namedIcon : _iconStateMap.values()) { 164 max = Math.max(namedIcon.getIconHeight(), max); 165 } 166 } 167 return max; 168 } 169 170 @Override 171 public int maxWidth() { 172 int max = 0; 173 if ( _iconStateMap != null ) { 174 for (NamedIcon namedIcon : _iconStateMap.values()) { 175 max = Math.max(namedIcon.getIconWidth(), max); 176 } 177 } 178 return max; 179 } 180 181 /** 182 * Get current state of attached turnout 183 * 184 * @return A state variable from a Turnout, e.g. Turnout.CLOSED 185 */ 186 int turnoutState() { 187 if (namedTurnout != null) { 188 return getTurnout().getKnownState(); 189 } else { 190 return Turnout.UNKNOWN; 191 } 192 } 193 194 // update icon as state of turnout changes 195 @Override 196 public void propertyChange(java.beans.PropertyChangeEvent e) { 197 if (log.isDebugEnabled()) { 198 log.debug("property change: {} {} is now {}", getNameString(), e.getPropertyName(), e.getNewValue()); 199 } 200 201 // when there's feedback, transition through inconsistent icon for better 202 // animation 203 if (getTristate() 204 && (getTurnout().getFeedbackMode() != Turnout.DIRECT) 205 && (e.getPropertyName().equals(Turnout.PROPERTY_COMMANDED_STATE))) { 206 if (getTurnout().getCommandedState() != getTurnout().getKnownState()) { 207 int now = Turnout.INCONSISTENT; 208 displayState(now); 209 } 210 // this takes care of the quick double click 211 if (getTurnout().getCommandedState() == getTurnout().getKnownState()) { 212 int now = (Integer) e.getNewValue(); 213 displayState(now); 214 } 215 } 216 217 if (e.getPropertyName().equals(Turnout.PROPERTY_KNOWN_STATE)) { 218 int now = (Integer) e.getNewValue(); 219 displayState(now); 220 } 221 } 222 223 public String getStateName(int state) { 224 return _state2nameMap.get(state); 225 226 } 227 228 @Override 229 @Nonnull 230 public String getTypeString() { 231 return Bundle.getMessage("PositionableType_TurnoutIcon"); 232 } 233 234 @Override 235 public String getNameString() { 236 String name; 237 if (namedTurnout == null) { 238 name = Bundle.getMessage("NotConnected"); 239 } else { 240 name = getTurnout().getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME); 241 } 242 return name; 243 } 244 245 public void setTristate(boolean set) { 246 tristate = set; 247 } 248 249 public boolean getTristate() { 250 return tristate; 251 } 252 private boolean tristate = false; 253 254 private boolean momentary = false; 255 256 public boolean getMomentary() { 257 return momentary; 258 } 259 260 public void setMomentary(boolean m) { 261 momentary = m; 262 } 263 264 private boolean directControl = false; 265 266 public boolean getDirectControl() { 267 return directControl; 268 } 269 270 public void setDirectControl(boolean m) { 271 directControl = m; 272 } 273 274 private final JCheckBoxMenuItem momentaryItem = new JCheckBoxMenuItem(Bundle.getMessage("Momentary")); 275 private final JCheckBoxMenuItem directControlItem = new JCheckBoxMenuItem(Bundle.getMessage("DirectControl")); 276 277 /** 278 * Pop-up displays unique attributes of turnouts 279 */ 280 @Override 281 public boolean showPopUp(JPopupMenu popup) { 282 if (isEditable()) { 283 // add tristate option if turnout has feedback 284 if (namedTurnout != null && getTurnout().getFeedbackMode() != Turnout.DIRECT) { 285 addTristateEntry(popup); 286 } 287 288 popup.add(momentaryItem); 289 momentaryItem.setSelected(getMomentary()); 290 momentaryItem.addActionListener(e -> setMomentary(momentaryItem.isSelected())); 291 292 popup.add(directControlItem); 293 directControlItem.setSelected(getDirectControl()); 294 directControlItem.addActionListener(e -> setDirectControl(directControlItem.isSelected())); 295 } else if (getDirectControl()) { 296 getTurnout().setCommandedState(Turnout.THROWN); 297 } 298 return true; 299 } 300 301 private javax.swing.JCheckBoxMenuItem tristateItem = null; 302 303 void addTristateEntry(JPopupMenu popup) { 304 tristateItem = new javax.swing.JCheckBoxMenuItem(Bundle.getMessage("Tristate")); 305 tristateItem.setSelected(getTristate()); 306 popup.add(tristateItem); 307 tristateItem.addActionListener(e -> setTristate(tristateItem.isSelected())); 308 } 309 310 /** 311 * ****** popup AbstractAction method overrides ******** 312 */ 313 @Override 314 protected void rotateOrthogonal() { 315 for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) { 316 entry.getValue().setRotation(entry.getValue().getRotation() + 1, this); 317 } 318 displayState(turnoutState()); 319 // bug fix, must repaint icons that have same width and height 320 repaint(); 321 } 322 323 @Override 324 public void setScale(double s) { 325 _scale = s; 326 for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) { 327 entry.getValue().scale(s, this); 328 } 329 displayState(turnoutState()); 330 } 331 332 @Override 333 public void rotate(int deg) { 334 for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) { 335 entry.getValue().rotate(deg, this); 336 } 337 setDegrees(deg); 338 displayState(turnoutState()); 339 } 340 341 /** 342 * Drive the current state of the display from the state of the turnout. 343 * {@inheritDoc} 344 */ 345 @Override 346 public void displayState(int state) { 347 if (getNamedTurnout() == null) { 348 log.debug("Display state {}, disconnected", state); 349 } else { 350 // log.debug("{} displayState {}", getNameString(), _state2nameMap.get(state)); 351 if (isText()) { 352 super.setText(_state2nameMap.get(state)); 353 } 354 if (isIcon()) { 355 NamedIcon icon = getIcon(state); 356 if (icon != null) { 357 super.setIcon(icon); 358 } 359 } 360 } 361 updateSize(); 362 } 363 364 TableItemPanel<Turnout> _itemPanel; 365 366 @Override 367 public boolean setEditItemMenu(JPopupMenu popup) { 368 String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout")); 369 popup.add(new javax.swing.AbstractAction(txt) { 370 @Override 371 public void actionPerformed(ActionEvent e) { 372 editItem(); 373 } 374 }); 375 return true; 376 } 377 378 protected void editItem() { 379 _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"), 380 Bundle.getMessage("BeanNameTurnout"))); 381 _itemPanel = new TableItemPanel<>(_paletteFrame, "Turnout", _iconFamily, 382 PickListModel.turnoutPickModelInstance()); // NOI18N 383 ActionListener updateAction = a -> updateItem(); 384 // duplicate icon map with state names rather than int states and unscaled and unrotated 385 HashMap<String, NamedIcon> strMap = new HashMap<>(); 386 for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) { 387 NamedIcon oldIcon = entry.getValue(); 388 NamedIcon newIcon = cloneIcon(oldIcon, this); 389 newIcon.rotate(0, this); 390 newIcon.scale(1.0, this); 391 newIcon.setRotation(4, this); 392 strMap.put(_state2nameMap.get(entry.getKey()), newIcon); 393 } 394 _itemPanel.init(updateAction, strMap); 395 _itemPanel.setSelection(getTurnout()); 396 initPaletteFrame(_paletteFrame, _itemPanel); 397 } 398 399 void updateItem() { 400 HashMap<Integer, NamedIcon> oldMap = cloneMap(_iconStateMap, this); 401 setTurnout(_itemPanel.getTableSelection().getSystemName()); 402 _iconFamily = _itemPanel.getFamilyName(); 403 HashMap<String, NamedIcon> iconMap = _itemPanel.getIconMap(); 404 if (iconMap != null) { 405 for (Entry<String, NamedIcon> entry : iconMap.entrySet()) { 406 if (log.isDebugEnabled()) { 407 log.debug("key= {}", entry.getKey()); 408 } 409 NamedIcon newIcon = entry.getValue(); 410 NamedIcon oldIcon = oldMap.get(_name2stateMap.get(entry.getKey())); 411 newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this); 412 newIcon.setRotation(oldIcon.getRotation(), this); 413 setIcon(entry.getKey(), newIcon); 414 } 415 } // otherwise retain current map 416 finishItemUpdate(_paletteFrame, _itemPanel); 417 } 418 419 @Override 420 public boolean setEditIconMenu(JPopupMenu popup) { 421 String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout")); 422 popup.add(new javax.swing.AbstractAction(txt) { 423 @Override 424 public void actionPerformed(ActionEvent e) { 425 edit(); 426 } 427 }); 428 return true; 429 } 430 431 @Override 432 protected void edit() { 433 makeIconEditorFrame(this, "Turnout", true, null); // NOI18N 434 _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.turnoutPickModelInstance()); 435 int i = 0; 436 for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) { 437 _iconEditor.setIcon(i++, _state2nameMap.get(entry.getKey()), entry.getValue()); 438 } 439 _iconEditor.makeIconPanel(false); 440 441 // set default icons, then override with this turnout's icons 442 ActionListener addIconAction = a -> updateTurnout(); 443 _iconEditor.complete(addIconAction, true, true, true); 444 _iconEditor.setSelection(getTurnout()); 445 } 446 447 void updateTurnout() { 448 HashMap<Integer, NamedIcon> oldMap = cloneMap(_iconStateMap, this); 449 setTurnout(_iconEditor.getTableSelection().getDisplayName()); 450 Hashtable<String, NamedIcon> iconMap = _iconEditor.getIconMap(); 451 452 for (Entry<String, NamedIcon> entry : iconMap.entrySet()) { 453 if (log.isDebugEnabled()) { 454 log.debug("key= {}", entry.getKey()); 455 } 456 NamedIcon newIcon = entry.getValue(); 457 NamedIcon oldIcon = oldMap.get(_name2stateMap.get(entry.getKey())); 458 newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this); 459 newIcon.setRotation(oldIcon.getRotation(), this); 460 setIcon(entry.getKey(), newIcon); 461 } 462 _iconEditorFrame.dispose(); 463 _iconEditorFrame = null; 464 _iconEditor = null; 465 invalidate(); 466 } 467 468 public boolean buttonLive() { 469 if (namedTurnout == null) { 470 log.error("No turnout connection, can't process click"); 471 return false; 472 } 473 return true; 474 } 475 476 @Override 477 public void doMousePressed(JmriMouseEvent e) { 478 if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) { 479 // this is a momentary button press 480 getTurnout().setCommandedState(Turnout.THROWN); 481 } 482 super.doMousePressed(e); 483 } 484 485 @Override 486 public void doMouseReleased(JmriMouseEvent e) { 487 if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) { 488 // this is a momentary button release 489 getTurnout().setCommandedState(Turnout.CLOSED); 490 } 491 super.doMouseReleased(e); 492 } 493 494 @Override 495 public void doMouseClicked(JmriMouseEvent e) { 496 if (!_editor.getFlag(Editor.OPTION_CONTROLS, isControlling())) { 497 return; 498 } 499 if (e.isMetaDown() || e.isAltDown() || !buttonLive() || getMomentary()) { 500 return; 501 } 502 503 if (getDirectControl() && !isEditable()) { 504 getTurnout().setCommandedState(Turnout.CLOSED); 505 } else { 506 alternateOnClick(); 507 } 508 } 509 510 void alternateOnClick() { 511 if (getTurnout().getKnownState() == Turnout.CLOSED) { // if clear known state, set to opposite 512 getTurnout().setCommandedState(Turnout.THROWN); 513 } else if (getTurnout().getKnownState() == Turnout.THROWN) { 514 getTurnout().setCommandedState(Turnout.CLOSED); 515 } else if (getTurnout().getCommandedState() == Turnout.CLOSED) { 516 getTurnout().setCommandedState(Turnout.THROWN); // otherwise, set to opposite of current commanded state if known 517 } else { 518 getTurnout().setCommandedState(Turnout.CLOSED); // just force closed. 519 } 520 } 521 522 @Override 523 public void dispose() { 524 if (namedTurnout != null) { 525 getTurnout().removePropertyChangeListener(this); 526 } 527 namedTurnout = null; 528 _iconStateMap = null; 529 _name2stateMap = null; 530 _state2nameMap = null; 531 532 super.dispose(); 533 } 534 535 protected HashMap<Integer, NamedIcon> cloneMap(HashMap<Integer, NamedIcon> map, 536 TurnoutIcon pos) { 537 HashMap<Integer, NamedIcon> clone = new HashMap<>(); 538 if (map != null) { 539 for (Entry<Integer, NamedIcon> entry : map.entrySet()) { 540 clone.put(entry.getKey(), cloneIcon(entry.getValue(), pos)); 541 if (pos != null) { 542 pos.setIcon(_state2nameMap.get(entry.getKey()), _iconStateMap.get(entry.getKey())); 543 } 544 } 545 } 546 return clone; 547 } 548 549 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TurnoutIcon.class); 550}