001package jmri.jmrit.display; 002 003import java.awt.Rectangle; 004import java.awt.event.ActionEvent; 005import java.awt.event.ActionListener; 006import java.util.ArrayList; 007import java.util.HashMap; 008import java.util.List; 009 010import javax.annotation.Nonnull; 011import javax.swing.AbstractAction; 012import javax.swing.JPopupMenu; 013 014import jmri.InstanceManager; 015import jmri.NamedBeanHandle; 016import jmri.Sensor; 017import jmri.jmrit.catalog.NamedIcon; 018import jmri.jmrit.display.palette.MultiSensorItemPanel; 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 set of Sensors. 027 * <p> 028 * Each sensor has an associated image. Normally, only one sensor will be active 029 * at a time, and in that case the associated image will be shown. If more than 030 * one is active, one of the corresponding images will be shown, but which one 031 * is not guaranteed. 032 * 033 * @author Bob Jacobsen Copyright (C) 2001, 2007 034 */ 035public class MultiSensorIcon extends PositionableLabel implements java.beans.PropertyChangeListener { 036 037 String _iconFamily; 038 039 public MultiSensorIcon(Editor editor) { 040 // super ctor call to make sure this is an icon label 041 super(new NamedIcon("resources/icons/smallschematics/tracksegments/circuit-error.gif", 042 "resources/icons/smallschematics/tracksegments/circuit-error.gif"), editor); 043 _control = true; 044 displayState(); 045 setPopupUtility(null); 046 } 047 048 boolean updown = false; 049 050 // if not updown, is rightleft 051 public void setUpDown(boolean b) { 052 updown = b; 053 } 054 055 public boolean getUpDown() { 056 return updown; 057 } 058 059 ArrayList<Entry> entries = new ArrayList<>(); 060 061 @Override 062 public Positionable deepClone() { 063 MultiSensorIcon pos = new MultiSensorIcon(_editor); 064 return finishClone(pos); 065 } 066 067 protected Positionable finishClone(MultiSensorIcon pos) { 068 pos.setInactiveIcon(cloneIcon(getInactiveIcon(), pos)); 069 pos.setInconsistentIcon(cloneIcon(getInconsistentIcon(), pos)); 070 pos.setUnknownIcon(cloneIcon(getUnknownIcon(), pos)); 071 for (int i = 0; i < entries.size(); i++) { 072 pos.addEntry(getSensorName(i), cloneIcon(getSensorIcon(i), pos)); 073 } 074 return super.finishClone(pos); 075 } 076 077 public void addEntry(NamedBeanHandle<Sensor> sensor, NamedIcon icon) { 078 if (sensor != null) { 079 if (log.isDebugEnabled()) { 080 log.debug("addEntry: sensor= {}", sensor.getName()); 081 } 082 Entry e = new Entry(); 083 sensor.getBean().addPropertyChangeListener(this, sensor.getName(), "MultiSensor Icon"); 084 e.namedSensor = sensor; 085 e.icon = icon; 086 entries.add(e); 087 displayState(); 088 } else { 089 log.error("Sensor not available, icon won't see changes"); 090 } 091 } 092 093 public void addEntry(String pName, NamedIcon icon) { 094 NamedBeanHandle<Sensor> sensor; 095 if (InstanceManager.getNullableDefault(jmri.SensorManager.class) != null) { 096 sensor = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class) 097 .getNamedBeanHandle(pName, InstanceManager.sensorManagerInstance().provideSensor(pName)); 098 addEntry(sensor, icon); 099 } else { 100 log.error("No SensorManager for this protocol, icon won't see changes"); 101 } 102 } 103 104 public int getNumEntries() { 105 return entries.size(); 106 } 107 108 public List<Sensor> getSensors() { 109 ArrayList<Sensor> list = new ArrayList<>(getNumEntries()); 110 for (Entry handle : entries) { 111 list.add(handle.namedSensor.getBean()); 112 } 113 return list; 114 } 115 116 @Override 117 public jmri.NamedBean getNamedBean() { 118 return entries.isEmpty() ? null : entries.get(0).namedSensor.getBean(); 119 } 120 121 public String getSensorName(int i) { 122 return entries.get(i).namedSensor.getName(); 123 } 124 125 public NamedIcon getSensorIcon(int i) { 126 return entries.get(i).icon; 127 } 128 129 public String getFamily() { 130 return _iconFamily; 131 } 132 133 public void setFamily(String family) { 134 _iconFamily = family; 135 } 136 137 // display icons 138 String inactiveName = "resources/icons/USS/plate/levers/l-inactive.gif"; 139 NamedIcon inactive = new NamedIcon(inactiveName, inactiveName); 140 141 String inconsistentName = "resources/icons/USS/plate/levers/l-inconsistent.gif"; 142 NamedIcon inconsistent = new NamedIcon(inconsistentName, inconsistentName); 143 144 String unknownName = "resources/icons/USS/plate/levers/l-unknown.gif"; 145 NamedIcon unknown = new NamedIcon(unknownName, unknownName); 146 147 public NamedIcon getInactiveIcon() { 148 return inactive; 149 } 150 151 public void setInactiveIcon(NamedIcon i) { 152 inactive = i; 153 } 154 155 public NamedIcon getInconsistentIcon() { 156 return inconsistent; 157 } 158 159 public void setInconsistentIcon(NamedIcon i) { 160 inconsistent = i; 161 } 162 163 public NamedIcon getUnknownIcon() { 164 return unknown; 165 } 166 167 public void setUnknownIcon(NamedIcon i) { 168 unknown = i; 169 } 170 171 // update icon as state of turnout changes 172 @Override 173 public void propertyChange(java.beans.PropertyChangeEvent e) { 174 if (log.isDebugEnabled()) { 175 String prop = e.getPropertyName(); 176 Sensor sen = (Sensor) e.getSource(); 177 log.debug("property change({}) Sensor state= {} - old= {}, new= {}", 178 prop, sen.getKnownState(), e.getOldValue(), e.getNewValue()); 179 } 180 if (e.getPropertyName().equals("KnownState")) { 181 Rectangle dirty = getBounds(); 182 displayState(); 183 dirty.add(getBounds()); // union with the new bounds, in case displayState resized this icon 184 _editor.repaintTargetPanel(dirty); 185 } 186 } 187 188 @Override 189 @Nonnull 190 public String getTypeString() { 191 return Bundle.getMessage("PositionableType_MultiSensorIcon"); 192 } 193 194 @Override 195 public String getNameString() { 196 StringBuilder name = new StringBuilder(); 197 if ((entries == null) || (entries.size() < 1)) { 198 name.append(Bundle.getMessage("NotConnected")); 199 } else { 200 name.append(entries.get(0).namedSensor.getName()); 201 entries.forEach((entry) -> name.append(",").append(entry.namedSensor.getName())); 202 } 203 return name.toString(); 204 } 205 206 /** 207 * ****** popup AbstractAction.actionPerformed method overrides ******** 208 */ 209 @Override 210 protected void rotateOrthogonal() { 211 for (Entry entry : entries) { 212 NamedIcon icon = entry.icon; 213 icon.setRotation(icon.getRotation() + 1, this); 214 } 215 inactive.setRotation(inactive.getRotation() + 1, this); 216 unknown.setRotation(unknown.getRotation() + 1, this); 217 inconsistent.setRotation(inconsistent.getRotation() + 1, this); 218 displayState(); 219 // bug fix, must repaint icons that have same width and height 220 repaint(); 221 } 222 223 @Override 224 public void setScale(double s) { 225 for (Entry entry : entries) { 226 NamedIcon icon = entry.icon; 227 icon.scale(s, this); 228 } 229 inactive.scale(s, this); 230 unknown.scale(s, this); 231 inconsistent.scale(s, this); 232 displayState(); 233 } 234 235 @Override 236 public void rotate(int deg) { 237 for (Entry entry : entries) { 238 NamedIcon icon = entry.icon; 239 icon.rotate(deg, this); 240 } 241 inactive.rotate(deg, this); 242 unknown.rotate(deg, this); 243 inconsistent.rotate(deg, this); 244 displayState(); 245 } 246 247 @Override 248 public boolean setEditItemMenu(JPopupMenu popup) { 249 String txt = Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor")); 250 popup.add(new javax.swing.AbstractAction(txt) { 251 @Override 252 public void actionPerformed(ActionEvent e) { 253 editItem(); 254 } 255 }); 256 return true; 257 } 258 259 MultiSensorItemPanel _itemPanel; 260 261 protected void editItem() { 262 _paletteFrame = makePaletteFrame(Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor"))); 263 _itemPanel = new MultiSensorItemPanel(_paletteFrame, "MultiSensor", _iconFamily, 264 PickListModel.multiSensorPickModelInstance()); 265 ActionListener updateAction = (ActionEvent a) -> updateItem(); 266 // duplicate _iconMap map with unscaled and unrotated icons 267 HashMap<String, NamedIcon> map = new HashMap<>(); 268 map.put("SensorStateInactive", inactive); 269 map.put("BeanStateInconsistent", inconsistent); 270 map.put("BeanStateUnknown", unknown); 271 for (int i = 0; i < entries.size(); i++) { 272 map.put(MultiSensorItemPanel.getPositionName(i), entries.get(i).icon); 273 } 274 _itemPanel.init(updateAction, map); 275 for (Entry entry : entries) { 276 _itemPanel.setSelection(entry.namedSensor.getBean()); 277 } 278 _itemPanel.setUpDown(getUpDown()); 279 initPaletteFrame(_paletteFrame, _itemPanel); 280 } 281 282 void updateItem() { 283 if (!_itemPanel.oktoUpdate()) { 284 return; 285 } 286 HashMap<String, NamedIcon> iconMap = _itemPanel.getIconMap(); 287 ArrayList<Sensor> selections = _itemPanel.getTableSelections(); 288 setInactiveIcon(new NamedIcon(iconMap.get("SensorStateInactive"))); 289 setInconsistentIcon(new NamedIcon(iconMap.get("BeanStateInconsistent"))); 290 setUnknownIcon(new NamedIcon(iconMap.get("BeanStateUnknown"))); 291 entries = new ArrayList<>(selections.size()); 292 for (int i = 0; i < selections.size(); i++) { 293 addEntry(selections.get(i).getDisplayName(), new NamedIcon(iconMap.get(MultiSensorItemPanel.getPositionName(i)))); 294 } 295 _iconFamily = _itemPanel.getFamilyName(); 296 _itemPanel.clearSelections(); 297 setUpDown(_itemPanel.getUpDown()); 298 finishItemUpdate(_paletteFrame, _itemPanel); 299 } 300 301 @Override 302 public boolean setEditIconMenu(JPopupMenu popup) { 303 String txt = Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor")); 304 popup.add(new AbstractAction(txt) { 305 @Override 306 public void actionPerformed(ActionEvent e) { 307 edit(); 308 } 309 }); 310 return true; 311 } 312 313 @Override 314 protected void edit() { 315 MultiSensorIconAdder iconEditor = new MultiSensorIconAdder("MultiSensor"); 316 makeIconEditorFrame(this, "MultiSensor", false, iconEditor); 317 _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.sensorPickModelInstance()); 318 _iconEditor.setIcon(2, "SensorStateInactive", inactive); 319 _iconEditor.setIcon(0, "BeanStateInconsistent", inconsistent); 320 _iconEditor.setIcon(1, "BeanStateUnknown", unknown); 321 if (_iconEditor instanceof MultiSensorIconAdder) { 322 ((MultiSensorIconAdder) _iconEditor).setMultiIcon(entries); 323 _iconEditor.makeIconPanel(false); 324 325 ActionListener addIconAction = (ActionEvent a) -> updateSensor(); 326 iconEditor.complete(addIconAction, true, true, true); 327 } 328 } 329 330 void updateSensor() { 331 if (_iconEditor instanceof MultiSensorIconAdder) { 332 MultiSensorIconAdder iconEditor = (MultiSensorIconAdder) _iconEditor; 333 setInactiveIcon(iconEditor.getIcon("SensorStateInactive")); 334 setInconsistentIcon(iconEditor.getIcon("BeanStateInconsistent")); 335 setUnknownIcon(iconEditor.getIcon("BeanStateUnknown")); 336 for (Entry entry : entries) { 337 entry.namedSensor.getBean().removePropertyChangeListener(this); 338 } 339 int numPositions = iconEditor.getNumIcons(); 340 entries = new ArrayList<>(numPositions); 341 for (int i = 3; i < numPositions; i++) { 342 NamedIcon icon = iconEditor.getIcon(i); 343 NamedBeanHandle<Sensor> namedSensor = iconEditor.getSensor(i); 344 addEntry(namedSensor, icon); 345 } 346 setUpDown(iconEditor.getUpDown()); 347 } 348 _iconEditorFrame.dispose(); 349 _iconEditorFrame = null; 350 _iconEditor = null; 351 invalidate(); 352 } 353 /** 354 * *********** end popup action methods *************** 355 */ 356 357 int displaying = -1; 358 359 /** 360 * Drive the current state of the display from the state of the turnout. 361 */ 362 public void displayState() { 363 364 updateSize(); 365 366 // run the entries 367 boolean foundActive = false; 368 369 for (int i = 0; i < entries.size(); i++) { 370 Entry e = entries.get(i); 371 372 int state = e.namedSensor.getBean().getKnownState(); 373 374 switch (state) { 375 case Sensor.ACTIVE: 376 if (isText()) { 377 super.setText(Bundle.getMessage("SensorStateActive")); 378 } 379 if (isIcon()) { 380 super.setIcon(e.icon); 381 } 382 foundActive = true; 383 displaying = i; 384 break; // look at the next ones too 385 case Sensor.UNKNOWN: 386 if (isText()) { 387 super.setText(Bundle.getMessage("BeanStateUnknown")); 388 } 389 if (isIcon()) { 390 super.setIcon(unknown); 391 } 392 return; // this trumps all others 393 case Sensor.INCONSISTENT: 394 if (isText()) { 395 super.setText(Bundle.getMessage("BeanStateInconsistent")); 396 } 397 if (isIcon()) { 398 super.setIcon(inconsistent); 399 } 400 break; 401 default: 402 break; 403 } 404 } 405 // loop has gotten to here 406 if (foundActive) { 407 return; // set active 408 } // only case left is all inactive 409 if (isText()) { 410 super.setText(Bundle.getMessage("SensorStateInactive")); 411 } 412 if (isIcon()) { 413 super.setIcon(inactive); 414 } 415 } 416 417 // Use largest size. If icons are not same size, 418 // this can result in drawing artifacts. 419 @Override 420 public int maxHeight() { 421 int size = Math.max( 422 ((inactive != null) ? inactive.getIconHeight() : 0), 423 Math.max((unknown != null) ? unknown.getIconHeight() : 0, 424 (inconsistent != null) ? inconsistent.getIconHeight() : 0) 425 ); 426 if (entries != null) { 427 for (Entry entry : entries) { 428 size = Math.max(size, entry.icon.getIconHeight()); 429 } 430 } 431 return size; 432 } 433 434 // Use largest size. If icons are not same size, 435 // this can result in drawing artifacts. 436 @Override 437 public int maxWidth() { 438 int size = Math.max( 439 ((inactive != null) ? inactive.getIconWidth() : 0), 440 Math.max((unknown != null) ? unknown.getIconWidth() : 0, 441 (inconsistent != null) ? inconsistent.getIconWidth() : 0) 442 ); 443 if (entries != null) { 444 for (Entry entry : entries) { 445 size = Math.max(size, entry.icon.getIconWidth()); 446 } 447 } 448 return size; 449 } 450 451 public void performMouseClicked(JmriMouseEvent e, int xx, int yy) { 452 if (log.isDebugEnabled()) { 453 log.debug("performMouseClicked: location ({}, {}), click from ({}, {}) displaying={}", 454 getX(), getY(), xx, yy, displaying); 455 } 456 if (!buttonLive() || (entries == null || entries.size() < 1)) { 457 if (log.isDebugEnabled()) { 458 log.debug("performMouseClicked: buttonLive={}, entries={}", buttonLive(), entries.size()); 459 } 460 return; 461 } 462 463 // find if we want to increment or decrement 464 // regardless of the zooming scale, (getX(), getY()) is the un-zoomed position in _editor._contents 465 // but the click is at the zoomed position 466 double ratio = _editor.getPaintScale(); 467 boolean dec = false; 468 if (updown) { 469 if ((yy/ratio - getY()) > (double)(maxHeight()) / 2) { 470 dec = true; 471 } 472 } else { 473 if ((xx/ratio - getX()) < (double)(maxWidth()) / 2) { 474 dec = true; 475 } 476 } 477 478 // get new index 479 int next; 480 if (dec) { 481 next = displaying - 1; 482 } else { 483 next = displaying + 1; 484 } 485 if (next < 0) { 486 next = 0; 487 } 488 if (next >= entries.size()) { 489 next = entries.size() - 1; 490 } 491 492 int drop = displaying; 493 if (log.isDebugEnabled()) { 494 log.debug("dec= {} displaying={} next= {}", dec, displaying, next); 495 } 496 try { 497 entries.get(next).namedSensor.getBean().setKnownState(Sensor.ACTIVE); 498 if (drop >= 0 && drop != next) { 499 entries.get(drop).namedSensor.getBean().setKnownState(Sensor.INACTIVE); 500 } 501 } catch (jmri.JmriException ex) { 502 log.error("Click failed to set sensor: ", ex); 503 } 504 } 505 506 boolean buttonLive() { 507 return _editor.getFlag(Editor.OPTION_CONTROLS, isControlling()); 508 } 509 510 @Override 511 public void doMouseClicked(JmriMouseEvent e) { 512 if (!e.isAltDown() && !e.isMetaDown()) { 513 performMouseClicked(e, e.getX(), e.getY()); 514 } 515 } 516 517 @Override 518 public void dispose() { 519 // remove listeners 520 for (Entry entry : entries) { 521 entry.namedSensor.getBean().removePropertyChangeListener(this); 522 } 523 super.dispose(); 524 } 525 526 static class Entry { 527 528 NamedBeanHandle<Sensor> namedSensor; 529 NamedIcon icon; 530 } 531 532 private static final Logger log = LoggerFactory.getLogger(MultiSensorIcon.class); 533 534}