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}