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}