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