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