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 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025/** 026 * An icon to display a status of a turnout. 027 * <p> 028 * This responds to only KnownState, leaving CommandedState to some other 029 * graphic representation later. 030 * <p> 031 * A click on the icon will command a state change. Specifically, it will set 032 * the CommandedState to the opposite (THROWN vs CLOSED) of the current 033 * KnownState. 034 * <p> 035 * The default icons are for a left-handed turnout, facing point for east-bound 036 * traffic. 037 * 038 * @author Bob Jacobsen Copyright (c) 2002 039 * @author PeteCressman Copyright (C) 2010, 2011 040 */ 041public class TurnoutIcon extends PositionableIcon implements java.beans.PropertyChangeListener { 042 043 protected HashMap<Integer, NamedIcon> _iconStateMap; // state int to icon 044 protected HashMap<String, Integer> _name2stateMap; // name to state 045 protected HashMap<Integer, String> _state2nameMap; // state to name 046 047 public TurnoutIcon(Editor editor) { 048 // super ctor call to make sure this is an icon label 049 super(new NamedIcon("resources/icons/smallschematics/tracksegments/os-lefthand-east-closed.gif", 050 "resources/icons/smallschematics/tracksegments/os-lefthand-east-closed.gif"), editor); 051 _control = true; 052 setPopupUtility(null); 053 } 054 055 @Override 056 public Positionable deepClone() { 057 TurnoutIcon pos = new TurnoutIcon(_editor); 058 return finishClone(pos); 059 } 060 061 protected Positionable finishClone(TurnoutIcon pos) { 062 pos.setTurnout(getNamedTurnout().getName()); 063 pos._iconStateMap = cloneMap(_iconStateMap, pos); 064 pos.setTristate(getTristate()); 065 pos.setMomentary(getMomentary()); 066 pos.setDirectControl(getDirectControl()); 067 pos._iconFamily = _iconFamily; 068 return super.finishClone(pos); 069 } 070 071 // the associated Turnout object 072 //Turnout turnout = null; 073 private NamedBeanHandle<Turnout> namedTurnout = null; 074 075 /** 076 * Attach a named turnout to this display item. 077 * 078 * @param pName Used as a system/user name to lookup the turnout object 079 */ 080 public void setTurnout(String pName) { 081 if (InstanceManager.getNullableDefault(jmri.TurnoutManager.class) != null) { 082 try { 083 Turnout turnout = InstanceManager.turnoutManagerInstance().provideTurnout(pName); 084 setTurnout(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, turnout)); 085 } catch (IllegalArgumentException ex) { 086 log.error("Turnout '{}' not available, icon won't see changes", pName); 087 } 088 } else { 089 log.error("No TurnoutManager for this protocol, icon won't see changes"); 090 } 091 } 092 093 public void setTurnout(NamedBeanHandle<Turnout> to) { 094 if (namedTurnout != null) { 095 getTurnout().removePropertyChangeListener(this); 096 } 097 namedTurnout = to; 098 if (namedTurnout != null) { 099 _iconStateMap = new HashMap<>(); 100 _name2stateMap = new HashMap<>(); 101 _name2stateMap.put("BeanStateUnknown", Turnout.UNKNOWN); 102 _name2stateMap.put("BeanStateInconsistent", Turnout.INCONSISTENT); 103 _name2stateMap.put("TurnoutStateClosed", Turnout.CLOSED); 104 _name2stateMap.put("TurnoutStateThrown", Turnout.THROWN); 105 _state2nameMap = new HashMap<>(); 106 _state2nameMap.put(Turnout.UNKNOWN, "BeanStateUnknown"); 107 _state2nameMap.put(Turnout.INCONSISTENT, "BeanStateInconsistent"); 108 _state2nameMap.put(Turnout.CLOSED, "TurnoutStateClosed"); 109 _state2nameMap.put(Turnout.THROWN, "TurnoutStateThrown"); 110 displayState(turnoutState()); 111 getTurnout().addPropertyChangeListener(this, namedTurnout.getName(), "Panel Editor Turnout Icon"); 112 } 113 } 114 115 public Turnout getTurnout() { 116 return namedTurnout.getBean(); 117 } 118 119 public NamedBeanHandle<Turnout> getNamedTurnout() { 120 return namedTurnout; 121 } 122 123 @Override 124 public jmri.NamedBean getNamedBean() { 125 return getTurnout(); 126 } 127 128 /** 129 * Place icon by its localized bean state name. 130 * 131 * @param name the state name 132 * @param icon the icon to place 133 */ 134 public void setIcon(String name, NamedIcon icon) { 135 if (log.isDebugEnabled()) { 136 log.debug("setIcon for name \"{}\" state= {}", name, _name2stateMap.get(name)); 137 } 138 _iconStateMap.put(_name2stateMap.get(name), icon); 139 displayState(turnoutState()); 140 } 141 142 /** 143 * Get icon by its localized bean state name. 144 */ 145 @Override 146 public NamedIcon getIcon(String state) { 147 return _iconStateMap.get(_name2stateMap.get(state)); 148 } 149 150 public NamedIcon getIcon(int state) { 151 return _iconStateMap.get(state); 152 } 153 154 @Override 155 public int maxHeight() { 156 int max = 0; 157 for (NamedIcon namedIcon : _iconStateMap.values()) { 158 max = Math.max(namedIcon.getIconHeight(), max); 159 } 160 return max; 161 } 162 163 @Override 164 public int maxWidth() { 165 int max = 0; 166 for (NamedIcon namedIcon : _iconStateMap.values()) { 167 max = Math.max(namedIcon.getIconWidth(), max); 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("CommandedState"))) { 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("KnownState")) { 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 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 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 JCheckBoxMenuItem momentaryItem = new JCheckBoxMenuItem(Bundle.getMessage("Momentary")); 266 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(jmri.Turnout.THROWN); 288 } 289 return true; 290 } 291 292 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 */ 335 @Override 336 public void displayState(int state) { 337 if (getNamedTurnout() == null) { 338 log.debug("Display state {}, disconnected", state); 339 } else { 340 // log.debug("{} displayState {}", getNameString(), _state2nameMap.get(state)); 341 if (isText()) { 342 super.setText(_state2nameMap.get(state)); 343 } 344 if (isIcon()) { 345 NamedIcon icon = getIcon(state); 346 if (icon != null) { 347 super.setIcon(icon); 348 } 349 } 350 } 351 updateSize(); 352 } 353 354 TableItemPanel<Turnout> _itemPanel; 355 356 @Override 357 public boolean setEditItemMenu(JPopupMenu popup) { 358 String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout")); 359 popup.add(new javax.swing.AbstractAction(txt) { 360 @Override 361 public void actionPerformed(ActionEvent e) { 362 editItem(); 363 } 364 }); 365 return true; 366 } 367 368 protected void editItem() { 369 _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"), 370 Bundle.getMessage("BeanNameTurnout"))); 371 _itemPanel = new TableItemPanel<>(_paletteFrame, "Turnout", _iconFamily, 372 PickListModel.turnoutPickModelInstance()); // NOI18N 373 ActionListener updateAction = a -> updateItem(); 374 // duplicate icon map with state names rather than int states and unscaled and unrotated 375 HashMap<String, NamedIcon> strMap = new HashMap<>(); 376 for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) { 377 NamedIcon oldIcon = entry.getValue(); 378 NamedIcon newIcon = cloneIcon(oldIcon, this); 379 newIcon.rotate(0, this); 380 newIcon.scale(1.0, this); 381 newIcon.setRotation(4, this); 382 strMap.put(_state2nameMap.get(entry.getKey()), newIcon); 383 } 384 _itemPanel.init(updateAction, strMap); 385 _itemPanel.setSelection(getTurnout()); 386 initPaletteFrame(_paletteFrame, _itemPanel); 387 } 388 389 void updateItem() { 390 HashMap<Integer, NamedIcon> oldMap = cloneMap(_iconStateMap, this); 391 setTurnout(_itemPanel.getTableSelection().getSystemName()); 392 _iconFamily = _itemPanel.getFamilyName(); 393 HashMap<String, NamedIcon> iconMap = _itemPanel.getIconMap(); 394 if (iconMap != null) { 395 for (Entry<String, NamedIcon> entry : iconMap.entrySet()) { 396 if (log.isDebugEnabled()) { 397 log.debug("key= {}", entry.getKey()); 398 } 399 NamedIcon newIcon = entry.getValue(); 400 NamedIcon oldIcon = oldMap.get(_name2stateMap.get(entry.getKey())); 401 newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this); 402 newIcon.setRotation(oldIcon.getRotation(), this); 403 setIcon(entry.getKey(), newIcon); 404 } 405 } // otherwise retain current map 406 finishItemUpdate(_paletteFrame, _itemPanel); 407 } 408 409 @Override 410 public boolean setEditIconMenu(JPopupMenu popup) { 411 String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout")); 412 popup.add(new javax.swing.AbstractAction(txt) { 413 @Override 414 public void actionPerformed(ActionEvent e) { 415 edit(); 416 } 417 }); 418 return true; 419 } 420 421 @Override 422 protected void edit() { 423 makeIconEditorFrame(this, "Turnout", true, null); // NOI18N 424 _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.turnoutPickModelInstance()); 425 int i = 0; 426 for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) { 427 _iconEditor.setIcon(i++, _state2nameMap.get(entry.getKey()), entry.getValue()); 428 } 429 _iconEditor.makeIconPanel(false); 430 431 // set default icons, then override with this turnout's icons 432 ActionListener addIconAction = a -> updateTurnout(); 433 _iconEditor.complete(addIconAction, true, true, true); 434 _iconEditor.setSelection(getTurnout()); 435 } 436 437 void updateTurnout() { 438 HashMap<Integer, NamedIcon> oldMap = cloneMap(_iconStateMap, this); 439 setTurnout(_iconEditor.getTableSelection().getDisplayName()); 440 Hashtable<String, NamedIcon> iconMap = _iconEditor.getIconMap(); 441 442 for (Entry<String, NamedIcon> entry : iconMap.entrySet()) { 443 if (log.isDebugEnabled()) { 444 log.debug("key= {}", entry.getKey()); 445 } 446 NamedIcon newIcon = entry.getValue(); 447 NamedIcon oldIcon = oldMap.get(_name2stateMap.get(entry.getKey())); 448 newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this); 449 newIcon.setRotation(oldIcon.getRotation(), this); 450 setIcon(entry.getKey(), newIcon); 451 } 452 _iconEditorFrame.dispose(); 453 _iconEditorFrame = null; 454 _iconEditor = null; 455 invalidate(); 456 } 457 458 public boolean buttonLive() { 459 if (namedTurnout == null) { 460 log.error("No turnout connection, can't process click"); 461 return false; 462 } 463 return true; 464 } 465 466 @Override 467 public void doMousePressed(JmriMouseEvent e) { 468 if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) { 469 // this is a momentary button press 470 getTurnout().setCommandedState(jmri.Turnout.THROWN); 471 } 472 super.doMousePressed(e); 473 } 474 475 @Override 476 public void doMouseReleased(JmriMouseEvent e) { 477 if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) { 478 // this is a momentary button release 479 getTurnout().setCommandedState(jmri.Turnout.CLOSED); 480 } 481 super.doMouseReleased(e); 482 } 483 484 @Override 485 public void doMouseClicked(JmriMouseEvent e) { 486 if (!_editor.getFlag(Editor.OPTION_CONTROLS, isControlling())) { 487 return; 488 } 489 if (e.isMetaDown() || e.isAltDown() || !buttonLive() || getMomentary()) { 490 return; 491 } 492 493 if (getDirectControl() && !isEditable()) { 494 getTurnout().setCommandedState(jmri.Turnout.CLOSED); 495 } else { 496 alternateOnClick(); 497 } 498 } 499 500 void alternateOnClick() { 501 if (getTurnout().getKnownState() == jmri.Turnout.CLOSED) { // if clear known state, set to opposite 502 getTurnout().setCommandedState(jmri.Turnout.THROWN); 503 } else if (getTurnout().getKnownState() == jmri.Turnout.THROWN) { 504 getTurnout().setCommandedState(jmri.Turnout.CLOSED); 505 } else if (getTurnout().getCommandedState() == jmri.Turnout.CLOSED) { 506 getTurnout().setCommandedState(jmri.Turnout.THROWN); // otherwise, set to opposite of current commanded state if known 507 } else { 508 getTurnout().setCommandedState(jmri.Turnout.CLOSED); // just force closed. 509 } 510 } 511 512 @Override 513 public void dispose() { 514 if (namedTurnout != null) { 515 getTurnout().removePropertyChangeListener(this); 516 } 517 namedTurnout = null; 518 _iconStateMap = null; 519 _name2stateMap = null; 520 _state2nameMap = null; 521 522 super.dispose(); 523 } 524 525 protected HashMap<Integer, NamedIcon> cloneMap(HashMap<Integer, NamedIcon> map, 526 TurnoutIcon pos) { 527 HashMap<Integer, NamedIcon> clone = new HashMap<>(); 528 if (map != null) { 529 for (Entry<Integer, NamedIcon> entry : map.entrySet()) { 530 clone.put(entry.getKey(), cloneIcon(entry.getValue(), pos)); 531 if (pos != null) { 532 pos.setIcon(_state2nameMap.get(entry.getKey()), _iconStateMap.get(entry.getKey())); 533 } 534 } 535 } 536 return clone; 537 } 538 539 private final static Logger log = LoggerFactory.getLogger(TurnoutIcon.class); 540}