001package jmri.jmrit.display.controlPanelEditor.shape; 002 003import java.awt.BasicStroke; 004import java.awt.Color; 005import java.awt.Dimension; 006import java.awt.Graphics; 007import java.awt.Graphics2D; 008import java.awt.Point; 009import java.awt.Rectangle; 010import java.awt.RenderingHints; 011import java.awt.Shape; 012import java.awt.geom.AffineTransform; 013import java.awt.geom.PathIterator; 014import java.beans.PropertyChangeListener; 015import java.util.Optional; 016 017import javax.annotation.Nonnull; 018import javax.swing.JPopupMenu; 019 020import jmri.InstanceManager; 021import jmri.NamedBeanHandle; 022import jmri.NamedBeanHandleManager; 023import jmri.Sensor; 024import jmri.SensorManager; 025import jmri.jmrit.display.Editor; 026import jmri.jmrit.display.Positionable; 027import jmri.jmrit.display.PositionableJComponent; 028import jmri.jmrit.display.controlPanelEditor.ControlPanelEditor; 029import jmri.util.SystemType; 030import jmri.util.swing.JmriMouseEvent; 031 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035/** 036 * PositionableShape is item drawn by java.awt.Graphics2D. 037 * 038 * @author Pete Cressman Copyright (c) 2012 039 */ 040public abstract class PositionableShape extends PositionableJComponent implements PropertyChangeListener { 041 042 private Shape _shape; 043 protected Color _lineColor = Color.black; 044 protected Color _fillColor = new Color(255, 255, 255, 0); 045 protected int _lineWidth = 1; 046 private int _degrees; 047 protected AffineTransform _transform; 048 private NamedBeanHandle<Sensor> _controlSensor = null; 049 private int _saveLevel = ControlPanelEditor.ICONS; // default level set in popup 050 private int _changeLevel = 5; 051 private boolean _doHide; // whether sensor controls show/hide or change level 052 // GUI resizing params 053 private Rectangle[] _handles; 054 protected int _hitIndex = -1; // dual use! also is index of polygon's vertices 055 protected int _lastX; 056 protected int _lastY; 057 // params for shape's bounding box 058 protected int _width; 059 protected int _height; 060 061 protected DrawFrame _editFrame; 062 063 static final int TOP = 0; 064 static final int RIGHT = 1; 065 static final int BOTTOM = 2; 066 static final int LEFT = 3; 067 static final int SIZE = 4; 068 069 public PositionableShape(Editor editor) { 070 super(editor); 071 super.setName("Graphic"); // NOI18N 072 super.setShowToolTip(false); 073 super.setDisplayLevel(ControlPanelEditor.ICONS); 074 } 075 076 public PositionableShape(Editor editor, @Nonnull Shape shape) { 077 this(editor); 078 PositionableShape.this.setShape(shape); 079 } 080 081 public PathIterator getPathIterator(AffineTransform at) { 082 return getShape().getPathIterator(at); 083 } 084 085 protected void setShape(@Nonnull Shape s) { 086 _shape = s; 087 } 088 089 @Nonnull 090 protected Shape getShape() { 091 if (_shape == null) { 092 _shape = makeShape(); 093 } 094 return _shape; 095 } 096 097 public AffineTransform getTransform() { 098 return _transform; 099 } 100 101 public void setWidth(int w) { 102 _width = Math.max(w, SIZE); 103 invalidateShape(); 104 } 105 106 public void setHeight(int h) { 107 _height = Math.max(h, SIZE); 108 invalidateShape(); 109 } 110 111 @Override 112 public int getHeight() { 113 return _height; 114 } 115 116 @Override 117 public int getWidth() { 118 return _width; 119 } 120 121 /** 122 * Create the shape returned by {@link #getShape()}. 123 * 124 * @return the created shape 125 */ 126 @Nonnull 127 protected abstract Shape makeShape(); 128 129 /** 130 * Force the shape to be regenerated next time it is needed. 131 */ 132 protected void invalidateShape() { 133 _shape = null; 134 } 135 136 public void setLineColor(Color c) { 137 if (c != null) { 138 _lineColor = c; 139 } 140 invalidateShape(); 141 } 142 143 public Color getLineColor() { 144 return _lineColor; 145 } 146 147 public void setFillColor(Color c) { 148 if (c != null) { 149 _fillColor = c; 150 } 151 invalidateShape(); 152 } 153 154 public Color getFillColor() { 155 return _fillColor; 156 } 157 158 public void setLineWidth(int w) { 159 _lineWidth = w; 160 invalidateShape(); 161 } 162 163 public int getLineWidth() { 164 return _lineWidth; 165 } 166 167 @Override 168 public void rotate(int deg) { 169 _degrees = deg % 360; 170 if (_degrees == 0) { 171 _transform = null; 172 } else { 173 double rad = Math.toRadians(_degrees); 174 _transform = new AffineTransform(); 175 // use bit shift to avoid SpotBugs paranoia 176 _transform.setToRotation(rad, (_width >>> 1), (_height >>> 1)); 177 } 178 updateSize(); 179 } 180 181 @Override 182 public void paint(Graphics g) { 183 if (!getEditor().isEditable() && !isVisible()) { 184 return; 185 } 186 if (!(g instanceof Graphics2D)) { 187 return; 188 } 189 Graphics2D g2d = (Graphics2D) g; 190 191 // set antialiasing hint for macOS and Windows 192 // note: antialiasing has performance problems on constrained systems 193 // like the Raspberry Pi, assuming Linux variants are constrained 194 if (SystemType.isMacOSX() || SystemType.isWindows()) { 195 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, 196 RenderingHints.VALUE_RENDER_QUALITY); 197 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 198 RenderingHints.VALUE_ANTIALIAS_ON); 199 g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, 200 RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); 201 // Turned off due to poor performance, see Issue #3850 and PR #3855 for background 202 // g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 203 // RenderingHints.VALUE_INTERPOLATION_BICUBIC); 204 } 205 206 g2d.setClip(null); 207 if (_transform != null) { 208 g2d.transform(_transform); 209 } 210 if (_fillColor != null) { 211 g2d.setColor(_fillColor); 212 g2d.fill(getShape()); 213 } 214 if (_lineColor != null) { 215 BasicStroke stroke = new BasicStroke(_lineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10f); 216 g2d.setColor(_lineColor); 217 g2d.setStroke(stroke); 218 g2d.draw(getShape()); 219 } 220 paintHandles(g2d); 221 } 222 223 protected void paintHandles(Graphics2D g2d) { 224 if (_editor.isEditable() && _handles != null) { 225 g2d.setColor(Editor.HIGHLIGHT_COLOR); 226 g2d.setStroke(new java.awt.BasicStroke(2.0f)); 227 Rectangle r = getBounds(); 228 r.x = -_lineWidth / 2; 229 r.y = -_lineWidth / 2; 230 r.width += _lineWidth; 231 r.height += _lineWidth; 232 g2d.draw(r); 233 // g2d.fill(r); 234 for (Rectangle handle : _handles) { 235 if (handle != null) { 236 g2d.setColor(Color.RED); 237 g2d.fill(handle); 238 g2d.setColor(Editor.HIGHLIGHT_COLOR); 239 g2d.draw(handle); 240 } 241 } 242 } 243 } 244 245 @Override 246 public abstract Positionable deepClone(); 247 248 protected Positionable finishClone(PositionableShape pos) { 249 pos._lineWidth = _lineWidth; 250 if (_fillColor != null) { 251 pos._fillColor = 252 new Color(_fillColor.getRed(), _fillColor.getGreen(), _fillColor.getBlue(), _fillColor.getAlpha()); 253 } 254 if (_lineColor != null) { 255 pos._lineColor = 256 new Color(_lineColor.getRed(), _lineColor.getGreen(), _lineColor.getBlue(), _lineColor.getAlpha()); 257 } 258 pos._doHide = _doHide; 259 pos._changeLevel = _changeLevel; 260 pos.setControlSensor(getSensorName()); 261 pos.setWidth(_width); 262 pos.setHeight(_height); 263 pos.invalidateShape(); 264 pos.rotate(getDegrees()); // recreates invalidated shape 265 return super.finishClone(pos); 266 } 267 268 @Override 269 public Dimension getSize(Dimension rv) { 270 return new Dimension(maxWidth(), maxHeight()); 271 } 272 273 @Override 274 public void updateSize() { 275 Rectangle r = getShape().getBounds(); 276 setWidth(r.width); 277 setHeight(r.height); 278 setSize(r.width, r.height); 279 getEditor().repaint(); 280 } 281 282 @Override 283 public int maxWidth() { 284 return getSize().width; 285 } 286 287 @Override 288 public int maxHeight() { 289 return getSize().height; 290 } 291 292 @Override 293 public boolean showPopUp(JPopupMenu popup) { 294 return false; 295 } 296 297 /** 298 * Add a rotation menu to the contextual menu for this PostionableShape. 299 * 300 * @param popup the menu to add a rotation menu to 301 * @return true if rotation menu is added; false otherwise 302 */ 303 @Override 304 public boolean setRotateMenu(JPopupMenu popup) { 305 if (super.getDisplayLevel() > Editor.BKG) { 306 popup.add(jmri.jmrit.display.CoordinateEdit.getRotateEditAction(this)); 307 return true; 308 } 309 return false; 310 } 311 312 @Override 313 public boolean setScaleMenu(JPopupMenu popup) { 314 return false; 315 } 316 317 @Override 318 public int getDegrees() { 319 return _degrees; 320 } 321 322 @Override 323 public void propertyChange(java.beans.PropertyChangeEvent evt) { 324 if (log.isDebugEnabled()) { 325 log.debug("property change: \"{}\"= {} for {} {}", 326 evt.getPropertyName(), evt.getNewValue(), getSensorName(), hashCode()); 327 } 328 if (!_editor.isEditable()) { 329 if (evt.getPropertyName().equals("KnownState")) { 330 switch ((Integer) evt.getNewValue()) { 331 case Sensor.ACTIVE: 332 if (_doHide) { 333 setVisible(true); 334 } else { 335 super.setDisplayLevel(_changeLevel); 336 setVisible(true); 337 } 338 break; 339 case Sensor.INACTIVE: 340 if (_doHide) { 341 setVisible(false); 342 } else { 343 super.setDisplayLevel(_saveLevel); 344 setVisible(true); 345 } 346 break; 347 default: 348 super.setDisplayLevel(_saveLevel); 349 setVisible(true); 350 break; 351 } 352 ((ControlPanelEditor) _editor).mouseMoved(new JmriMouseEvent(this, 353 JmriMouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 354 0, getX(), getY(), 0, false)); 355 repaint(); 356 _editor.getTargetPanel().revalidate(); 357 } 358 } else { 359 super.setDisplayLevel(_saveLevel); 360 setVisible(true); 361 } 362 if (log.isDebugEnabled()) { 363 log.debug("_changeLevel= {} _saveLevel= {} displayLevel= {} _doHide= {} visible= {}", 364 _changeLevel, _saveLevel, getDisplayLevel(), _doHide, isVisible()); 365 } 366 } 367 368 @Override 369 // changing the level from regular popup 370 public void setDisplayLevel(int l) { 371 super.setDisplayLevel(l); 372 _saveLevel = l; 373 } 374 375 /** 376 * Attach a named sensor to a PositionableShape. 377 * 378 * @param pName Used as a system/user name to lookup the sensor object 379 * @return errror message or null 380 */ 381 public String setControlSensor(String pName) { 382 String msg = null; 383 log.debug("setControlSensor: name= {}", pName); 384 if (pName == null || pName.trim().isEmpty()) { 385 removeListener(); 386 _controlSensor = null; 387 return null; 388 } 389// _saveLevel = super.getDisplayLevel(); 390 Optional<SensorManager> sensorManager = InstanceManager.getOptionalDefault(SensorManager.class); 391 if (sensorManager.isPresent()) { 392 Sensor sensor = sensorManager.get().getSensor(pName); 393 Optional<NamedBeanHandleManager> nbhm = InstanceManager.getOptionalDefault(NamedBeanHandleManager.class); 394 if (sensor != null) { 395 nbhm.ifPresent(namedBeanHandleManager -> _controlSensor = namedBeanHandleManager.getNamedBeanHandle(pName, sensor)); 396 } else { 397 msg = Bundle.getMessage("badSensorName", pName); // NOI18N 398 } 399 } else { 400 msg = Bundle.getMessage("NoSensorManager"); // NOI18N 401 } 402 if (msg != null) { 403 log.warn("{} for {} sensor", msg, Bundle.getMessage("VisibleSensor")); 404 } 405 return msg; 406 } 407 408 public Sensor getControlSensor() { 409 if (_controlSensor == null) { 410 return null; 411 } 412 return _controlSensor.getBean(); 413 } 414 415 protected String getSensorName() { 416 Sensor s = getControlSensor(); 417 if (s != null) { 418 return s.getDisplayName(); 419 } 420 return null; 421 } 422 423 public NamedBeanHandle<Sensor> getControlSensorHandle() { 424 return _controlSensor; 425 } 426 427 public boolean isHideOnSensor() { 428 return _doHide; 429 } 430 public void setHide(boolean h) { 431 _doHide = h; 432 if (_doHide) { 433 _changeLevel = _saveLevel; 434 } 435 } 436 437 public int getChangeLevel() { 438 return _changeLevel; 439 } 440 441 public void setChangeLevel(int l) { 442 _changeLevel = l; 443 } 444 445 public void setListener() { 446 if (_controlSensor != null) { 447 getControlSensor().addPropertyChangeListener(this, getSensorName(), "PositionalShape"); 448 } 449 } 450 451 /* 452 * Remove listen, if any, but retain handle. 453 */ 454 protected void removeListener() { 455 if (_controlSensor != null) { 456 getControlSensor().removePropertyChangeListener(this); 457 } 458 } 459 460 abstract protected DrawFrame makeEditFrame(boolean create); 461 462 protected DrawFrame getEditFrame() { 463 return _editFrame; 464 } 465 466 public void removeHandles() { 467 _handles = null; 468 invalidateShape(); 469 repaint(); 470 } 471 472 public void drawHandles() { 473 _handles = new Rectangle[4]; 474 int rectSize = 2 * SIZE; 475 if (_width < rectSize || _height < rectSize) { 476 rectSize = Math.min(_width, _height); 477 } 478 _handles[RIGHT] = new Rectangle(_width - rectSize, _height / 2 - rectSize / 2, rectSize, rectSize); 479 _handles[LEFT] = new Rectangle(0, _height / 2 - rectSize / 2, rectSize, rectSize); 480 _handles[TOP] = new Rectangle(_width / 2 - rectSize / 2, 0, rectSize, rectSize); 481 _handles[BOTTOM] = new Rectangle(_width / 2 - rectSize / 2, _height - rectSize, rectSize, rectSize); 482 } 483 484 public Point getInversePoint(int x, int y) throws java.awt.geom.NoninvertibleTransformException { 485 if (_transform != null) { 486 java.awt.geom.AffineTransform t = _transform.createInverse(); 487 float[] pt = new float[2]; 488 pt[0] = x; 489 pt[1] = y; 490 t.transform(pt, 0, pt, 0, 1); 491 return new Point(Math.round(pt[0]), Math.round(pt[1])); 492 } 493 return new Point(x, y); 494 } 495 496 @Override 497 public void doMousePressed(JmriMouseEvent event) { 498 _hitIndex = -1; 499 if (!_editor.isEditable()) { 500 return; 501 } 502 if (_handles != null) { 503 _lastX = event.getX(); 504 _lastY = event.getY(); 505 int x = _lastX - getX(); 506 int y = _lastY - getY(); 507 Point pt; 508 try { 509 pt = getInversePoint(x, y); 510 } catch (java.awt.geom.NoninvertibleTransformException nte) { 511 log.error("Can't locate Hit Rectangles {}", nte.getMessage()); 512 return; 513 } 514 for (int i = 0; i < _handles.length; i++) { 515 if (_handles[i] != null && _handles[i].contains(pt.x, pt.y)) { 516 _hitIndex = i; 517 } 518 } 519 log.debug("doMousePressed _hitIndex= {}", _hitIndex); 520 } 521 } 522 523 protected boolean doHandleMove(JmriMouseEvent event) { 524 if (_hitIndex >= 0 && _editor.isEditable()) { 525 int deltaX = event.getX() - _lastX; 526 int deltaY = event.getY() - _lastY; 527 int height = _height; 528 int width = _width; 529 switch (_hitIndex) { 530 case TOP: 531 if (_height - deltaY > SIZE) { 532 height = _height - deltaY; 533 _editor.moveItem(this, 0, deltaY); 534 } else { 535 height = SIZE; 536 } 537 setHeight(height); 538 break; 539 case RIGHT: 540 width = Math.max(SIZE, _width + deltaX); 541 setWidth(width); 542 break; 543 case BOTTOM: 544 height = Math.max(SIZE, _height + deltaY); 545 setHeight(height); 546 break; 547 case LEFT: 548 if (_width - deltaX > SIZE) { 549 width = Math.max(SIZE, _width - deltaX); 550 _editor.moveItem(this, deltaX, 0); 551 } else { 552 width = SIZE; 553 } 554 setWidth(width); 555 break; 556 default: 557 log.warn("Unhandled dir: {}", _hitIndex); 558 break; 559 } 560 if (_editFrame != null) { 561 _editFrame.setDisplayWidth(_width); 562 _editFrame.setDisplayHeight(_height); 563 } 564 invalidateShape(); 565 updateSize(); 566 drawHandles(); 567 repaint(); 568 _lastX = event.getX(); 569 _lastY = event.getY(); 570 log.debug("doHandleMove _hitIndex= {}", _hitIndex); 571 return true; 572 } 573 return false; 574 } 575 576 private final static Logger log = LoggerFactory.getLogger(PositionableShape.class); 577}