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 private 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.setLineWidth(_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 /** 283 * The custom paint method paints a border outside of the bounds. 284 * {@inheritDoc } 285 */ 286 @Override 287 public int maxWidth() { 288 return getWidth() + 2 * getLineWidth(); // border on both sides 289 } 290 291 /** 292 * The custom paint method paints a border outside of the bounds. 293 * {@inheritDoc } 294 */ 295 @Override 296 public int maxHeight() { 297 return getHeight() + 2 * getLineWidth(); // border on both sides 298 } 299 300 @Override 301 public boolean showPopUp(JPopupMenu popup) { 302 return false; 303 } 304 305 /** 306 * Add a rotation menu to the contextual menu for this PostionableShape. 307 * 308 * @param popup the menu to add a rotation menu to 309 * @return true if rotation menu is added; false otherwise 310 */ 311 @Override 312 public boolean setRotateMenu(JPopupMenu popup) { 313 if (super.getDisplayLevel() > Editor.BKG) { 314 popup.add(jmri.jmrit.display.CoordinateEdit.getRotateEditAction(this)); 315 return true; 316 } 317 return false; 318 } 319 320 @Override 321 public boolean setScaleMenu(JPopupMenu popup) { 322 return false; 323 } 324 325 @Override 326 public int getDegrees() { 327 return _degrees; 328 } 329 330 @Override 331 public void propertyChange(java.beans.PropertyChangeEvent evt) { 332 if (log.isDebugEnabled()) { 333 log.debug("property change: \"{}\"= {} for {} {}", 334 evt.getPropertyName(), evt.getNewValue(), getSensorName(), hashCode()); 335 } 336 if (!_editor.isEditable()) { 337 if ( Sensor.PROPERTY_KNOWN_STATE.equals(evt.getPropertyName())) { 338 switch ((Integer) evt.getNewValue()) { 339 case Sensor.ACTIVE: 340 if (_doHide) { 341 setVisible(true); 342 } else { 343 super.setDisplayLevel(_changeLevel); 344 setVisible(true); 345 } 346 break; 347 case Sensor.INACTIVE: 348 if (_doHide) { 349 setVisible(false); 350 } else { 351 super.setDisplayLevel(_saveLevel); 352 setVisible(true); 353 } 354 break; 355 default: 356 super.setDisplayLevel(_saveLevel); 357 setVisible(true); 358 break; 359 } 360 ((ControlPanelEditor) _editor).mouseMoved(new JmriMouseEvent(this, 361 JmriMouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 362 0, getX(), getY(), 0, false)); 363 repaint(); 364 _editor.getTargetPanel().revalidate(); 365 } 366 } else { 367 super.setDisplayLevel(_saveLevel); 368 setVisible(true); 369 } 370 if (log.isDebugEnabled()) { 371 log.debug("_changeLevel= {} _saveLevel= {} displayLevel= {} _doHide= {} visible= {}", 372 _changeLevel, _saveLevel, getDisplayLevel(), _doHide, isVisible()); 373 } 374 } 375 376 @Override 377 // changing the level from regular popup 378 public void setDisplayLevel(int l) { 379 super.setDisplayLevel(l); 380 _saveLevel = l; 381 } 382 383 /** 384 * Attach a named sensor to a PositionableShape. 385 * 386 * @param pName Used as a system/user name to lookup the sensor object 387 * @return errror message or null 388 */ 389 public String setControlSensor(String pName) { 390 String msg = null; 391 log.debug("setControlSensor: name= {}", pName); 392 if (pName == null || pName.trim().isEmpty()) { 393 removeListener(); 394 _controlSensor = null; 395 return null; 396 } 397// _saveLevel = super.getDisplayLevel(); 398 Optional<SensorManager> sensorManager = InstanceManager.getOptionalDefault(SensorManager.class); 399 if (sensorManager.isPresent()) { 400 Sensor sensor = sensorManager.get().getSensor(pName); 401 Optional<NamedBeanHandleManager> nbhm = InstanceManager.getOptionalDefault(NamedBeanHandleManager.class); 402 if (sensor != null) { 403 nbhm.ifPresent(namedBeanHandleManager -> _controlSensor = namedBeanHandleManager.getNamedBeanHandle(pName, sensor)); 404 } else { 405 msg = Bundle.getMessage("badSensorName", pName); // NOI18N 406 } 407 } else { 408 msg = Bundle.getMessage("NoSensorManager"); // NOI18N 409 } 410 if (msg != null) { 411 log.warn("{} for {} sensor", msg, Bundle.getMessage("VisibleSensor")); 412 } 413 return msg; 414 } 415 416 public Sensor getControlSensor() { 417 if (_controlSensor == null) { 418 return null; 419 } 420 return _controlSensor.getBean(); 421 } 422 423 protected String getSensorName() { 424 Sensor s = getControlSensor(); 425 if (s != null) { 426 return s.getDisplayName(); 427 } 428 return null; 429 } 430 431 public NamedBeanHandle<Sensor> getControlSensorHandle() { 432 return _controlSensor; 433 } 434 435 public boolean isHideOnSensor() { 436 return _doHide; 437 } 438 public void setHide(boolean h) { 439 _doHide = h; 440 if (_doHide) { 441 _changeLevel = _saveLevel; 442 } 443 } 444 445 public int getChangeLevel() { 446 return _changeLevel; 447 } 448 449 public void setChangeLevel(int l) { 450 _changeLevel = l; 451 } 452 453 public void setListener() { 454 if (_controlSensor != null) { 455 getControlSensor().addPropertyChangeListener(this, getSensorName(), "PositionalShape"); 456 } 457 } 458 459 /* 460 * Remove listen, if any, but retain handle. 461 */ 462 protected void removeListener() { 463 if (_controlSensor != null) { 464 getControlSensor().removePropertyChangeListener(this); 465 } 466 } 467 468 abstract protected DrawFrame makeEditFrame(boolean create); 469 470 protected DrawFrame getEditFrame() { 471 return _editFrame; 472 } 473 474 public void removeHandles() { 475 _handles = null; 476 invalidateShape(); 477 repaint(); 478 } 479 480 public void drawHandles() { 481 _handles = new Rectangle[4]; 482 int rectSize = 2 * SIZE; 483 if (_width < rectSize || _height < rectSize) { 484 rectSize = Math.min(_width, _height); 485 } 486 _handles[RIGHT] = new Rectangle(_width - rectSize, _height / 2 - rectSize / 2, rectSize, rectSize); 487 _handles[LEFT] = new Rectangle(0, _height / 2 - rectSize / 2, rectSize, rectSize); 488 _handles[TOP] = new Rectangle(_width / 2 - rectSize / 2, 0, rectSize, rectSize); 489 _handles[BOTTOM] = new Rectangle(_width / 2 - rectSize / 2, _height - rectSize, rectSize, rectSize); 490 } 491 492 public Point getInversePoint(int x, int y) throws java.awt.geom.NoninvertibleTransformException { 493 if (_transform != null) { 494 java.awt.geom.AffineTransform t = _transform.createInverse(); 495 float[] pt = new float[2]; 496 pt[0] = x; 497 pt[1] = y; 498 t.transform(pt, 0, pt, 0, 1); 499 return new Point(Math.round(pt[0]), Math.round(pt[1])); 500 } 501 return new Point(x, y); 502 } 503 504 @Override 505 public void doMousePressed(JmriMouseEvent event) { 506 _hitIndex = -1; 507 if (!_editor.isEditable()) { 508 return; 509 } 510 if (_handles != null) { 511 _lastX = event.getX(); 512 _lastY = event.getY(); 513 int x = _lastX - getX(); 514 int y = _lastY - getY(); 515 Point pt; 516 try { 517 pt = getInversePoint(x, y); 518 } catch (java.awt.geom.NoninvertibleTransformException nte) { 519 log.error("Can't locate Hit Rectangles {}", nte.getMessage()); 520 return; 521 } 522 for (int i = 0; i < _handles.length; i++) { 523 if (_handles[i] != null && _handles[i].contains(pt.x, pt.y)) { 524 _hitIndex = i; 525 } 526 } 527 log.debug("doMousePressed _hitIndex= {}", _hitIndex); 528 } 529 } 530 531 protected boolean doHandleMove(JmriMouseEvent event) { 532 if (_hitIndex >= 0 && _editor.isEditable()) { 533 int deltaX = event.getX() - _lastX; 534 int deltaY = event.getY() - _lastY; 535 int height = _height; 536 int width = _width; 537 switch (_hitIndex) { 538 case TOP: 539 if (_height - deltaY > SIZE) { 540 height = _height - deltaY; 541 _editor.moveItem(this, 0, deltaY); 542 } else { 543 height = SIZE; 544 } 545 setHeight(height); 546 break; 547 case RIGHT: 548 width = Math.max(SIZE, _width + deltaX); 549 setWidth(width); 550 break; 551 case BOTTOM: 552 height = Math.max(SIZE, _height + deltaY); 553 setHeight(height); 554 break; 555 case LEFT: 556 if (_width - deltaX > SIZE) { 557 width = Math.max(SIZE, _width - deltaX); 558 _editor.moveItem(this, deltaX, 0); 559 } else { 560 width = SIZE; 561 } 562 setWidth(width); 563 break; 564 default: 565 log.warn("Unhandled dir: {}", _hitIndex); 566 break; 567 } 568 if (_editFrame != null) { 569 _editFrame.setDisplayWidth(_width); 570 _editFrame.setDisplayHeight(_height); 571 } 572 invalidateShape(); 573 updateSize(); 574 drawHandles(); 575 repaint(); 576 _lastX = event.getX(); 577 _lastY = event.getY(); 578 log.debug("doHandleMove _hitIndex= {}", _hitIndex); 579 return true; 580 } 581 return false; 582 } 583 584 private final static Logger log = LoggerFactory.getLogger(PositionableShape.class); 585}