001package jmri.jmrit.display.layoutEditor;
002
003import java.awt.*;
004import java.awt.event.MouseEvent;
005import java.awt.geom.*;
006import java.util.List;
007import java.util.*;
008
009import javax.annotation.CheckForNull;
010import javax.annotation.Nonnull;
011import javax.swing.*;
012
013import jmri.JmriException;
014import jmri.Turnout;
015import jmri.util.*;
016
017/**
018 * MVC View component abstract base for the LayoutTrack hierarchy.
019 * <p>
020 * This contains the display information, including screen geometry, for a
021 * LayoutEditor panel. The geometry/connectivity information is held in
022 * {@link LayoutTrack} subclasses.
023 * <ul>
024 * <li>Position(s) of the screen icons and its parts, typically the center;
025 * scaling and translation; size and bounds
026 * <li>Line colors
027 * <li>Flipped status; drawing details like bezier curve points
028 * <li>Various decorations: arrows, tunnels, bridges
029 * <li>Hidden status
030 * </ul>
031 *
032 * @author Bob Jacobsen Copyright (c) 2020
033 *
034 */
035abstract public class LayoutTrackView {
036
037    /**
038     * Constructor method.
039     *
040     * @param track        the layout track to view
041     * @param layoutEditor the panel in which to place the view
042     */
043    public LayoutTrackView(@Nonnull LayoutTrack track, @Nonnull LayoutEditor layoutEditor) {
044        this.layoutTrack = track;
045        this.layoutEditor = layoutEditor;
046    }
047
048    /**
049     * constructor method
050     *
051     * @param track        the track to view
052     * @param c            display location
053     * @param layoutEditor for reference to tools
054     */
055    public LayoutTrackView(@Nonnull LayoutTrack track, @Nonnull Point2D c, @Nonnull LayoutEditor layoutEditor) {
056        this.layoutTrack = track;
057        this.layoutEditor = layoutEditor;
058        this.center = c;
059    }
060
061    final private LayoutTrack layoutTrack;
062
063    final protected LayoutEditor layoutEditor;
064
065    // Accessor Methods
066
067    @Nonnull
068    final public String getId() {  // temporary Id vs name; is one for the View?
069        return layoutTrack.getId();
070    }
071
072    @Nonnull
073    final public String getName() {
074        return layoutTrack.getName();
075    }
076
077    final protected void setIdent(@Nonnull String ident) {
078        layoutTrack.setIdent(ident);
079    }
080
081    // temporary accessor?  Or is this a long term thing?
082    // @Nonnull temporary until we gigure out if can be null or not
083    public LayoutTrack getLayoutTrack() {
084        return layoutTrack;
085    }
086
087    /**
088     * Set center coordinates
089     *
090     * @return The center coordinates
091     */
092    public Point2D getCoordsCenter() { // should be final for efficiency, temporary not to allow redirction overrides.
093        return center;
094    }
095
096    /**
097     * Set center coordinates.
098     * <p>
099     * Some subtypes may reimplement this is "center" is a more complicated
100     * idea, i.e. for Bezier curves
101     *
102     * @param p the coordinates to set
103     */
104    public void setCoordsCenter(@Nonnull Point2D p) {  // temporary = want to make protected after migration
105        center = p;
106    }
107
108    private Point2D center = new Point2D.Double(50.0, 50.0);
109
110    /**
111     * @return true if this track segment has decorations
112     */
113    public boolean hasDecorations() {
114        return false;
115    }
116
117    /**
118     * Get current decorations
119     *
120     * @return the decorations
121     */
122    public Map<String, String> getDecorations() {
123        return decorations;
124    }
125
126    /**
127     * Set new decorations
128     *
129     * This is a complete replacement of the decorations, not an appending.
130     *
131     * @param decorations A map from strings ("arrow", "bridge", "bumper",..) to
132     *                    specific value strings ("single", "entry;right", ),
133     *                    perhaps including multiple values separated by
134     *                    semicolons.
135     */
136    public void setDecorations(Map<String, String> decorations) {
137        this.decorations = decorations;
138    }
139    protected Map<String, String> decorations = null;
140
141    /**
142     * convenience method for accessing...
143     *
144     * @return the layout editor's toolbar panel
145     */
146    @Nonnull
147    final public LayoutEditorToolBarPanel getLayoutEditorToolBarPanel() {
148        return layoutEditor.getLayoutEditorToolBarPanel();
149    }
150
151    // these are convenience methods to return circles & rectangle used to draw onscreen
152    //
153    // compute the control point rect at inPoint; use the turnout circle size
154    final public Ellipse2D trackEditControlCircleAt(@Nonnull Point2D inPoint) {
155        return trackControlCircleAt(inPoint);
156    }
157
158    // compute the turnout circle at inPoint (used for drawing)
159    final public Ellipse2D trackControlCircleAt(@Nonnull Point2D inPoint) {
160        return new Ellipse2D.Double(inPoint.getX() - layoutEditor.circleRadius,
161                inPoint.getY() - layoutEditor.circleRadius,
162                layoutEditor.circleDiameter, layoutEditor.circleDiameter);
163    }
164
165    // compute the turnout circle control rect at inPoint
166    final public Rectangle2D trackControlCircleRectAt(@Nonnull Point2D inPoint) {
167        return new Rectangle2D.Double(inPoint.getX() - layoutEditor.circleRadius,
168                inPoint.getY() - layoutEditor.circleRadius,
169                layoutEditor.circleDiameter, layoutEditor.circleDiameter);
170    }
171
172    final protected Color getColorForTrackBlock(
173            @CheckForNull LayoutBlock layoutBlock, boolean forceBlockTrackColor) {
174        Color result = ColorUtil.CLEAR;  // transparent
175        if (layoutBlock != null) {
176            if (forceBlockTrackColor) {
177                result = layoutBlock.getBlockTrackColor();
178            } else {
179                result = layoutBlock.getBlockColor();
180            }
181        }
182        return result;
183    }
184
185    // optional parameter forceTrack = false
186    final protected Color getColorForTrackBlock(@CheckForNull LayoutBlock lb) {
187        return getColorForTrackBlock(lb, false);
188    }
189
190    final protected Color setColorForTrackBlock(Graphics2D g2,
191            @CheckForNull LayoutBlock layoutBlock, boolean forceBlockTrackColor) {
192        Color result = getColorForTrackBlock(layoutBlock, forceBlockTrackColor);
193        g2.setColor(result);
194        return result;
195    }
196
197    // optional parameter forceTrack = false
198    final protected Color setColorForTrackBlock(Graphics2D g2, @CheckForNull LayoutBlock lb) {
199        return setColorForTrackBlock(g2, lb, false);
200    }
201
202    /**
203     * draw one line (Ballast, ties, center or 3rd rail, block lines)
204     *
205     * @param g2      the graphics context
206     * @param isMain  true if drawing mainlines
207     * @param isBlock true if drawing block lines
208     */
209    abstract protected void draw1(Graphics2D g2, boolean isMain, boolean isBlock);
210
211    /**
212     * draw two lines (rails)
213     *
214     * @param g2               the graphics context
215     * @param isMain           true if drawing mainlines
216     * @param railDisplacement the offset from center to draw the lines
217     */
218    abstract protected void draw2(Graphics2D g2, boolean isMain, float railDisplacement);
219
220    /**
221     * draw hidden track
222     *
223     * @param g2 the graphics context
224     */
225    // abstract protected void drawHidden(Graphics2D g2);
226    // note: placeholder until I get this implemented in all sub-classes
227    // TODO: replace with abstract declaration (above)
228    final protected void drawHidden(Graphics2D g2) {
229        // nothing to do here... move along...
230    }
231
232    /**
233     * draw the text for this layout track
234     * @param g
235     * note: currently can't override (final); change this if you need to
236     */
237    final protected void drawLayoutTrackText(Graphics2D g) {
238        // get the center coordinates
239        int x = (int) center.getX(), y = (int) center.getY();
240
241        // get the name of this track
242        String name = getName();
243
244        // get the FontMetrics
245        FontMetrics metrics = g.getFontMetrics(g.getFont());
246
247        // determine the X coordinate for the text
248        x -= metrics.stringWidth(name) / 2;
249
250        // determine the Y coordinate for the text
251        y += metrics.getHeight() / 2;
252
253        // (note we add the ascent, as in java 2d 0 is top of the screen)
254        //y += (int) metrics.getAscent();
255
256        g.drawString(name, x, y);
257    }
258
259    /**
260     * Load a file for a specific arrow ending.
261     *
262     * @param n               The arrow type as a number
263     * @param arrowsCountMenu menu containing the arrows to set visible
264     *                        selection
265     * @return An item for the arrow menu
266     */
267    public JCheckBoxMenuItem loadArrowImageToJCBItem(int n, JMenu arrowsCountMenu) {
268        ImageIcon imageIcon = new ImageIcon(FileUtil.findURL("program:resources/icons/decorations/ArrowStyle" + n + ".png"));
269        JCheckBoxMenuItem jcbmi = new JCheckBoxMenuItem(imageIcon);
270        arrowsCountMenu.add(jcbmi);
271        jcbmi.setToolTipText(Bundle.getMessage("DecorationStyleMenuToolTip"));
272        // can't set selected here because the ActionListener has to be set first
273        return jcbmi;
274    }
275    protected static final int NUM_ARROW_TYPES = 6;
276
277    /**
278     * highlight unconnected connections
279     *
280     * @param g2           the graphics context
281     * @param specificType the specific connection to draw (or NONE for all)
282     */
283    abstract protected void highlightUnconnected(Graphics2D g2, HitPointType specificType);
284
285    // optional parameter specificType = NONE
286    final protected void highlightUnconnected(Graphics2D g2) {
287        highlightUnconnected(g2, HitPointType.NONE);
288    }
289
290    /**
291     * draw the edit controls
292     *
293     * @param g2 the graphics context
294     */
295    abstract protected void drawEditControls(Graphics2D g2);
296
297    /**
298     * Draw the turnout controls
299     *
300     * @param g2 the graphics context
301     */
302    abstract protected void drawTurnoutControls(Graphics2D g2);
303
304    /**
305     * Draw track decorations
306     *
307     * @param g2 the graphics context
308     */
309    abstract protected void drawDecorations(Graphics2D g2);
310
311    /**
312     * Get the hidden state of the track element.
313     *
314     * @return true if hidden; false otherwise
315     */
316    final public boolean isHidden() {
317        return hidden;
318    }
319
320    final public void setHidden(boolean hide) {
321        if (hidden != hide) {
322            hidden = hide;
323            if (layoutEditor != null) {
324                layoutEditor.redrawPanel();
325            }
326        }
327    }
328
329    private boolean hidden = false;
330
331    /*
332    * non-accessor methods
333     */
334    /**
335     * get turnout state string
336     *
337     * @param turnoutState of the turnout
338     * @return the turnout state string
339     */
340    final public String getTurnoutStateString(int turnoutState) {
341        String result = "";
342        if (turnoutState == Turnout.CLOSED) {
343            result = Bundle.getMessage("TurnoutStateClosed");
344        } else if (turnoutState == Turnout.THROWN) {
345            result = Bundle.getMessage("TurnoutStateThrown");
346        } else {
347            result = Bundle.getMessage("BeanStateUnknown");
348        }
349        return result;
350    }
351
352    /**
353     * Check for active block boundaries.
354     * <p>
355     * If any connection point of a layout track object has attached objects,
356     * such as signal masts, signal heads or NX sensors, the layout track object
357     * cannot be deleted.
358     *
359     * @return true if the layout track object can be deleted.
360     */
361    abstract public boolean canRemove();
362
363    /**
364     * Display the attached items that prevent removing the layout track item.
365     *
366     * @param itemList A list of the attached heads, masts and/or sensors.
367     * @param typeKey  The object type such as Turnout, Level Crossing, etc.
368     */
369    final public void displayRemoveWarningDialog(List<String> itemList, String typeKey) {
370        itemList.sort(null);
371        StringBuilder msg = new StringBuilder(Bundle.getMessage("MakeLabel", // NOI18N
372                Bundle.getMessage("DeleteTrackItem", Bundle.getMessage(typeKey))));  // NOI18N
373        for (String item : itemList) {
374            msg.append("\n    " + item);  // NOI18N
375        }
376        javax.swing.JOptionPane.showMessageDialog(layoutEditor,
377                msg.toString(),
378                Bundle.getMessage("WarningTitle"), // NOI18N
379                javax.swing.JOptionPane.WARNING_MESSAGE);
380    }
381
382    /**
383     * scale this LayoutTrack's coordinates by the x and y factors
384     *
385     * @param xFactor the amount to scale X coordinates
386     * @param yFactor the amount to scale Y coordinates
387     */
388    abstract public void scaleCoords(double xFactor, double yFactor);
389
390    /**
391     * translate this LayoutTrack's coordinates by the x and y factors
392     *
393     * @param xFactor the amount to translate X coordinates
394     * @param yFactor the amount to translate Y coordinates
395     */
396    abstract public void translateCoords(double xFactor, double yFactor);
397
398    /**
399     * rotate this LayoutTrack's coordinates by angleDEG's
400     *
401     * @param angleDEG the amount to rotate in degrees
402     */
403    abstract public void rotateCoords(double angleDEG);
404
405    final protected Point2D rotatePoint(@Nonnull Point2D p, double sineRot, double cosineRot) {
406        double cX = center.getX();
407        double cY = center.getY();
408
409        double deltaX = p.getX() - cX;
410        double deltaY = p.getY() - cY;
411
412        double x = cX + cosineRot * deltaX - sineRot * deltaY;
413        double y = cY + sineRot * deltaX + cosineRot * deltaY;
414
415        return new Point2D.Double(x, y);
416    }
417
418    /**
419     * find the hit (location) type for a point
420     *
421     * @param hitPoint           the point
422     * @param useRectangles      whether to use (larger) rectangles or (smaller)
423     *                           circles for hit testing
424     * @param requireUnconnected whether to only return hit types for free
425     *                           connections
426     * @return the location type for the point (or NONE)
427     * @since 7.4.3
428     */
429    abstract protected HitPointType findHitPointType(@Nonnull Point2D hitPoint,
430                                                    boolean useRectangles,
431                                                    boolean requireUnconnected);
432
433    // optional useRectangles & requireUnconnected parameters default to false
434    final protected HitPointType findHitPointType(@Nonnull Point2D p) {
435        return findHitPointType(p, false, false);
436    }
437
438    // optional requireUnconnected parameter defaults to false
439    final protected HitPointType findHitPointType(@Nonnull Point2D p, boolean useRectangles) {
440        return findHitPointType(p, useRectangles, false);
441    }
442
443    /**
444     * return the coordinates for a specified connection type (abstract: should
445     * be overridden by ALL subclasses)
446     *
447     * @param connectionType the connection type
448     * @return the coordinates for the specified connection type
449     */
450    abstract public Point2D getCoordsForConnectionType(HitPointType connectionType);
451
452    /**
453     * @return the bounds of this track
454     */
455    abstract public Rectangle2D getBounds();
456
457    /**
458     * show the popup menu for this layout track
459     *
460     * @param mouseEvent the mouse down event that triggered this popup
461     * @return the popup menu for this layout track
462     */
463    @Nonnull
464    abstract protected JPopupMenu showPopup(@Nonnull MouseEvent mouseEvent);
465
466    /**
467     * show the popup menu for this layout track
468     *
469     * @param where to show the popup
470     * @return the popup menu for this layout track
471     */
472    @Nonnull
473    final protected JPopupMenu showPopup(Point2D where) {
474        return this.showPopup(new MouseEvent(
475                layoutEditor.getTargetPanel(), // source
476                MouseEvent.MOUSE_CLICKED, // id
477                System.currentTimeMillis(), // when
478                0, // modifiers
479                (int) where.getX(), (int) where.getY(), // where
480                0, // click count
481                true));                         // popup trigger
482
483    }
484
485    /**
486     * show the popup menu for this layout track
487     *
488     * @return the popup menu for this layout track
489     */
490    @Nonnull
491    final protected JPopupMenu showPopup() {
492        Point2D where = MathUtil.multiply(getCoordsCenter(),
493                layoutEditor.getZoom());
494        return this.showPopup(where);
495    }
496
497    /**
498     * get the LayoutTrack connected at the specified connection type
499     *
500     * @param connectionType where on us to get the connection
501     * @return the LayoutTrack connected at the specified connection type
502     * @throws JmriException - if the connectionType is invalid
503     */
504    abstract public LayoutTrack getConnection(HitPointType connectionType) throws JmriException;
505
506    /**
507     * set the LayoutTrack connected at the specified connection type
508     *
509     * @param connectionType where on us to set the connection
510     * @param o              the LayoutTrack that is to be connected
511     * @param type           where on the LayoutTrack we are connected
512     * @throws JmriException - if connectionType or type are invalid
513     */
514    abstract public void setConnection(HitPointType connectionType, LayoutTrack o, HitPointType type) throws JmriException;
515
516    /**
517     * abstract method... subclasses should implement _IF_ they need to recheck
518     * their block boundaries
519     */
520    abstract protected void reCheckBlockBoundary();
521
522    /**
523     * get the layout connectivity for this track
524     *
525     * @return the list of Layout Connectivity objects
526     */
527    abstract protected List<LayoutConnectivity> getLayoutConnectivity();
528
529    /**
530     * return true if this connection type is disconnected
531     *
532     * @param connectionType the connection type to test
533     * @return true if the connection for this connection type is free
534     */
535    public boolean isDisconnected(HitPointType connectionType) {
536        throw new IllegalArgumentException("should have called in Object instead of View (temporary)");
537    }
538
539    /**
540     * return a list of the available connections for this layout track
541     *
542     * @return the list of available connections
543     */
544    // note: used by LayoutEditorChecks.setupCheckUnConnectedTracksMenu()
545    //
546    // This could have just returned a boolean but I thought a list might be
547    // more useful (eventually... not currently being used; we just check to see
548    // if it's not empty.)
549    @Nonnull
550    abstract public List<HitPointType> checkForFreeConnections();
551
552    /**
553     * determine if all the appropriate blocks have been assigned to this track
554     *
555     * @return true if all appropriate blocks have been assigned
556     */
557    // note: used by LayoutEditorChecks.setupCheckUnBlockedTracksMenu()
558    //
559    abstract public boolean checkForUnAssignedBlocks();
560
561    /**
562     * check this track and its neighbors for non-contiguous blocks
563     * <p>
564     * For each (non-null) blocks of this track do: #1) If it's got an entry in
565     * the blockNamesToTrackNameSetMap then #2) If this track is not in one of
566     * the TrackNameSets for this block #3) add a new set (with this
567     * block/track) to blockNamesToTrackNameSetMap and #4) check all the
568     * connections in this block (by calling the 2nd method below)
569     * <p>
570     * Basically, we're maintaining contiguous track sets for each block found
571     * (in blockNamesToTrackNameSetMap)
572     *
573     * @param blockNamesToTrackNameSetMaps hashmap of key:block names to lists
574     *                                     of track name sets for those blocks
575     */
576    // note: used by LayoutEditorChecks.setupCheckNonContiguousBlocksMenu()
577    //
578    abstract public void checkForNonContiguousBlocks(
579            @Nonnull HashMap<String, List<Set<String>>> blockNamesToTrackNameSetMaps);
580
581    /**
582     * recursive routine to check for all contiguous tracks in this blockName
583     *
584     * @param blockName    the block that we're checking for
585     * @param TrackNameSet the set of track names in this block
586     */
587    abstract public void collectContiguousTracksNamesInBlockNamed(
588            @Nonnull String blockName,
589            @Nonnull Set<String> TrackNameSet);
590
591    /**
592     * Assign all the layout blocks in this track
593     *
594     * @param layoutBlock to this layout block (used by the Tools menu's "Assign
595     *                    block to selection" item)
596     */
597    abstract public void setAllLayoutBlocks(LayoutBlock layoutBlock);
598
599    // private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutTrackView.class);
600}