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 public String getSensorName(int i) { 117 return entries.get(i).namedSensor.getName(); 118 } 119 120 public NamedIcon getSensorIcon(int i) { 121 return entries.get(i).icon; 122 } 123 124 public String getFamily() { 125 return _iconFamily; 126 } 127 128 public void setFamily(String family) { 129 _iconFamily = family; 130 } 131 132 // display icons 133 String inactiveName = "resources/icons/USS/plate/levers/l-inactive.gif"; 134 NamedIcon inactive = new NamedIcon(inactiveName, inactiveName); 135 136 String inconsistentName = "resources/icons/USS/plate/levers/l-inconsistent.gif"; 137 NamedIcon inconsistent = new NamedIcon(inconsistentName, inconsistentName); 138 139 String unknownName = "resources/icons/USS/plate/levers/l-unknown.gif"; 140 NamedIcon unknown = new NamedIcon(unknownName, unknownName); 141 142 public NamedIcon getInactiveIcon() { 143 return inactive; 144 } 145 146 public void setInactiveIcon(NamedIcon i) { 147 inactive = i; 148 } 149 150 public NamedIcon getInconsistentIcon() { 151 return inconsistent; 152 } 153 154 public void setInconsistentIcon(NamedIcon i) { 155 inconsistent = i; 156 } 157 158 public NamedIcon getUnknownIcon() { 159 return unknown; 160 } 161 162 public void setUnknownIcon(NamedIcon i) { 163 unknown = i; 164 } 165 166 // update icon as state of turnout changes 167 @Override 168 public void propertyChange(java.beans.PropertyChangeEvent e) { 169 if (log.isDebugEnabled()) { 170 String prop = e.getPropertyName(); 171 Sensor sen = (Sensor) e.getSource(); 172 log.debug("property change({}) Sensor state= {} - old= {}, new= {}", 173 prop, sen.getKnownState(), e.getOldValue(), e.getNewValue()); 174 } 175 if (e.getPropertyName().equals("KnownState")) { 176 Rectangle dirty = getBounds(); 177 displayState(); 178 dirty.add(getBounds()); // union with the new bounds, in case displayState resized this icon 179 _editor.repaintTargetPanel(dirty); 180 } 181 } 182 183 @Override 184 @Nonnull 185 public String getTypeString() { 186 return Bundle.getMessage("PositionableType_MultiSensorIcon"); 187 } 188 189 @Override 190 public String getNameString() { 191 StringBuilder name = new StringBuilder(); 192 if ((entries == null) || (entries.size() < 1)) { 193 name.append(Bundle.getMessage("NotConnected")); 194 } else { 195 name.append(entries.get(0).namedSensor.getName()); 196 entries.forEach((entry) -> name.append(",").append(entry.namedSensor.getName())); 197 } 198 return name.toString(); 199 } 200 201 /** 202 * ****** popup AbstractAction.actionPerformed method overrides ******** 203 */ 204 @Override 205 protected void rotateOrthogonal() { 206 for (Entry entry : entries) { 207 NamedIcon icon = entry.icon; 208 icon.setRotation(icon.getRotation() + 1, this); 209 } 210 inactive.setRotation(inactive.getRotation() + 1, this); 211 unknown.setRotation(unknown.getRotation() + 1, this); 212 inconsistent.setRotation(inconsistent.getRotation() + 1, this); 213 displayState(); 214 // bug fix, must repaint icons that have same width and height 215 repaint(); 216 } 217 218 @Override 219 public void setScale(double s) { 220 for (Entry entry : entries) { 221 NamedIcon icon = entry.icon; 222 icon.scale(s, this); 223 } 224 inactive.scale(s, this); 225 unknown.scale(s, this); 226 inconsistent.scale(s, this); 227 displayState(); 228 } 229 230 @Override 231 public void rotate(int deg) { 232 for (Entry entry : entries) { 233 NamedIcon icon = entry.icon; 234 icon.rotate(deg, this); 235 } 236 inactive.rotate(deg, this); 237 unknown.rotate(deg, this); 238 inconsistent.rotate(deg, this); 239 displayState(); 240 } 241 242 @Override 243 public boolean setEditItemMenu(JPopupMenu popup) { 244 String txt = Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor")); 245 popup.add(new javax.swing.AbstractAction(txt) { 246 @Override 247 public void actionPerformed(ActionEvent e) { 248 editItem(); 249 } 250 }); 251 return true; 252 } 253 254 MultiSensorItemPanel _itemPanel; 255 256 protected void editItem() { 257 _paletteFrame = makePaletteFrame(Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor"))); 258 _itemPanel = new MultiSensorItemPanel(_paletteFrame, "MultiSensor", _iconFamily, 259 PickListModel.multiSensorPickModelInstance()); 260 ActionListener updateAction = (ActionEvent a) -> updateItem(); 261 // duplicate _iconMap map with unscaled and unrotated icons 262 HashMap<String, NamedIcon> map = new HashMap<>(); 263 map.put("SensorStateInactive", inactive); 264 map.put("BeanStateInconsistent", inconsistent); 265 map.put("BeanStateUnknown", unknown); 266 for (int i = 0; i < entries.size(); i++) { 267 map.put(MultiSensorItemPanel.getPositionName(i), entries.get(i).icon); 268 } 269 _itemPanel.init(updateAction, map); 270 for (Entry entry : entries) { 271 _itemPanel.setSelection(entry.namedSensor.getBean()); 272 } 273 _itemPanel.setUpDown(getUpDown()); 274 initPaletteFrame(_paletteFrame, _itemPanel); 275 } 276 277 void updateItem() { 278 if (!_itemPanel.oktoUpdate()) { 279 return; 280 } 281 HashMap<String, NamedIcon> iconMap = _itemPanel.getIconMap(); 282 ArrayList<Sensor> selections = _itemPanel.getTableSelections(); 283 setInactiveIcon(new NamedIcon(iconMap.get("SensorStateInactive"))); 284 setInconsistentIcon(new NamedIcon(iconMap.get("BeanStateInconsistent"))); 285 setUnknownIcon(new NamedIcon(iconMap.get("BeanStateUnknown"))); 286 entries = new ArrayList<>(selections.size()); 287 for (int i = 0; i < selections.size(); i++) { 288 addEntry(selections.get(i).getDisplayName(), new NamedIcon(iconMap.get(MultiSensorItemPanel.getPositionName(i)))); 289 } 290 _iconFamily = _itemPanel.getFamilyName(); 291 _itemPanel.clearSelections(); 292 setUpDown(_itemPanel.getUpDown()); 293 finishItemUpdate(_paletteFrame, _itemPanel); 294 } 295 296 @Override 297 public boolean setEditIconMenu(JPopupMenu popup) { 298 String txt = Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor")); 299 popup.add(new AbstractAction(txt) { 300 @Override 301 public void actionPerformed(ActionEvent e) { 302 edit(); 303 } 304 }); 305 return true; 306 } 307 308 @Override 309 protected void edit() { 310 MultiSensorIconAdder iconEditor = new MultiSensorIconAdder("MultiSensor"); 311 makeIconEditorFrame(this, "MultiSensor", false, iconEditor); 312 _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.sensorPickModelInstance()); 313 _iconEditor.setIcon(2, "SensorStateInactive", inactive); 314 _iconEditor.setIcon(0, "BeanStateInconsistent", inconsistent); 315 _iconEditor.setIcon(1, "BeanStateUnknown", unknown); 316 if (_iconEditor instanceof MultiSensorIconAdder) { 317 ((MultiSensorIconAdder) _iconEditor).setMultiIcon(entries); 318 _iconEditor.makeIconPanel(false); 319 320 ActionListener addIconAction = (ActionEvent a) -> updateSensor(); 321 iconEditor.complete(addIconAction, true, true, true); 322 } 323 } 324 325 void updateSensor() { 326 if (_iconEditor instanceof MultiSensorIconAdder) { 327 MultiSensorIconAdder iconEditor = (MultiSensorIconAdder) _iconEditor; 328 setInactiveIcon(iconEditor.getIcon("SensorStateInactive")); 329 setInconsistentIcon(iconEditor.getIcon("BeanStateInconsistent")); 330 setUnknownIcon(iconEditor.getIcon("BeanStateUnknown")); 331 for (Entry entry : entries) { 332 entry.namedSensor.getBean().removePropertyChangeListener(this); 333 } 334 int numPositions = iconEditor.getNumIcons(); 335 entries = new ArrayList<>(numPositions); 336 for (int i = 3; i < numPositions; i++) { 337 NamedIcon icon = iconEditor.getIcon(i); 338 NamedBeanHandle<Sensor> namedSensor = iconEditor.getSensor(i); 339 addEntry(namedSensor, icon); 340 } 341 setUpDown(iconEditor.getUpDown()); 342 } 343 _iconEditorFrame.dispose(); 344 _iconEditorFrame = null; 345 _iconEditor = null; 346 invalidate(); 347 } 348 /** 349 * *********** end popup action methods *************** 350 */ 351 352 int displaying = -1; 353 354 /** 355 * Drive the current state of the display from the state of the turnout. 356 */ 357 public void displayState() { 358 359 updateSize(); 360 361 // run the entries 362 boolean foundActive = false; 363 364 for (int i = 0; i < entries.size(); i++) { 365 Entry e = entries.get(i); 366 367 int state = e.namedSensor.getBean().getKnownState(); 368 369 switch (state) { 370 case Sensor.ACTIVE: 371 if (isText()) { 372 super.setText(Bundle.getMessage("SensorStateActive")); 373 } 374 if (isIcon()) { 375 super.setIcon(e.icon); 376 } 377 foundActive = true; 378 displaying = i; 379 break; // look at the next ones too 380 case Sensor.UNKNOWN: 381 if (isText()) { 382 super.setText(Bundle.getMessage("BeanStateUnknown")); 383 } 384 if (isIcon()) { 385 super.setIcon(unknown); 386 } 387 return; // this trumps all others 388 case Sensor.INCONSISTENT: 389 if (isText()) { 390 super.setText(Bundle.getMessage("BeanStateInconsistent")); 391 } 392 if (isIcon()) { 393 super.setIcon(inconsistent); 394 } 395 break; 396 default: 397 break; 398 } 399 } 400 // loop has gotten to here 401 if (foundActive) { 402 return; // set active 403 } // only case left is all inactive 404 if (isText()) { 405 super.setText(Bundle.getMessage("SensorStateInactive")); 406 } 407 if (isIcon()) { 408 super.setIcon(inactive); 409 } 410 } 411 412 // Use largest size. If icons are not same size, 413 // this can result in drawing artifacts. 414 @Override 415 public int maxHeight() { 416 int size = Math.max( 417 ((inactive != null) ? inactive.getIconHeight() : 0), 418 Math.max((unknown != null) ? unknown.getIconHeight() : 0, 419 (inconsistent != null) ? inconsistent.getIconHeight() : 0) 420 ); 421 if (entries != null) { 422 for (Entry entry : entries) { 423 size = Math.max(size, entry.icon.getIconHeight()); 424 } 425 } 426 return size; 427 } 428 429 // Use largest size. If icons are not same size, 430 // this can result in drawing artifacts. 431 @Override 432 public int maxWidth() { 433 int size = Math.max( 434 ((inactive != null) ? inactive.getIconWidth() : 0), 435 Math.max((unknown != null) ? unknown.getIconWidth() : 0, 436 (inconsistent != null) ? inconsistent.getIconWidth() : 0) 437 ); 438 if (entries != null) { 439 for (Entry entry : entries) { 440 size = Math.max(size, entry.icon.getIconWidth()); 441 } 442 } 443 return size; 444 } 445 446 public void performMouseClicked(JmriMouseEvent e, int xx, int yy) { 447 if (log.isDebugEnabled()) { 448 log.debug("performMouseClicked: location ({}, {}), click from ({}, {}) displaying={}", 449 getX(), getY(), xx, yy, displaying); 450 } 451 if (!buttonLive() || (entries == null || entries.size() < 1)) { 452 if (log.isDebugEnabled()) { 453 log.debug("performMouseClicked: buttonLive={}, entries={}", buttonLive(), entries.size()); 454 } 455 return; 456 } 457 458 // find if we want to increment or decrement 459 // regardless of the zooming scale, (getX(), getY()) is the un-zoomed position in _editor._contents 460 // but the click is at the zoomed position 461 double ratio = _editor.getPaintScale(); 462 boolean dec = false; 463 if (updown) { 464 if ((yy/ratio - getY()) > (double)(maxHeight()) / 2) { 465 dec = true; 466 } 467 } else { 468 if ((xx/ratio - getX()) < (double)(maxWidth()) / 2) { 469 dec = true; 470 } 471 } 472 473 // get new index 474 int next; 475 if (dec) { 476 next = displaying - 1; 477 } else { 478 next = displaying + 1; 479 } 480 if (next < 0) { 481 next = 0; 482 } 483 if (next >= entries.size()) { 484 next = entries.size() - 1; 485 } 486 487 int drop = displaying; 488 if (log.isDebugEnabled()) { 489 log.debug("dec= {} displaying={} next= {}", dec, displaying, next); 490 } 491 try { 492 entries.get(next).namedSensor.getBean().setKnownState(Sensor.ACTIVE); 493 if (drop >= 0 && drop != next) { 494 entries.get(drop).namedSensor.getBean().setKnownState(Sensor.INACTIVE); 495 } 496 } catch (jmri.JmriException ex) { 497 log.error("Click failed to set sensor: ", ex); 498 } 499 } 500 501 boolean buttonLive() { 502 return _editor.getFlag(Editor.OPTION_CONTROLS, isControlling()); 503 } 504 505 @Override 506 public void doMouseClicked(JmriMouseEvent e) { 507 if (!e.isAltDown() && !e.isMetaDown()) { 508 performMouseClicked(e, e.getX(), e.getY()); 509 } 510 } 511 512 @Override 513 public void dispose() { 514 // remove listeners 515 for (Entry entry : entries) { 516 entry.namedSensor.getBean().removePropertyChangeListener(this); 517 } 518 super.dispose(); 519 } 520 521 static class Entry { 522 523 NamedBeanHandle<Sensor> namedSensor; 524 NamedIcon icon; 525 } 526 527 private static final Logger log = LoggerFactory.getLogger(MultiSensorIcon.class); 528 529}