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