001package jmri.jmrit.display.layoutEditor;
002
003import java.awt.*;
004import java.awt.event.ActionEvent;
005import java.awt.geom.*;
006import java.util.List;
007import java.util.*;
008import java.util.function.*;
009
010import javax.annotation.CheckForNull;
011import javax.annotation.Nonnull;
012import javax.swing.*;
013
014import jmri.jmrit.display.layoutEditor.blockRoutingTable.LayoutBlockRouteTableAction;
015import jmri.util.*;
016import jmri.util.swing.JmriColorChooser;
017import jmri.util.swing.JmriMouseEvent;
018
019/**
020 * MVC View component for the TrackSegment class.
021 * <p>
022 * Arrows and bumpers are visual, presentation aspects handled in the View.
023 *
024 * @author Bob Jacobsen Copyright (c) 2020
025 */
026public class TrackSegmentView extends LayoutTrackView {
027
028    public TrackSegmentView(@Nonnull TrackSegment track, @Nonnull LayoutEditor layoutEditor) {
029        super(track, layoutEditor);
030
031        this.trackSegment = track;
032
033        setupDefaultBumperSizes(layoutEditor);
034        editor = new jmri.jmrit.display.layoutEditor.LayoutEditorDialogs.TrackSegmentEditor(layoutEditor);
035    }
036
037    /**
038     * constructor method.
039     *
040     * @param track        the track segment to view
041     * @param layoutEditor for reference to tools
042     * @param arc          specify display
043     * @param flip         specify display
044     * @param circle       specify display
045     */
046    public TrackSegmentView(@Nonnull TrackSegment track, @Nonnull LayoutEditor layoutEditor,
047            boolean arc, boolean flip, boolean circle
048    ) {
049        this(track, layoutEditor);
050    }
051
052    // persistent instances variables (saved between sessions)
053    private boolean dashed = false;
054
055    private boolean arc = false;
056    private boolean circle = false;
057    private boolean flip = false;
058    private double angle = 0.0D;
059
060    private boolean changed = false;
061    private boolean bezier = false;
062
063    // for Bezier
064    private final ArrayList<Point2D> bezierControlPoints = new ArrayList<>(); // list of control point displacements
065
066    // temporary reference to the Editor that will eventually be part of View
067    private final jmri.jmrit.display.layoutEditor.LayoutEditorDialogs.TrackSegmentEditor editor;
068
069    final private TrackSegment trackSegment;
070
071    // temporary?
072    @Nonnull
073    public TrackSegment getTrackSegment() {
074        return trackSegment;
075    }
076
077    /**
078     * Get debugging string for the TrackSegment.
079     *
080     * @return text showing id and connections of this segment
081     */
082    @Override
083    public String toString() {
084        return "TrackSegmentView " + getName()
085                + " c1:{" + getConnect1Name() + " (" + getType1() + ")},"
086                + " c2:{" + getConnect2Name() + " (" + getType2() + ")}";
087
088    }
089
090    /*
091    * Accessor methods
092     */
093    @Nonnull
094    public String getBlockName() {
095        return trackSegment.getBlockName();
096    }
097
098    public HitPointType getType1() {
099        return trackSegment.getType1();
100    }
101
102    public HitPointType getType2() {
103        return trackSegment.getType2();
104    }
105
106    public LayoutTrack getConnect1() {
107        return trackSegment.getConnect1();
108    }
109
110    public LayoutTrack getConnect2() {
111        return trackSegment.getConnect2();
112    }
113
114    /**
115     * set a new connection 1
116     *
117     * @param connectTrack   the track we want to connect to
118     * @param connectionType where on that track we want to be connected
119     */
120    protected void setNewConnect1(@CheckForNull LayoutTrack connectTrack, HitPointType connectionType) {
121        trackSegment.setNewConnect1(connectTrack, connectionType);
122    }
123
124    /**
125     * set a new connection 2
126     *
127     * @param connectTrack   the track we want to connect to
128     * @param connectionType where on that track we want to be connected
129     */
130    protected void setNewConnect2(@CheckForNull LayoutTrack connectTrack, HitPointType connectionType) {
131        trackSegment.setNewConnect2(connectTrack, connectionType);
132    }
133
134    /**
135     * replace old track connection with new track connection
136     *
137     * @param oldTrack the old track connection
138     * @param newTrack the new track connection
139     * @param newType  type of the new track connection
140     * @return true if successful
141     */
142    public boolean replaceTrackConnection(@CheckForNull LayoutTrack oldTrack, @CheckForNull LayoutTrack newTrack, HitPointType newType) {
143        return trackSegment.replaceTrackConnection(oldTrack, newTrack, newType);
144    }
145
146    /**
147     * @return true if track segment should be drawn dashed
148     */
149    public boolean isDashed() {
150        return dashed;
151    }
152
153    public void setDashed(boolean dash) {
154        if (dashed != dash) {
155            dashed = dash;
156            layoutEditor.redrawPanel();
157            layoutEditor.setDirty();
158        }
159    }
160
161    /**
162     * @return true if track segment is an arc
163     */
164    public boolean isArc() {
165        return arc;
166    }
167
168    public void setArc(boolean boo) {
169        if (arc != boo) {
170            arc = boo;
171            if (arc) {
172                circle = false;
173                bezier = false;
174                hideConstructionLines(SHOWCON);
175            }
176            changed = true;
177        }
178    }
179
180    /**
181     * @return true if track segment is circle
182     */
183    public boolean isCircle() {
184        return circle;
185    }
186
187    public void setCircle(boolean boo) {
188        if (circle != boo) {
189            circle = boo;
190            if (circle) {
191                // if it was a bezier
192                if (bezier) {
193                    // then use control point to calculate arc
194                    // adjacent connections must be defined...
195                    if ((getConnect1() != null) && (getConnect2() != null)) {
196                        Point2D end1 = layoutEditor.getCoords(getConnect1(), getType1());
197                        Point2D end2 = layoutEditor.getCoords(getConnect2(), getType2());
198                        double chordLength = MathUtil.distance(end1, end2);
199
200                        // get first and last control points
201                        int cnt = bezierControlPoints.size();
202
203                        Point2D cp0 = bezierControlPoints.get(0);
204                        Point2D cpN = bezierControlPoints.get(cnt - 1);
205                        // calculate orthoginal points
206                        Point2D op1 = MathUtil.add(end1, MathUtil.orthogonal(MathUtil.subtract(cp0, end1)));
207                        Point2D op2 = MathUtil.subtract(end2, MathUtil.orthogonal(MathUtil.subtract(cpN, end2)));
208                        // use them to find center point
209                        Point2D ip = MathUtil.intersect(end1, op1, end2, op2);
210                        if (ip != null) {   // single intersection point found
211                            double r1 = MathUtil.distance(ip, end1);
212                            double r2 = MathUtil.distance(ip, end2);
213                            if (Math.abs(r1 - r2) <= 1.0) {
214                                // calculate arc: θ = 2 sin-1(c/(2r))
215                                setAngle(Math.toDegrees(2.0 * Math.asin(chordLength / (2.0 * r1))));
216                                // the sign of the distance tells what side of the line the center point is on
217                                double distance = MathUtil.distance(end1, end2, ip);
218                                setFlip(distance < 0.0);
219                            }
220                        }
221                    }
222                    bezier = false;
223                } else if (getAngle() < 1.0D) {
224                    setAngle(90.0D);
225                }
226                arc = true;
227                hideConstructionLines(SHOWCON);
228            }
229            changed = true;
230        }
231    }
232
233    /**
234     * @return true if track segment circle or arc should be drawn flipped
235     */
236    public boolean isFlip() {
237        return flip;
238    }
239
240    public void setFlip(boolean boo) {
241        if (flip != boo) {
242            flip = boo;
243            changed = true;
244            hideConstructionLines(SHOWCON);
245            layoutEditor.redrawPanel();
246            layoutEditor.setDirty();
247        }
248    }
249
250    /**
251     * @return true if track segment is a bezier curve
252     */
253    public boolean isBezier() {
254        return bezier;
255    }
256
257    /**
258     * @param bool Set true to turn on Bezier curve representation.
259     */
260    public void setBezier(boolean bool) {
261        if (bezier != bool) {
262            bezier = bool;
263            if (bezier) {
264                arc = false;
265                circle = false;
266                hideConstructionLines(SHOWCON);
267            }
268            changed = true;
269        }
270    }
271
272    public double getAngle() {
273        return angle;
274    }
275
276    public void setAngle(double x) {
277        angle = MathUtil.pin(x, 0.0D, 180.0D);
278        changed = true;
279    }
280
281    /**
282     * Get the direction from end point 1 to 2.
283     * <p>
284     * Note: Goes CW from east (0) to south (PI/2) to west (PI) to north
285     * (PI*3/2), etc.
286     *
287     * @return The direction (in radians)
288     */
289    public double getDirectionRAD() {
290        Point2D ep1 = getCoordsCenter(), ep2 = getCoordsCenter();
291        if (getConnect1() != null) {
292            ep1 = layoutEditor.getCoords(getConnect1(), getType1());
293        }
294        if (getConnect2() != null) {
295            ep2 = layoutEditor.getCoords(getConnect2(), getType2());
296        }
297        return (Math.PI / 2.D) - MathUtil.computeAngleRAD(ep1, ep2);
298    }
299
300    /**
301     * Get the direction from end point 1 to 2.
302     * <p>
303     * Note: Goes CW from east (0) to south (90) to west (180) to north (270),
304     * etc.
305     *
306     * @return the direction (in degrees)
307     */
308    public double getDirectionDEG() {
309        return Math.toDegrees(getDirectionRAD());
310    }
311
312    /**
313     * Determine if we need to redraw a curved piece of track. Saves having to
314     * recalculate the circle details each time.
315     *
316     * @return true means needs to be (re)drawn
317     */
318    public boolean trackNeedsRedraw() {
319        return changed;
320    }
321
322    public void trackRedrawn() {
323        changed = false;
324    }
325
326    public LayoutBlock getLayoutBlock() {
327        return trackSegment.getLayoutBlock();
328    }
329
330    public String getConnect1Name() {
331        return trackSegment.getConnect1Name();
332    }
333
334    public String getConnect2Name() {
335        return trackSegment.getConnect2Name();
336    }
337
338    @Override
339    public LayoutTrack getConnection(HitPointType connectionType) throws jmri.JmriException {
340        return trackSegment.getConnection(connectionType);
341    }
342
343    /**
344     * {@inheritDoc}
345     * <p>
346     * This implementation does nothing because {@link #setNewConnect1} and
347     * {@link #setNewConnect2} should be used instead.
348     */
349    // only implemented here to suppress "does not override abstract method " error in compiler
350    @Override
351    public void setConnection(HitPointType connectionType, @CheckForNull LayoutTrack o, HitPointType type) throws jmri.JmriException {
352    }
353
354    public int getNumberOfBezierControlPoints() {
355        return bezierControlPoints.size();
356    }
357
358    /**
359     * @param index If negative, this is index from the end i.e. -1 is the last
360     *              element
361     * @return Reference to the indexed control point
362     */
363    public Point2D getBezierControlPoint(int index) {
364        Point2D result = getCoordsCenter();
365        if (index < 0) {
366            index += bezierControlPoints.size();
367        }
368        if ((index >= 0) && (index < bezierControlPoints.size())) {
369            result = bezierControlPoints.get(index);
370        }
371        return result;
372    }
373
374    /**
375     * @param p     the location of the point to be set
376     * @param index If negative, this is index from the end i.e. -1 is the last
377     *              element
378     */
379    public void setBezierControlPoint(@CheckForNull Point2D p, int index) {
380        if (index < 0) {
381            index += bezierControlPoints.size();
382        }
383        if ((index >= 0) && (index <= bezierControlPoints.size())) {
384            if (index < bezierControlPoints.size()) {
385                bezierControlPoints.set(index, p);
386            } else {
387                bezierControlPoints.add(p);
388            }
389        }
390    }
391
392    @Nonnull
393    public ArrayList<Point2D> getBezierControlPoints() {
394        return bezierControlPoints;
395    }
396
397    /**
398     * Set up a LayoutBlock for this Track Segment.
399     *
400     * @param newLayoutBlock the LayoutBlock to set
401     */
402    public void setLayoutBlock(@CheckForNull LayoutBlock newLayoutBlock) {
403        trackSegment.setLayoutBlock(newLayoutBlock);
404    }
405
406    /**
407     * Set up a LayoutBlock for this Track Segment.
408     *
409     * @param name the name of the new LayoutBlock
410     */
411    public void setLayoutBlockByName(@CheckForNull String name) {
412        trackSegment.setLayoutBlockByName(name);
413    }
414
415    /*
416    * non-accessor methods
417     */
418    /**
419     * {@inheritDoc}
420     */
421    @Override
422    public void scaleCoords(double xFactor, double yFactor) {
423        Point2D factor = new Point2D.Double(xFactor, yFactor);
424        super.setCoordsCenter(MathUtil.multiply(getCoordsCenter(), factor));
425        if (isBezier()) {
426            for (Point2D p : bezierControlPoints) {
427                p.setLocation(MathUtil.multiply(p, factor));
428            }
429        }
430    }
431
432    /**
433     * {@inheritDoc}
434     */
435    @Override
436    public void translateCoords(double xFactor, double yFactor) {
437        super.setCoordsCenter(MathUtil.add(getCoordsCenter(), new Point2D.Double(xFactor, yFactor)));
438    }
439
440    /**
441     * {@inheritDoc}
442     */
443    @Override
444    public void rotateCoords(double angleDEG) {
445        if (isBezier()) {
446            for (Point2D p : bezierControlPoints) {
447                p.setLocation(MathUtil.rotateDEG(p, getCoordsCenter(), angleDEG));
448            }
449        }
450    }
451
452    /**
453     * Set center coordinates.
454     *
455     * @param newCenterPoint the coordinates to set
456     */
457    @Override
458    public void setCoordsCenter(@Nonnull Point2D newCenterPoint) {
459        if (getCoordsCenter() != newCenterPoint) {
460            if (isBezier()) {
461                Point2D delta = MathUtil.subtract(newCenterPoint, getCoordsCenter());
462                for (Point2D p : bezierControlPoints) {
463                    p.setLocation(MathUtil.add(p, delta));
464                }
465            }
466            super.setCoordsCenter(newCenterPoint);
467        }
468    }
469
470    // initialization instance variables (used when loading a LayoutEditor)
471    public String tConnect1Name = "";
472    public String tConnect2Name = "";
473
474    public String tLayoutBlockName = "";
475
476    public void updateBlockInfo() {
477        trackSegment.updateBlockInfo();
478    }
479
480    /**
481     * {@inheritDoc}
482     */
483    @Override
484    protected HitPointType findHitPointType(Point2D hitPoint, boolean useRectangles, boolean requireUnconnected) {
485        HitPointType result = HitPointType.NONE;  // assume point not on connection
486
487        if (!requireUnconnected) {
488            // note: optimization here: instead of creating rectangles for all the
489            // points to check below, we create a rectangle for the test point
490            // and test if the points below are in that rectangle instead.
491            Rectangle2D r = layoutEditor.layoutEditorControlCircleRectAt(hitPoint);
492            Point2D p, minPoint = MathUtil.zeroPoint2D;
493            double circleRadius = LayoutEditor.SIZE * layoutEditor.getTurnoutCircleSize();
494            double distance, minDistance = Float.POSITIVE_INFINITY;
495
496            if (isCircle()) {
497                p = getCoordsCenterCircle();
498                distance = MathUtil.distance(p, hitPoint);
499                if (distance < minDistance) {
500                    minDistance = distance;
501                    minPoint = p;
502                    result = HitPointType.TRACK_CIRCLE_CENTRE;
503                }
504            } else if (isBezier()) {
505                // hit testing for the control points
506                for (int index = 0; index < bezierControlPoints.size(); index++) {
507                    p = bezierControlPoints.get(index);
508                    distance = MathUtil.distance(p, hitPoint);
509                    if (distance < minDistance) {
510                        minDistance = distance;
511                        minPoint = p;
512                        result = HitPointType.bezierPointIndexedValue(index);
513                    }
514                }
515            }
516            p = getCentreSeg();
517            if (r.contains(p)) {
518                distance = MathUtil.distance(p, hitPoint);
519                if (distance <= minDistance) {
520                    minDistance = distance;
521                    minPoint = p;
522                    result = HitPointType.TRACK;
523                }
524            }
525            if ((result != HitPointType.NONE) && (useRectangles ? !r.contains(minPoint) : (minDistance > circleRadius))) {
526                result = HitPointType.NONE;
527            }
528        }
529        return result;
530    }   // findHitPointType
531
532    /**
533     * Get the coordinates for a specified connection type.
534     *
535     * @param connectionType the connection type
536     * @return the coordinates for the specified connection type
537     */
538    @Override
539    public Point2D getCoordsForConnectionType(HitPointType connectionType) {
540        Point2D result = getCentreSeg();
541        if (connectionType == HitPointType.TRACK_CIRCLE_CENTRE) {
542            result = getCoordsCenterCircle();
543        } else if (HitPointType.isBezierHitType(connectionType)) {
544            result = getBezierControlPoint(connectionType.bezierPointIndex());
545        }
546        return result;
547    }
548
549    /**
550     * @return the bounds of this track segment
551     */
552    @Override
553    public Rectangle2D getBounds() {
554        Point2D ep1 = getCoordsCenter(), ep2 = getCoordsCenter();
555        if (getConnect1() != null) {
556            ep1 = layoutEditor.getCoords(getConnect1(), getType1());
557        }
558        if (getConnect2() != null) {
559            ep2 = layoutEditor.getCoords(getConnect2(), getType2());
560        }
561
562        Rectangle2D result = new Rectangle2D.Double(ep1.getX(), ep1.getY(), 0, 0);
563        result.add(ep2);
564
565        if (isArc()) {
566            result.add(getCentreSeg());
567            if (isCircle()) {
568                result.add(getCoordsCenterCircle());
569            }
570        } else if (isBezier()) {
571            for (int index = 0; index < bezierControlPoints.size(); index++) {
572                result.add(bezierControlPoints.get(index));
573            }
574        }
575        result.add(getCoordsCenter());
576
577        return result;
578    }
579
580    private JPopupMenu popupMenu = null;
581    private final JCheckBoxMenuItem mainlineCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("MainlineCheckBoxMenuItemTitle"));
582    private final JCheckBoxMenuItem hiddenCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("HiddenCheckBoxMenuItemTitle"));
583    private final JCheckBoxMenuItem dashedCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("DashedCheckBoxMenuItemTitle"));
584    private final JCheckBoxMenuItem flippedCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("FlippedCheckBoxMenuItemTitle"));
585
586    /**
587     * Maximum length of the bumper decoration.
588     */
589    public static final int MAX_BUMPER_LENGTH = 40;
590    public static final int MIN_BUMPER_LENGTH = 8;
591    public static final int MAX_BUMPER_WIDTH = 10;
592    public static final int MIN_BUMPER_WIDTH = 1;
593
594    private static final int MAX_ARROW_LINE_WIDTH = 5;
595    private static final int MAX_ARROW_LENGTH = 60;
596    private static final int MAX_ARROW_GAP = 40;
597
598    private static final int MAX_BRIDGE_LINE_WIDTH = 5;
599    private static final int MIN_BRIDGE_LINE_WIDTH = 1;
600
601    private static final int MAX_BRIDGE_APPROACH_WIDTH = 100;
602    private static final int MIN_BRIDGE_APPROACH_WIDTH = 8;
603
604    private static final int MAX_BRIDGE_DECK_WIDTH = 80;
605    private static final int MIN_BRIDGE_DECK_WIDTH = 6;
606
607    private static final int MAX_BUMPER_LINE_WIDTH = 9;
608    private static final int MIN_BUMPER_LINE_WIDTH = 1;
609
610    private static final int MAX_TUNNEL_FLOOR_WIDTH = 40;
611    private static final int MIN_TUNNEL_FLOOR_WIDTH = 4;
612
613    private static final int MAX_TUNNEL_LINE_WIDTH = 9;
614    private static final int MIN_TUNNEL_LINE_WIDTH = 1;
615
616    private static final int MAX_TUNNEL_ENTRANCE_WIDTH = 80;
617    private static final int MIN_TUNNEL_ENTRANCE_WIDTH = 1;
618
619    /**
620     * Helper method, which adds "Set value" item to the menu. The value can be
621     * optionally range-checked. Item will be appended at the end of the menu.
622     *
623     * @param menu       the target menu.
624     * @param titleKey   bundle key for the menu title/dialog title
625     * @param toolTipKey bundle key for the menu item tooltip
626     * @param val        value getter
627     * @param set        value setter
628     * @param predicate  checking predicate, possibly null.
629     */
630    private void addNumericMenuItem(@Nonnull JMenu menu,
631            @Nonnull String titleKey, @Nonnull String toolTipKey,
632            @Nonnull Supplier<Integer> val,
633            @Nonnull Consumer<Integer> set,
634            @CheckForNull Predicate<Integer> predicate) {
635        int oldVal = val.get();
636        JMenuItem jmi = menu.add(new JMenuItem(Bundle.getMessage("MakeLabel",
637                Bundle.getMessage(titleKey)) + oldVal));
638        jmi.setToolTipText(Bundle.getMessage(toolTipKey));
639        jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
640            // prompt for lineWidth
641            int newValue = QuickPromptUtil.promptForInteger(layoutEditor,
642                    Bundle.getMessage(titleKey),
643                    Bundle.getMessage(titleKey),
644                    // getting again, maybe something changed from the menu construction ?
645                    val.get(), predicate);
646            set.accept(newValue);
647            layoutEditor.repaint();
648        });
649    }
650
651    /**
652     * {@inheritDoc}
653     */
654    @Override
655    @Nonnull
656    protected JPopupMenu showPopup(@Nonnull JmriMouseEvent mouseEvent) {
657        if (popupMenu != null) {
658            popupMenu.removeAll();
659        } else {
660            popupMenu = new JPopupMenu();
661        }
662
663        String info = Bundle.getMessage("TrackSegment");
664        if (isArc()) {
665            if (isCircle()) {
666                info = info + " (" + Bundle.getMessage("Circle") + ")";
667            } else {
668                info = info + " (" + Bundle.getMessage("Ellipse") + ")";
669            }
670        } else if (isBezier()) {
671            info = info + " (" + Bundle.getMessage("Bezier") + ")";
672        } else {
673            info = info + " (" + Bundle.getMessage("Line") + ")";
674        }
675
676        JMenuItem jmi = popupMenu.add(Bundle.getMessage("MakeLabel", info) + getName());
677        jmi.setEnabled(false);
678
679        if (getBlockName().isEmpty()) {
680            jmi = popupMenu.add(Bundle.getMessage("NoBlock"));
681        } else {
682            jmi = popupMenu.add(Bundle.getMessage("MakeLabel", Bundle.getMessage("BeanNameBlock")) + getLayoutBlock().getDisplayName());
683        }
684        jmi.setEnabled(false);
685
686        // if there are any track connections
687        if ((getConnect1() != null) || (getConnect2() != null)) {
688            JMenu connectionsMenu = new JMenu(Bundle.getMessage("Connections")); // there is no pane opening (which is what ... implies)
689            if (getConnect1() != null) {
690                connectionsMenu.add(new AbstractAction(Bundle.getMessage("MakeLabel", "1") + getConnect1().getName()) {
691                    @Override
692                    public void actionPerformed(ActionEvent e) {
693                        LayoutEditorFindItems lf = layoutEditor.getFinder();
694                        LayoutTrack lt = lf.findObjectByName(getConnect1().getName());
695                        // this shouldn't ever be null... however...
696                        if (lt != null) {
697                            LayoutTrackView ltv = layoutEditor.getLayoutTrackView(lt);
698                            layoutEditor.setSelectionRect(ltv.getBounds());
699                            ltv.showPopup();
700                        }
701                    }
702                });
703            }
704            if (getConnect2() != null) {
705                connectionsMenu.add(new AbstractAction(Bundle.getMessage("MakeLabel", "2") + getConnect2().getName()) {
706                    @Override
707                    public void actionPerformed(ActionEvent e) {
708                        LayoutEditorFindItems lf = layoutEditor.getFinder();
709                        LayoutTrack lt = lf.findObjectByName(getConnect2().getName());
710                        // this shouldn't ever be null... however...
711                        if (lt != null) {
712                            LayoutTrackView ltv = layoutEditor.getLayoutTrackView(lt);
713                            layoutEditor.setSelectionRect(ltv.getBounds());
714                            ltv.showPopup();
715                        }
716                    }
717                });
718            }
719            popupMenu.add(connectionsMenu);
720        }
721
722        popupMenu.add(new JSeparator(JSeparator.HORIZONTAL));
723
724        popupMenu.add(mainlineCheckBoxMenuItem);
725        mainlineCheckBoxMenuItem.addActionListener((java.awt.event.ActionEvent e3) -> trackSegment.setMainline(mainlineCheckBoxMenuItem.isSelected()));
726        mainlineCheckBoxMenuItem.setToolTipText(Bundle.getMessage("MainlineCheckBoxMenuItemToolTip"));
727        mainlineCheckBoxMenuItem.setSelected(trackSegment.isMainline());
728
729        popupMenu.add(hiddenCheckBoxMenuItem);
730        hiddenCheckBoxMenuItem.addActionListener((java.awt.event.ActionEvent e3) -> setHidden(hiddenCheckBoxMenuItem.isSelected()));
731        hiddenCheckBoxMenuItem.setToolTipText(Bundle.getMessage("HiddenCheckBoxMenuItemToolTip"));
732        hiddenCheckBoxMenuItem.setSelected(isHidden());
733
734        popupMenu.add(dashedCheckBoxMenuItem);
735        dashedCheckBoxMenuItem.addActionListener((java.awt.event.ActionEvent e3) -> setDashed(dashedCheckBoxMenuItem.isSelected()));
736        dashedCheckBoxMenuItem.setToolTipText(Bundle.getMessage("DashedCheckBoxMenuItemToolTip"));
737        dashedCheckBoxMenuItem.setSelected(dashed);
738
739        if (isArc()) {
740            popupMenu.add(flippedCheckBoxMenuItem);
741            flippedCheckBoxMenuItem.addActionListener((java.awt.event.ActionEvent e3) -> setFlip(flippedCheckBoxMenuItem.isSelected()));
742            flippedCheckBoxMenuItem.setToolTipText(Bundle.getMessage("FlippedCheckBoxMenuItemToolTip"));
743            flippedCheckBoxMenuItem.setSelected(isFlip());
744        }
745
746        //
747        // decorations menu
748        //
749        JMenu decorationsMenu = new JMenu(Bundle.getMessage("DecorationMenuTitle"));
750        decorationsMenu.setToolTipText(Bundle.getMessage("DecorationMenuToolTip"));
751
752        JCheckBoxMenuItem jcbmi;
753
754        //
755        // arrows menus
756        //
757        // arrows can only be added at edge connector
758        //
759        boolean hasEC1 = false;
760        if (getType1() == HitPointType.POS_POINT) {
761            PositionablePoint pp = (PositionablePoint) getConnect1();
762            if (pp.getType() == PositionablePoint.PointType.EDGE_CONNECTOR) {
763                hasEC1 = true;
764            }
765        }
766        boolean hasEC2 = false;
767        if (getType2() == HitPointType.POS_POINT) {
768            PositionablePoint pp = (PositionablePoint) getConnect2();
769            if (pp.getType() == PositionablePoint.PointType.EDGE_CONNECTOR) {
770                hasEC2 = true;
771            }
772        }
773        if (hasEC1 || hasEC2) {
774            JMenu arrowsMenu = new JMenu(Bundle.getMessage("ArrowsMenuTitle"));
775            decorationsMenu.setToolTipText(Bundle.getMessage("ArrowsMenuToolTip"));
776            decorationsMenu.add(arrowsMenu);
777
778            JMenu arrowsCountMenu = new JMenu(Bundle.getMessage("DecorationStyleMenuTitle"));
779            arrowsCountMenu.setToolTipText(Bundle.getMessage("DecorationStyleMenuToolTip"));
780            arrowsMenu.add(arrowsCountMenu);
781
782            jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationNoneMenuItemTitle"));
783            arrowsCountMenu.add(jcbmi);
784            jcbmi.setToolTipText(Bundle.getMessage("DecorationNoneMenuItemToolTip"));
785            jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
786                setArrowEndStart(false);
787                setArrowEndStop(false);
788                // setArrowStyle(0);
789            });
790            jcbmi.setSelected(arrowStyle == 0);
791
792            // configure the arrows
793            for (int i = 1; i < NUM_ARROW_TYPES; i++) {
794                jcbmi = loadArrowImageToJCBItem(i, arrowsCountMenu);
795                final int n = i;
796                jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
797                    setArrowEndStart((getType1() == HitPointType.POS_POINT) && (((PositionablePoint) getConnect1()).getType() == PositionablePoint.PointType.EDGE_CONNECTOR));
798                    setArrowEndStop((getType2() == HitPointType.POS_POINT) && (((PositionablePoint) getConnect2()).getType() == PositionablePoint.PointType.EDGE_CONNECTOR));
799                    setArrowStyle(n);
800                });
801                jcbmi.setSelected(arrowStyle == i);
802            }
803
804            if (hasEC1 && hasEC2) {
805                JMenu arrowsEndMenu = new JMenu(Bundle.getMessage("DecorationEndMenuTitle"));
806                arrowsEndMenu.setToolTipText(Bundle.getMessage("DecorationEndMenuToolTip"));
807                arrowsMenu.add(arrowsEndMenu);
808
809                jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationNoneMenuItemTitle"));
810                arrowsEndMenu.add(jcbmi);
811                jcbmi.setToolTipText(Bundle.getMessage("DecorationNoneMenuItemToolTip"));
812                jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
813                    setArrowEndStart(false);
814                    setArrowEndStop(false);
815                });
816                jcbmi.setSelected(!arrowEndStart && !arrowEndStop);
817
818                jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationStartMenuItemTitle"));
819                arrowsEndMenu.add(jcbmi);
820                jcbmi.setToolTipText(Bundle.getMessage("DecorationStartMenuItemToolTip"));
821                jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
822                    setArrowEndStart(true);
823                    setArrowEndStop(false);
824                });
825                jcbmi.setSelected(arrowEndStart && !arrowEndStop);
826
827                jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationEndMenuItemTitle"));
828                arrowsEndMenu.add(jcbmi);
829                jcbmi.setToolTipText(Bundle.getMessage("DecorationEndMenuItemToolTip"));
830                jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
831                    setArrowEndStop(true);
832                    setArrowEndStart(false);
833                });
834                jcbmi.setSelected(!arrowEndStart && arrowEndStop);
835
836                jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationBothMenuItemTitle"));
837                arrowsEndMenu.add(jcbmi);
838                jcbmi.setToolTipText(Bundle.getMessage("DecorationBothMenuItemToolTip"));
839                jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
840                    setArrowEndStart(true);
841                    setArrowEndStop(true);
842                });
843                jcbmi.setSelected(arrowEndStart && arrowEndStop);
844            }
845
846            JMenu arrowsDirMenu = new JMenu(Bundle.getMessage("ArrowsDirectionMenuTitle"));
847            arrowsDirMenu.setToolTipText(Bundle.getMessage("ArrowsDirectionMenuToolTip"));
848            arrowsMenu.add(arrowsDirMenu);
849
850            jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationNoneMenuItemTitle"));
851            arrowsDirMenu.add(jcbmi);
852            jcbmi.setToolTipText(Bundle.getMessage("DecorationNoneMenuItemToolTip"));
853            jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
854                setArrowDirIn(false);
855                setArrowDirOut(false);
856            });
857            jcbmi.setSelected(!arrowDirIn && !arrowDirOut);
858
859            jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("ArrowsDirectionInMenuItemTitle"));
860            arrowsDirMenu.add(jcbmi);
861            jcbmi.setToolTipText(Bundle.getMessage("ArrowsDirectionInMenuItemToolTip"));
862            jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
863                setArrowDirIn(true);
864                setArrowDirOut(false);
865            });
866            jcbmi.setSelected(arrowDirIn && !arrowDirOut);
867
868            jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("ArrowsDirectionOutMenuItemTitle"));
869            arrowsDirMenu.add(jcbmi);
870            jcbmi.setToolTipText(Bundle.getMessage("ArrowsDirectionOutMenuItemToolTip"));
871            jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
872                setArrowDirOut(true);
873                setArrowDirIn(false);
874            });
875            jcbmi.setSelected(!arrowDirIn && arrowDirOut);
876
877            jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("ArrowsDirectionBothMenuItemTitle"));
878            arrowsDirMenu.add(jcbmi);
879            jcbmi.setToolTipText(Bundle.getMessage("ArrowsDirectionBothMenuItemToolTip"));
880            jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
881                setArrowDirIn(true);
882                setArrowDirOut(true);
883            });
884            jcbmi.setSelected(arrowDirIn && arrowDirOut);
885
886            jmi = arrowsMenu.add(new JMenuItem(Bundle.getMessage("DecorationColorMenuItemTitle")));
887            jmi.setToolTipText(Bundle.getMessage("DecorationColorMenuItemToolTip"));
888            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
889                Color newColor = JmriColorChooser.showDialog(null, "Choose a color", arrowColor);
890                if ((newColor != null) && !newColor.equals(arrowColor)) {
891                    setArrowColor(newColor);
892                }
893            });
894            jmi.setForeground(arrowColor);
895            jmi.setBackground(ColorUtil.contrast(arrowColor));
896
897            jmi = arrowsMenu.add(new JMenuItem(Bundle.getMessage("MakeLabel",
898                    Bundle.getMessage("DecorationLineWidthMenuItemTitle")) + arrowLineWidth));
899            jmi.setToolTipText(Bundle.getMessage("DecorationLineWidthMenuItemToolTip"));
900            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
901                // prompt for arrow line width
902                int newValue = QuickPromptUtil.promptForInt(layoutEditor,
903                        Bundle.getMessage("DecorationLineWidthMenuItemTitle"),
904                        Bundle.getMessage("DecorationLineWidthMenuItemTitle"),
905                        arrowLineWidth);
906                setArrowLineWidth(newValue);
907            });
908
909            jmi = arrowsMenu.add(new JMenuItem(Bundle.getMessage("MakeLabel",
910                    Bundle.getMessage("DecorationLengthMenuItemTitle")) + arrowLength));
911            jmi.setToolTipText(Bundle.getMessage("DecorationLengthMenuItemToolTip"));
912            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
913                // prompt for arrow length
914                int newValue = QuickPromptUtil.promptForInt(layoutEditor,
915                        Bundle.getMessage("DecorationLengthMenuItemTitle"),
916                        Bundle.getMessage("DecorationLengthMenuItemTitle"),
917                        arrowLength);
918                setArrowLength(newValue);
919            });
920
921            jmi = arrowsMenu.add(new JMenuItem(Bundle.getMessage("MakeLabel",
922                    Bundle.getMessage("DecorationGapMenuItemTitle")) + arrowGap));
923            jmi.setToolTipText(Bundle.getMessage("DecorationGapMenuItemToolTip"));
924            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
925                // prompt for arrow gap
926                int newValue = QuickPromptUtil.promptForInt(layoutEditor,
927                        Bundle.getMessage("DecorationGapMenuItemTitle"),
928                        Bundle.getMessage("DecorationGapMenuItemTitle"),
929                        arrowGap);
930                setArrowGap(newValue);
931            });
932        }
933
934        //
935        // bridge menus
936        //
937        JMenu bridgeMenu = new JMenu(Bundle.getMessage("BridgeMenuTitle"));
938        decorationsMenu.setToolTipText(Bundle.getMessage("BridgeMenuToolTip"));
939        decorationsMenu.add(bridgeMenu);
940
941        JMenu bridgeSideMenu = new JMenu(Bundle.getMessage("DecorationSideMenuTitle"));
942        bridgeSideMenu.setToolTipText(Bundle.getMessage("DecorationSideMenuToolTip"));
943        bridgeMenu.add(bridgeSideMenu);
944
945        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationNoneMenuItemTitle"));
946        bridgeSideMenu.add(jcbmi);
947        jcbmi.setToolTipText(Bundle.getMessage("DecorationNoneMenuItemToolTip"));
948        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
949            setBridgeSideLeft(false);
950            setBridgeSideRight(false);
951        });
952        jcbmi.setSelected(!bridgeSideLeft && !bridgeSideRight);
953
954        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationSideLeftMenuItemTitle"));
955        bridgeSideMenu.add(jcbmi);
956        jcbmi.setToolTipText(Bundle.getMessage("DecorationSideLeftMenuItemToolTip"));
957        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
958            setBridgeSideLeft(true);
959            setBridgeSideRight(false);
960        });
961        jcbmi.setSelected(bridgeSideLeft && !bridgeSideRight);
962
963        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationSideRightMenuItemTitle"));
964        bridgeSideMenu.add(jcbmi);
965        jcbmi.setToolTipText(Bundle.getMessage("DecorationSideRightMenuItemToolTip"));
966        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
967            setBridgeSideRight(true);
968            setBridgeSideLeft(false);
969        });
970        jcbmi.setSelected(!bridgeSideLeft && bridgeSideRight);
971
972        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationBothMenuItemTitle"));
973        bridgeSideMenu.add(jcbmi);
974        jcbmi.setToolTipText(Bundle.getMessage("DecorationBothMenuItemToolTip"));
975        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
976            setBridgeSideLeft(true);
977            setBridgeSideRight(true);
978        });
979        jcbmi.setSelected(bridgeSideLeft && bridgeSideRight);
980
981        JMenu bridgeEndMenu = new JMenu(Bundle.getMessage("DecorationEndMenuTitle"));
982        bridgeEndMenu.setToolTipText(Bundle.getMessage("DecorationEndMenuToolTip"));
983        bridgeMenu.add(bridgeEndMenu);
984
985        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationNoneMenuItemTitle"));
986        bridgeEndMenu.add(jcbmi);
987        jcbmi.setToolTipText(Bundle.getMessage("DecorationNoneMenuItemToolTip"));
988        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
989            setBridgeHasEntry(false);
990            setBridgeHasExit(false);
991        });
992        jcbmi.setSelected(!bridgeHasEntry && !bridgeHasExit);
993
994        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationEntryMenuItemTitle"));
995        bridgeEndMenu.add(jcbmi);
996        jcbmi.setToolTipText(Bundle.getMessage("DecorationEntryMenuItemToolTip"));
997        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
998            setBridgeHasEntry(true);
999            setBridgeHasExit(false);
1000        });
1001        jcbmi.setSelected(bridgeHasEntry && !bridgeHasExit);
1002
1003        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationExitMenuItemTitle"));
1004        bridgeEndMenu.add(jcbmi);
1005        jcbmi.setToolTipText(Bundle.getMessage("DecorationExitMenuItemToolTip"));
1006        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1007            setBridgeHasExit(true);
1008            setBridgeHasEntry(false);
1009        });
1010        jcbmi.setSelected(!bridgeHasEntry && bridgeHasExit);
1011
1012        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationBothMenuItemTitle"));
1013        bridgeEndMenu.add(jcbmi);
1014        jcbmi.setToolTipText(Bundle.getMessage("DecorationBothMenuItemToolTip"));
1015        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1016            setBridgeHasEntry(true);
1017            setBridgeHasExit(true);
1018        });
1019        jcbmi.setSelected(bridgeHasEntry && bridgeHasExit);
1020
1021        jmi = bridgeMenu.add(new JMenuItem(Bundle.getMessage("DecorationColorMenuItemTitle")));
1022        jmi.setToolTipText(Bundle.getMessage("DecorationColorMenuItemToolTip"));
1023        jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1024            Color newColor = JmriColorChooser.showDialog(null, "Choose a color", bridgeColor);
1025            if ((newColor != null) && !newColor.equals(bridgeColor)) {
1026                setBridgeColor(newColor);
1027            }
1028        });
1029        jmi.setForeground(bridgeColor);
1030        jmi.setBackground(ColorUtil.contrast(bridgeColor));
1031
1032        addNumericMenuItem(bridgeMenu,
1033                "DecorationLineWidthMenuItemTitle", "DecorationLineWidthMenuItemToolTip",
1034                this::getBridgeLineWidth, this::setBridgeLineWidth,
1035                QuickPromptUtil.checkIntRange(1, MAX_BRIDGE_LINE_WIDTH, null));
1036
1037        addNumericMenuItem(bridgeMenu,
1038                "BridgeApproachWidthMenuItemTitle", "BridgeApproachWidthMenuItemToolTip",
1039                this::getBridgeApproachWidth, this::setBridgeApproachWidth,
1040                QuickPromptUtil.checkIntRange(4, MAX_BRIDGE_APPROACH_WIDTH, null));
1041
1042        addNumericMenuItem(bridgeMenu,
1043                "BridgeDeckWidthMenuItemTitle", "BridgeDeckWidthMenuItemToolTip",
1044                this::getBridgeDeckWidth, this::setBridgeDeckWidth,
1045                QuickPromptUtil.checkIntRange(1, MAX_BRIDGE_DECK_WIDTH, null));
1046
1047        //
1048        // end bumper menus
1049        //
1050        // end bumper decorations can only be on end bumpers
1051        //
1052        boolean hasEB1 = false;
1053        if (getType1() == HitPointType.POS_POINT) {
1054            PositionablePoint pp = (PositionablePoint) getConnect1();
1055            if (pp.getType() == PositionablePoint.PointType.END_BUMPER) {
1056                hasEB1 = true;
1057            }
1058        }
1059        boolean hasEB2 = false;
1060        if (getType2() == HitPointType.POS_POINT) {
1061            PositionablePoint pp = (PositionablePoint) getConnect2();
1062            if (pp.getType() == PositionablePoint.PointType.END_BUMPER) {
1063                hasEB2 = true;
1064            }
1065        }
1066        if (hasEB1 || hasEB2) {
1067            JMenu endBumperMenu = new JMenu(Bundle.getMessage("EndBumperMenuTitle"));
1068            decorationsMenu.setToolTipText(Bundle.getMessage("EndBumperMenuToolTip"));
1069            decorationsMenu.add(endBumperMenu);
1070
1071            if (hasEB1 && hasEB2) {
1072                JMenu endBumperEndMenu = new JMenu(Bundle.getMessage("DecorationEndMenuTitle"));
1073                endBumperEndMenu.setToolTipText(Bundle.getMessage("DecorationEndMenuToolTip"));
1074                endBumperMenu.add(endBumperEndMenu);
1075
1076                jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationNoneMenuItemTitle"));
1077                endBumperEndMenu.add(jcbmi);
1078                jcbmi.setToolTipText(Bundle.getMessage("DecorationNoneMenuItemToolTip"));
1079                jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1080                    setBumperEndStart(false);
1081                    setBumperEndStop(false);
1082                });
1083                jcbmi.setSelected(!bumperEndStart && !bumperEndStop);
1084
1085                jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationStartMenuItemTitle"));
1086                endBumperEndMenu.add(jcbmi);
1087                jcbmi.setToolTipText(Bundle.getMessage("DecorationStartMenuItemToolTip"));
1088                jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1089                    setBumperEndStart(true);
1090                    setBumperEndStop(false);
1091                });
1092                jcbmi.setSelected(bumperEndStart && !bumperEndStop);
1093
1094                jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationEndMenuItemTitle"));
1095                endBumperEndMenu.add(jcbmi);
1096                jcbmi.setToolTipText(Bundle.getMessage("DecorationEndMenuItemToolTip"));
1097                jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1098                    setBumperEndStart(false);
1099                    setBumperEndStop(true);
1100                });
1101                jcbmi.setSelected(!bumperEndStart && bumperEndStop);
1102
1103                jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationBothMenuItemTitle"));
1104                endBumperEndMenu.add(jcbmi);
1105                jcbmi.setToolTipText(Bundle.getMessage("DecorationEndMenuItemToolTip"));
1106                jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1107                    setBumperEndStart(true);
1108                    setBumperEndStop(true);
1109                });
1110                jcbmi.setSelected(bumperEndStart && bumperEndStop);
1111            } else {
1112                JCheckBoxMenuItem enableCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("EndBumperEnableMenuItemTitle"));
1113                enableCheckBoxMenuItem.setToolTipText(Bundle.getMessage("EndBumperEnableMenuItemToolTip"));
1114
1115                endBumperMenu.add(enableCheckBoxMenuItem);
1116                enableCheckBoxMenuItem.addActionListener((java.awt.event.ActionEvent e3) -> {
1117                    if ((getType1() == HitPointType.POS_POINT) && (((PositionablePoint) getConnect1()).getType() == PositionablePoint.PointType.END_BUMPER)) {
1118                        setBumperEndStart(enableCheckBoxMenuItem.isSelected());
1119                    }
1120                    if ((getType2() == HitPointType.POS_POINT) && (((PositionablePoint) getConnect2()).getType() == PositionablePoint.PointType.END_BUMPER)) {
1121                        setBumperEndStop(enableCheckBoxMenuItem.isSelected());
1122                    }
1123                });
1124                enableCheckBoxMenuItem.setSelected(bumperEndStart || bumperEndStop);
1125            }
1126
1127            jmi = endBumperMenu.add(new JMenuItem(Bundle.getMessage("DecorationColorMenuItemTitle")));
1128            jmi.setToolTipText(Bundle.getMessage("DecorationColorMenuItemToolTip"));
1129            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1130                Color newColor = JmriColorChooser.showDialog(null, "Choose a color", bumperColor);
1131                if ((newColor != null) && !newColor.equals(bumperColor)) {
1132                    setBumperColor(newColor);
1133                }
1134            });
1135            jmi.setForeground(bumperColor);
1136            jmi.setBackground(ColorUtil.contrast(bumperColor));
1137
1138            jmi = endBumperMenu.add(new JMenuItem(Bundle.getMessage("MakeLabel",
1139                    Bundle.getMessage("DecorationLineWidthMenuItemTitle")) + bumperLineWidth));
1140            jmi.setToolTipText(Bundle.getMessage("DecorationLineWidthMenuItemToolTip"));
1141            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1142                // prompt for width
1143                int newValue = QuickPromptUtil.promptForInteger(layoutEditor,
1144                        Bundle.getMessage("DecorationLineWidthMenuItemTitle"),
1145                        Bundle.getMessage("DecorationLineWidthMenuItemTitle"),
1146                        getBumperLineWidth(), t -> {
1147                            if (t < 0 || t > MAX_BUMPER_WIDTH) {
1148                                throw new IllegalArgumentException(
1149                                        Bundle.getMessage("DecorationLengthMenuItemRange", MAX_BUMPER_WIDTH));
1150                            }
1151                            return true;
1152                        });
1153                setBumperLineWidth(newValue);
1154            });
1155
1156            jmi = endBumperMenu.add(new JMenuItem(Bundle.getMessage("MakeLabel",
1157                    Bundle.getMessage("DecorationLengthMenuItemTitle")) + bumperLength));
1158            jmi.setToolTipText(Bundle.getMessage("DecorationLengthMenuItemToolTip"));
1159            jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1160                // prompt for length
1161                int newValue = QuickPromptUtil.promptForInteger(layoutEditor,
1162                        Bundle.getMessage("DecorationLengthMenuItemTitle"),
1163                        Bundle.getMessage("DecorationLengthMenuItemTitle"),
1164                        bumperLength, t -> {
1165                            if (t < 0 || t > MAX_BUMPER_LENGTH) {
1166                                throw new IllegalArgumentException(
1167                                        Bundle.getMessage("DecorationLengthMenuItemRange", MAX_BUMPER_LENGTH));
1168                            }
1169                            return true;
1170                        });
1171                setBumperLength(newValue);
1172            });
1173        }
1174
1175        //
1176        // tunnel menus
1177        //
1178        JMenu tunnelMenu = new JMenu(Bundle.getMessage("TunnelMenuTitle"));
1179        decorationsMenu.setToolTipText(Bundle.getMessage("TunnelMenuToolTip"));
1180        decorationsMenu.add(tunnelMenu);
1181
1182        JMenu tunnelSideMenu = new JMenu(Bundle.getMessage("DecorationSideMenuTitle"));
1183        tunnelSideMenu.setToolTipText(Bundle.getMessage("DecorationSideMenuToolTip"));
1184        tunnelMenu.add(tunnelSideMenu);
1185
1186        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationNoneMenuItemTitle"));
1187        tunnelSideMenu.add(jcbmi);
1188        jcbmi.setToolTipText(Bundle.getMessage("DecorationNoneMenuItemToolTip"));
1189        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1190            setTunnelSideLeft(false);
1191            setTunnelSideRight(false);
1192        });
1193        jcbmi.setSelected(!tunnelSideLeft && !tunnelSideRight);
1194
1195        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationSideLeftMenuItemTitle"));
1196        tunnelSideMenu.add(jcbmi);
1197        jcbmi.setToolTipText(Bundle.getMessage("DecorationSideLeftMenuItemToolTip"));
1198        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1199            setTunnelSideLeft(true);
1200            setTunnelSideRight(false);
1201        });
1202        jcbmi.setSelected(tunnelSideLeft && !tunnelSideRight);
1203
1204        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationSideRightMenuItemTitle"));
1205        tunnelSideMenu.add(jcbmi);
1206        jcbmi.setToolTipText(Bundle.getMessage("DecorationSideRightMenuItemToolTip"));
1207        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1208            setTunnelSideRight(true);
1209            setTunnelSideLeft(false);
1210        });
1211        jcbmi.setSelected(!tunnelSideLeft && tunnelSideRight);
1212
1213        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationBothMenuItemTitle"));
1214        tunnelSideMenu.add(jcbmi);
1215        jcbmi.setToolTipText(Bundle.getMessage("DecorationBothMenuItemToolTip"));
1216        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1217            setTunnelSideLeft(true);
1218            setTunnelSideRight(true);
1219        });
1220        jcbmi.setSelected(tunnelSideLeft && tunnelSideRight);
1221
1222        JMenu tunnelEndMenu = new JMenu(Bundle.getMessage("DecorationEndMenuTitle"));
1223        tunnelEndMenu.setToolTipText(Bundle.getMessage("DecorationEndMenuToolTip"));
1224        tunnelMenu.add(tunnelEndMenu);
1225
1226        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationNoneMenuItemTitle"));
1227        tunnelEndMenu.add(jcbmi);
1228        jcbmi.setToolTipText(Bundle.getMessage("DecorationNoneMenuItemToolTip"));
1229        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1230            setTunnelHasEntry(false);
1231            setTunnelHasExit(false);
1232        });
1233        jcbmi.setSelected(!tunnelHasEntry && !tunnelHasExit);
1234
1235        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationEntryMenuItemTitle"));
1236        tunnelEndMenu.add(jcbmi);
1237        jcbmi.setToolTipText(Bundle.getMessage("DecorationEntryMenuItemToolTip"));
1238        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1239            setTunnelHasEntry(true);
1240            setTunnelHasExit(false);
1241        });
1242        jcbmi.setSelected(tunnelHasEntry && !tunnelHasExit);
1243
1244        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationExitMenuItemTitle"));
1245        tunnelEndMenu.add(jcbmi);
1246        jcbmi.setToolTipText(Bundle.getMessage("DecorationExitMenuItemToolTip"));
1247        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1248            setTunnelHasExit(true);
1249            setTunnelHasEntry(false);
1250        });
1251        jcbmi.setSelected(!tunnelHasEntry && tunnelHasExit);
1252
1253        jcbmi = new JCheckBoxMenuItem(Bundle.getMessage("DecorationBothMenuItemTitle"));
1254        tunnelEndMenu.add(jcbmi);
1255        jcbmi.setToolTipText(Bundle.getMessage("DecorationBothMenuItemToolTip"));
1256        jcbmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1257            setTunnelHasEntry(true);
1258            setTunnelHasExit(true);
1259        });
1260        jcbmi.setSelected(tunnelHasEntry && tunnelHasExit);
1261
1262        jmi = tunnelMenu.add(new JMenuItem(Bundle.getMessage("DecorationColorMenuItemTitle")));
1263        jmi.setToolTipText(Bundle.getMessage("DecorationColorMenuItemToolTip"));
1264        jmi.addActionListener((java.awt.event.ActionEvent e3) -> {
1265            Color newColor = JmriColorChooser.showDialog(null, "Choose a color", tunnelColor);
1266            if ((newColor != null) && !newColor.equals(tunnelColor)) {
1267                setTunnelColor(newColor);
1268            }
1269        });
1270        jmi.setForeground(tunnelColor);
1271        jmi.setBackground(ColorUtil.contrast(tunnelColor));
1272
1273        addNumericMenuItem(tunnelMenu,
1274                "TunnelFloorWidthMenuItemTitle", "TunnelFloorWidthMenuItemToolTip",
1275                this::getTunnelFloorWidth, this::setTunnelFloorWidth,
1276                QuickPromptUtil.checkIntRange(1, MAX_TUNNEL_FLOOR_WIDTH, null));
1277        addNumericMenuItem(tunnelMenu,
1278                "DecorationLineWidthMenuItemTitle", "DecorationLineWidthMenuItemToolTip",
1279                this::getTunnelLineWidth, this::setTunnelLineWidth,
1280                QuickPromptUtil.checkIntRange(1, MAX_TUNNEL_LINE_WIDTH, null));
1281        addNumericMenuItem(tunnelMenu,
1282                "TunnelEntranceWidthMenuItemTitle", "TunnelEntranceWidthMenuItemToolTip",
1283                this::getTunnelEntranceWidth, this::setTunnelEntranceWidth,
1284                QuickPromptUtil.checkIntRange(1, MAX_TUNNEL_ENTRANCE_WIDTH, null));
1285
1286        popupMenu.add(decorationsMenu);
1287
1288        popupMenu.add(new JSeparator(JSeparator.HORIZONTAL));
1289        popupMenu.add(new AbstractAction(Bundle.getMessage("ButtonEdit")) {
1290            @Override
1291            public void actionPerformed(ActionEvent e) {
1292                editor.editLayoutTrack(TrackSegmentView.this);
1293            }
1294        });
1295        popupMenu.add(new AbstractAction(Bundle.getMessage("ButtonDelete")) {
1296            @Override
1297            public void actionPerformed(ActionEvent e) {
1298                if (canRemove() && removeInlineLogixNG()) {
1299                    layoutEditor.removeTrackSegment(trackSegment);
1300                    remove();
1301                    dispose();
1302                }
1303            }
1304        });
1305        popupMenu.add(new AbstractAction(Bundle.getMessage("SplitTrackSegment")) {
1306            @Override
1307            public void actionPerformed(ActionEvent e) {
1308                splitTrackSegment();
1309            }
1310        });
1311
1312        JMenu lineType = new JMenu(Bundle.getMessage("ChangeTo"));
1313        jmi = lineType.add(new JCheckBoxMenuItem(new AbstractAction(Bundle.getMessage("Line")) {
1314            @Override
1315            public void actionPerformed(ActionEvent e) {
1316                changeType(0);
1317            }
1318        }));
1319        jmi.setSelected(!isArc() && !isBezier());
1320
1321        jmi = lineType.add(new JCheckBoxMenuItem(new AbstractAction(Bundle.getMessage("Circle")) {
1322            @Override
1323            public void actionPerformed(ActionEvent e) {
1324                changeType(1);
1325            }
1326        }));
1327        jmi.setSelected(isArc() && isCircle());
1328
1329        jmi = lineType.add(new JCheckBoxMenuItem(new AbstractAction(Bundle.getMessage("Ellipse")) {
1330            @Override
1331            public void actionPerformed(ActionEvent e) {
1332                changeType(2);
1333            }
1334        }));
1335        jmi.setSelected(isArc() && !isCircle());
1336
1337        jmi = lineType.add(new JCheckBoxMenuItem(new AbstractAction(Bundle.getMessage("Bezier")) {
1338            @Override
1339            public void actionPerformed(ActionEvent e) {
1340                changeType(3);
1341            }
1342        }));
1343        jmi.setSelected(!isArc() && isBezier());
1344
1345        popupMenu.add(lineType);
1346
1347        if (isArc() || isBezier()) {
1348            if (hideConstructionLines()) {
1349                popupMenu.add(new AbstractAction(Bundle.getMessage("ShowConstruct")) {
1350                    @Override
1351                    public void actionPerformed(ActionEvent e) {
1352                        hideConstructionLines(SHOWCON);
1353                    }
1354                });
1355            } else {
1356                popupMenu.add(new AbstractAction(Bundle.getMessage("HideConstruct")) {
1357                    @Override
1358                    public void actionPerformed(ActionEvent e) {
1359                        hideConstructionLines(HIDECON);
1360                    }
1361                });
1362            }
1363        }
1364        if ((!getBlockName().isEmpty()) && (jmri.InstanceManager.getDefault(LayoutBlockManager.class).isAdvancedRoutingEnabled())) {
1365            popupMenu.add(new AbstractAction(Bundle.getMessage("ViewBlockRouting")) {
1366                @Override
1367                public void actionPerformed(ActionEvent e) {
1368                    AbstractAction routeTableAction = new LayoutBlockRouteTableAction("ViewRouting", getLayoutBlock());
1369                    routeTableAction.actionPerformed(e);
1370                }
1371            });
1372        }
1373        addCommonPopupItems(mouseEvent, popupMenu);
1374        popupMenu.show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
1375        return popupMenu;
1376    }   // showPopup
1377
1378    /**
1379     * {@inheritDoc}
1380     */
1381    @Override
1382    public boolean canRemove() {
1383        List<String> itemList = new ArrayList<>();
1384
1385        HitPointType type1Temp = getType1();
1386        LayoutTrack conn1Temp = getConnect1();
1387        itemList.addAll(getPointReferences(type1Temp, conn1Temp));
1388
1389        HitPointType type2Temp = getType2();
1390        LayoutTrack conn2Temp = getConnect2();
1391        itemList.addAll(getPointReferences(type2Temp, conn2Temp));
1392
1393        if (!itemList.isEmpty()) {
1394            displayRemoveWarningDialog(itemList, "TrackSegment");  // NOI18N
1395        }
1396        return itemList.isEmpty();
1397    }
1398
1399    public ArrayList<String> getPointReferences(HitPointType type, LayoutTrack conn) {
1400        ArrayList<String> result = new ArrayList<>();
1401
1402        if (type == HitPointType.POS_POINT && conn instanceof PositionablePoint) {
1403            PositionablePoint pt = (PositionablePoint) conn;
1404            if (!pt.getEastBoundSignal().isEmpty()) {
1405                result.add(pt.getEastBoundSignal());
1406            }
1407            if (!pt.getWestBoundSignal().isEmpty()) {
1408                result.add(pt.getWestBoundSignal());
1409            }
1410            if (!pt.getEastBoundSignalMastName().isEmpty()) {
1411                result.add(pt.getEastBoundSignalMastName());
1412            }
1413            if (!pt.getWestBoundSignalMastName().isEmpty()) {
1414                result.add(pt.getWestBoundSignalMastName());
1415            }
1416            if (!pt.getEastBoundSensorName().isEmpty()) {
1417                result.add(pt.getEastBoundSensorName());
1418            }
1419            if (!pt.getWestBoundSensorName().isEmpty()) {
1420                result.add(pt.getWestBoundSensorName());
1421            }
1422            if (pt.getType() == PositionablePoint.PointType.EDGE_CONNECTOR && pt.getLinkedPoint() != null) {
1423                result.add(Bundle.getMessage("DeleteECisActive"));   // NOI18N
1424            }
1425        }
1426
1427        if (HitPointType.isTurnoutHitType(type) && conn instanceof LayoutTurnout) {
1428            LayoutTurnout lt = (LayoutTurnout) conn;
1429            switch (type) {
1430                case TURNOUT_A: {
1431                    result = lt.getBeanReferences("A");  // NOI18N
1432                    break;
1433                }
1434                case TURNOUT_B: {
1435                    result = lt.getBeanReferences("B");  // NOI18N
1436                    break;
1437                }
1438                case TURNOUT_C: {
1439                    result = lt.getBeanReferences("C");  // NOI18N
1440                    break;
1441                }
1442                case TURNOUT_D: {
1443                    result = lt.getBeanReferences("D");  // NOI18N
1444                    break;
1445                }
1446                default: {
1447                    log.error("Unexpected HitPointType: {}", type);
1448                }
1449            }
1450        }
1451
1452        if (HitPointType.isLevelXingHitType(type) && conn instanceof LevelXing) {
1453            LevelXing lx = (LevelXing) conn;
1454            switch (type) {
1455                case LEVEL_XING_A: {
1456                    result = lx.getBeanReferences("A");  // NOI18N
1457                    break;
1458                }
1459                case LEVEL_XING_B: {
1460                    result = lx.getBeanReferences("B");  // NOI18N
1461                    break;
1462                }
1463                case LEVEL_XING_C: {
1464                    result = lx.getBeanReferences("C");  // NOI18N
1465                    break;
1466                }
1467                case LEVEL_XING_D: {
1468                    result = lx.getBeanReferences("D");  // NOI18N
1469                    break;
1470                }
1471                default: {
1472                    log.error("Unexpected HitPointType: {}", type);
1473                }
1474            }
1475        }
1476
1477        if (HitPointType.isSlipHitType(type) && conn instanceof LayoutSlip) {
1478            LayoutSlip ls = (LayoutSlip) conn;
1479            switch (type) {
1480                case SLIP_A: {
1481                    result = ls.getBeanReferences("A");  // NOI18N
1482                    break;
1483                }
1484                case SLIP_B: {
1485                    result = ls.getBeanReferences("B");  // NOI18N
1486                    break;
1487                }
1488                case SLIP_C: {
1489                    result = ls.getBeanReferences("C");  // NOI18N
1490                    break;
1491                }
1492                case SLIP_D: {
1493                    result = ls.getBeanReferences("D");  // NOI18N
1494                    break;
1495                }
1496                default: {
1497                    log.error("Unexpected HitPointType: {}", type);
1498                }
1499            }
1500        }
1501
1502        return result;
1503    }
1504
1505    /**
1506     * split a track segment into two track segments with an anchor point in between.
1507     */
1508    public void splitTrackSegment() {
1509        // create a new anchor
1510        Point2D p = getCentreSeg();
1511        PositionablePoint newAnchor = layoutEditor.addAnchor(p);
1512        // link it to me
1513        layoutEditor.setLink(newAnchor, HitPointType.POS_POINT, trackSegment, HitPointType.TRACK);
1514
1515        // get unique name for a new track segment
1516        String name = layoutEditor.getFinder().uniqueName("T", 1);
1517
1518        // create it between the new anchor and my getConnect2()(/type2)
1519        TrackSegment newTrackSegment = new TrackSegment(name,
1520                newAnchor, HitPointType.POS_POINT,
1521                getConnect2(), getType2(),
1522                trackSegment.isMainline(), layoutEditor);
1523        TrackSegmentView ntsv = new TrackSegmentView(newTrackSegment,
1524                layoutEditor);
1525        // add it to known tracks
1526        layoutEditor.addLayoutTrack(newTrackSegment, ntsv);
1527        layoutEditor.setDirty();
1528
1529        // copy attributes to new track segment
1530        newTrackSegment.setLayoutBlock(this.getLayoutBlock());
1531        ntsv.setArc(this.isArc());
1532        ntsv.setCircle(this.isCircle());
1533        // split any angle between the two new track segments
1534        ntsv.setAngle(this.getAngle() / 2.0);
1535        this.setAngle(this.getAngle() / 2.0);
1536        // newTrackSegment.setBezier(this.isBezier());
1537        ntsv.setFlip(this.isFlip());
1538        ntsv.setDashed(this.isDashed());
1539
1540        // copy over decorations
1541        Map<String, String> d = new HashMap<>();
1542        this.getDecorations().forEach((k, v) -> {
1543            if (k.equals("arrow")) {                // if this is an arrow
1544                if (this.isArrowEndStop()) {        // and it's on the stop end
1545                    d.put(k, v);                    // copy it to new track
1546                    this.setArrowEndStop(false);    // and remove it from this track
1547                }
1548            } else if (k.equals("bumper")) {        // if this is an end bumper
1549                if (this.isBumperEndStop()) {       // amd it's on the stop end
1550                    d.put(k, v);                    // copy it to new track
1551                    this.setBumperEndStop(false);   // and remove it from this track
1552                }
1553            } else {                                // otherwise...
1554                d.put(k, v);                        // copy to new track
1555            }
1556        });
1557        ntsv.setDecorations(d);
1558
1559        // link my getConnect2() to the new track segment
1560        if (getConnect2() instanceof PositionablePoint) {
1561            PositionablePoint pp = (PositionablePoint) getConnect2();
1562            pp.replaceTrackConnection(trackSegment, newTrackSegment);
1563        } else {
1564            layoutEditor.setLink(getConnect2(), getType2(), newTrackSegment, HitPointType.TRACK);
1565        }
1566
1567        // link the new anchor to the new track segment
1568        layoutEditor.setLink(newAnchor, HitPointType.POS_POINT, newTrackSegment, HitPointType.TRACK);
1569
1570        // link me to the new newAnchor
1571        trackSegment.setConnect2(newAnchor, HitPointType.POS_POINT);
1572
1573        // check on layout block
1574        LayoutBlock b = this.getLayoutBlock();
1575
1576        if (b != null) {
1577            newTrackSegment.setLayoutBlock(b);
1578            layoutEditor.getLEAuxTools().setBlockConnectivityChanged();
1579            newTrackSegment.updateBlockInfo();
1580        }
1581        layoutEditor.setDirty();
1582        layoutEditor.redrawPanel();
1583    }   // splitTrackSegment
1584
1585    /**
1586     * Display popup menu for information and editing.
1587     *
1588     * @param e            The original event causing this
1589     * @param hitPointType the type of the underlying hit
1590     */
1591    protected void showBezierPopUp(JmriMouseEvent e, HitPointType hitPointType) {
1592        int bezierControlPointIndex = hitPointType.bezierPointIndex();
1593        if (popupMenu != null) {
1594            popupMenu.removeAll();
1595        } else {
1596            popupMenu = new JPopupMenu();
1597        }
1598
1599        JMenuItem jmi = popupMenu.add(Bundle.getMessage("BezierControlPoint") + " #" + bezierControlPointIndex);
1600        jmi.setEnabled(false);
1601        popupMenu.add(new JSeparator(JSeparator.HORIZONTAL));
1602
1603        if (bezierControlPoints.size() <= HitPointType.NUM_BEZIER_CONTROL_POINTS) {
1604            popupMenu.add(new AbstractAction(Bundle.getMessage("AddBezierControlPointAfter")) {
1605
1606                @Override
1607                public void actionPerformed(ActionEvent e) {
1608                    addBezierControlPointAfter(bezierControlPointIndex);
1609                }
1610            });
1611            popupMenu.add(new AbstractAction(Bundle.getMessage("AddBezierControlPointBefore")) {
1612
1613                @Override
1614                public void actionPerformed(ActionEvent e) {
1615                    addBezierControlPointBefore(bezierControlPointIndex);
1616                }
1617            });
1618        }
1619
1620        if (bezierControlPoints.size() > 2) {
1621            popupMenu.add(new AbstractAction(Bundle.getMessage("DeleteBezierControlPoint") + " #" + bezierControlPointIndex) {
1622
1623                @Override
1624                public void actionPerformed(ActionEvent e) {
1625                    deleteBezierControlPoint(bezierControlPointIndex);
1626                }
1627            });
1628        }
1629        addCommonPopupItems(e, popupMenu);
1630        popupMenu.show(e.getComponent(), e.getX(), e.getY());
1631    }
1632
1633    private void addBezierControlPointBefore(int index) {
1634        Point2D addPoint = getBezierControlPoint(index);
1635        if (index > 0) {
1636            addPoint = MathUtil.midPoint(getBezierControlPoint(index - 1), addPoint);
1637        } else {
1638            Point2D ep1 = layoutEditor.getCoords(getConnect1(), getType1());
1639            addPoint = MathUtil.midPoint(ep1, addPoint);
1640        }
1641        bezierControlPoints.add(index, addPoint);
1642        layoutEditor.redrawPanel();
1643        layoutEditor.setDirty();
1644    }
1645
1646    private void addBezierControlPointAfter(int index) {
1647        int cnt = bezierControlPoints.size();
1648        Point2D addPoint = getBezierControlPoint(index);
1649        if (index < cnt - 1) {
1650            addPoint = MathUtil.midPoint(addPoint, getBezierControlPoint(index + 1));
1651            bezierControlPoints.add(index + 1, addPoint);
1652        } else {
1653            Point2D ep2 = layoutEditor.getCoords(getConnect2(), getType2());
1654            addPoint = MathUtil.midPoint(addPoint, ep2);
1655            bezierControlPoints.add(addPoint);
1656        }
1657        layoutEditor.redrawPanel();
1658        layoutEditor.setDirty();
1659    }
1660
1661    private void deleteBezierControlPoint(int index) {
1662        if ((index >= 0) && (index < bezierControlPoints.size())) {
1663            bezierControlPoints.remove(index);
1664            layoutEditor.redrawPanel();
1665            layoutEditor.setDirty();
1666        }
1667    }
1668
1669    void changeType(int choice) {
1670        switch (choice) {
1671            case 0: // plain track segment (line)
1672                setArc(false);
1673                setAngle(0.0D);
1674                setCircle(false);
1675                setBezier(false);
1676                break;
1677            case 1: // circle
1678                setCircle(true);
1679                setArc(true);
1680//                setAngle(90.0D);
1681//                setBezier(false); // this is done in setCircle
1682                break;
1683            case 2: // arc
1684                setArc(true);
1685                setAngle(90.0D);
1686                setCircle(false);
1687                setBezier(false);
1688                break;
1689            case 3:
1690                setArc(false);  // bezier
1691                setCircle(false);
1692                if (bezierControlPoints.size() == 0) {
1693                    // TODO: Use MathUtil.intersect to find intersection of adjacent tracks
1694                    // TODO: and place the control points halfway between that and the two endpoints
1695
1696                    // set default control point displacements
1697                    Point2D ep1 = layoutEditor.getCoords(getConnect1(), getType1());
1698                    Point2D ep2 = layoutEditor.getCoords(getConnect2(), getType2());
1699
1700                    // compute orthogonal offset0 with length one third the distance from ep1 to ep2
1701                    Point2D offset = MathUtil.subtract(ep2, ep1);
1702                    offset = MathUtil.normalize(offset, MathUtil.length(offset) / 3.0);
1703                    offset = MathUtil.orthogonal(offset);
1704
1705                    // add & subtract orthogonal offset0 to 1/3rd and 2/3rd points
1706                    Point2D pt1 = MathUtil.add(MathUtil.oneThirdPoint(ep1, ep2), offset);
1707                    Point2D pt2 = MathUtil.subtract(MathUtil.twoThirdsPoint(ep1, ep2), offset);
1708
1709                    bezierControlPoints.add(pt1);
1710                    bezierControlPoints.add(pt2);
1711                }
1712                setBezier(true);    // do this last (it calls reCenter())
1713                break;
1714            default:
1715                break;
1716        }
1717        layoutEditor.redrawPanel();
1718        layoutEditor.setDirty();
1719    }
1720
1721    /**
1722     * Clean up when this object is no longer needed.
1723     * <p>
1724     * Should not be called while the object is still displayed.
1725     *
1726     * @see #remove()
1727     */
1728    public void dispose() {
1729        if (popupMenu != null) {
1730            popupMenu.removeAll();
1731        }
1732        popupMenu = null;
1733    }
1734
1735    /**
1736     * Remove this object from display and persistance.
1737     */
1738    public void remove() {
1739        // remove from persistance by flagging inactive
1740        active = false;
1741    }
1742
1743    private boolean active = true;
1744
1745    /**
1746     * Get state. "active" means that the object is still displayed, and should
1747     * be stored.
1748     *
1749     * @return true if active
1750     */
1751    public boolean isActive() {
1752        return active;
1753    }
1754
1755    public static final int SHOWCON = 0x01;
1756    public static final int HIDECON = 0x02;     // flag set on a segment basis.
1757    public static final int HIDECONALL = 0x04;  // Used by layout editor for hiding all
1758
1759    public int showConstructionLine = SHOWCON;
1760
1761    /**
1762     * @return true if HIDECON is not set and HIDECONALL is not set
1763     */
1764    public boolean isShowConstructionLines() {
1765        return (((showConstructionLine & HIDECON) != HIDECON)
1766                && ((showConstructionLine & HIDECONALL) != HIDECONALL));
1767    }
1768
1769    /**
1770     * Method used by LayoutEditor.
1771     * <p>
1772     * If the argument is
1773     * <ul>
1774     * <li>HIDECONALL then set HIDECONALL
1775     * <li>SHOWCON reset HIDECONALL is set, other wise set SHOWCON
1776     * <li>HIDECON or otherwise set HIDECON
1777     * </ul>
1778     * Then always redraw the LayoutEditor panel and set it dirty.
1779     *
1780     * @param hide The specification i.e. HIDECONALL, SHOWCON et al
1781     */
1782    public void hideConstructionLines(int hide) {
1783        if (hide == HIDECONALL) {
1784            showConstructionLine |= HIDECONALL;
1785        } else if (hide == SHOWCON) {
1786            if ((showConstructionLine & HIDECONALL) == HIDECONALL) {
1787                showConstructionLine &= ~HIDECONALL;
1788            } else {
1789                showConstructionLine = hide;
1790            }
1791        } else {
1792            showConstructionLine = HIDECON;
1793        }
1794        layoutEditor.redrawPanel();
1795        layoutEditor.setDirty();
1796    }
1797
1798    /**
1799     * @return true if SHOWCON is not set
1800     */
1801    public boolean hideConstructionLines() {
1802        return ((showConstructionLine & SHOWCON) != SHOWCON);
1803    }
1804
1805    /**
1806     * The following are used only as a local store after a circle or arc has
1807     * been calculated. This prevents the need to recalculate the values each
1808     * time a re-draw is required.
1809     */
1810    private Point2D pt1;
1811    private Point2D pt2;
1812
1813    public Point2D getTmpPt1() {
1814        return pt1;
1815    }
1816
1817    public Point2D getTmpPt2() {
1818        return pt2;
1819    }
1820
1821    public void setTmpPt1(Point2D Pt1) {
1822        pt1 = Pt1;
1823        changed = true;
1824    }
1825
1826    public void setTmpPt2(Point2D Pt2) {
1827        pt2 = Pt2;
1828        changed = true;
1829    }
1830
1831    private double cX;
1832
1833    public double getCX() {
1834        return cX;
1835    }
1836
1837    public void setCX(double CX) {
1838        cX = CX;
1839    }
1840
1841    private double cY;
1842
1843    public double getCY() {
1844        return cY;
1845    }
1846
1847    public void setCY(double CY) {
1848        cY = CY;
1849    }
1850
1851    private double cW;
1852
1853    public double getCW() {
1854        return cW;
1855    }
1856
1857    public void setCW(double CW) {
1858        cW = CW;
1859    }
1860
1861    private double cH;
1862
1863    public double getCH() {
1864        return cH;
1865    }
1866
1867    public void setCH(double CH) {
1868        cH = CH;
1869    }
1870
1871    private double startAdj;
1872
1873    public double getStartAdj() {
1874        return startAdj;
1875    }
1876
1877    public void setStartAdj(double startAdj) {
1878        this.startAdj = startAdj;
1879    }
1880
1881    // this is the center of the track segment (it is "on" the track segment)
1882    public double getCentreSegX() {
1883        return getCentreSeg().getX();
1884    }
1885
1886    public void setCentreSegX(double x) {
1887        super.setCoordsCenter(new Point2D.Double(x, getCentreSeg().getY()));
1888    }
1889
1890    public double getCentreSegY() {
1891        return getCentreSeg().getY();
1892    }
1893
1894    public void setCentreSegY(double y) {
1895        super.setCoordsCenter(new Point2D.Double(getCentreSeg().getX(), y));
1896    }
1897
1898    /**
1899     * @return the location of the middle of the segment (on the segment)
1900     */
1901    public Point2D getCentreSeg() {
1902        Point2D result = MathUtil.zeroPoint2D;
1903
1904        if ((getConnect1() != null) && (getConnect2() != null)) {
1905            // get the end points
1906            Point2D ep1 = layoutEditor.getCoords(getConnect1(), getType1());
1907            Point2D ep2 = layoutEditor.getCoords(getConnect2(), getType2());
1908
1909            if (isCircle()) {
1910                result = getCoordsCenter();
1911            } else if (isArc()) {
1912                super.setCoordsCenter(MathUtil.midPoint(ep1, ep2));
1913                if (isFlip()) {
1914                    Point2D t = ep1;
1915                    ep1 = ep2;
1916                    ep2 = t;
1917                }
1918                Point2D delta = MathUtil.subtract(ep1, ep2);
1919                // are they of the same sign?
1920                if ((delta.getX() >= 0.0) != (delta.getY() >= 0.0)) {
1921                    delta = MathUtil.divide(delta, +5.0, -5.0);
1922                } else {
1923                    delta = MathUtil.divide(delta, -5.0, +5.0);
1924                }
1925                result = MathUtil.add(getCoordsCenter(), delta);
1926            } else if (isBezier()) {
1927                // compute result Bezier point for (t == 0.5);
1928                // copy all the control points (including end points) into an array
1929                int len = bezierControlPoints.size() + 2;
1930                Point2D[] points = new Point2D[len];
1931                points[0] = ep1;
1932                for (int idx = 1; idx < len - 1; idx++) {
1933                    points[idx] = bezierControlPoints.get(idx - 1);
1934                }
1935                points[len - 1] = ep2;
1936
1937                // calculate midpoints of all points (len - 1 order times)
1938                for (int idx = len - 1; idx > 0; idx--) {
1939                    for (int jdx = 0; jdx < idx; jdx++) {
1940                        points[jdx] = MathUtil.midPoint(points[jdx], points[jdx + 1]);
1941                    }
1942                }
1943                result = points[0];
1944            } else {
1945                result = MathUtil.midPoint(ep1, ep2);
1946            }
1947            super.setCoordsCenter(result);
1948        }
1949        return result;
1950    }
1951
1952    public void setCentreSeg(Point2D p) {
1953        super.setCoordsCenter(p);
1954    }
1955
1956    // this is the center of the track segment when configured as a circle
1957    private double centreX;
1958
1959    public double getCentreX() {
1960        return centreX;
1961    }
1962
1963    public void setCentreX(double x) {
1964        centreX = x;
1965    }
1966
1967    private double centreY;
1968
1969    public double getCentreY() {
1970        return centreY;
1971    }
1972
1973    public void setCentreY(double y) {
1974        centreY = y;
1975    }
1976
1977    public Point2D getCentre() {
1978        return new Point2D.Double(centreX, centreY);
1979    }
1980
1981    private double tmpangle;
1982
1983    public double getTmpAngle() {
1984        return tmpangle;
1985    }
1986
1987    public void setTmpAngle(double a) {
1988        tmpangle = a;
1989    }
1990
1991    /**
1992     * get center coordinates
1993     *
1994     * @return the center coordinates
1995     */
1996    public Point2D getCoordsCenterCircle() {
1997        return getCentre();
1998    }
1999
2000    /**
2001     * set center coordinates
2002     *
2003     * @param p the coordinates to set
2004     */
2005    public void setCoordsCenterCircle(Point2D p) {
2006        centreX = p.getX();
2007        centreY = p.getY();
2008    }
2009
2010    private double chordLength;
2011
2012    public double getChordLength() {
2013        return chordLength;
2014    }
2015
2016    public void setChordLength(double chord) {
2017        chordLength = chord;
2018    }
2019
2020    /*
2021    * Called when the user changes the angle dynamically in edit mode
2022    * by dragging the centre of the cirle.
2023     */
2024    protected void reCalculateTrackSegmentAngle(double x, double y) {
2025        if (!isBezier()) {
2026            double pt2x;
2027            double pt2y;
2028            double pt1x;
2029            double pt1y;
2030
2031            if (isFlip()) {
2032                pt1x = getTmpPt2().getX();
2033                pt1y = getTmpPt2().getY();
2034                pt2x = getTmpPt1().getX();
2035                pt2y = getTmpPt1().getY();
2036            } else {
2037                pt1x = getTmpPt1().getX();
2038                pt1y = getTmpPt1().getY();
2039                pt2x = getTmpPt2().getX();
2040                pt2y = getTmpPt2().getY();
2041            }
2042            // Point 1 to new point distance
2043            double a;
2044            double o;
2045            double la;
2046            // Compute arc's chord
2047            a = pt2x - x;
2048            o = pt2y - y;
2049            la = Math.hypot(a, o);
2050
2051            double lb;
2052            a = pt1x - x;
2053            o = pt1y - y;
2054            lb = Math.hypot(a, o);
2055
2056            double newangle = Math.toDegrees(Math.acos((-getChordLength() * getChordLength() + la * la + lb * lb) / (2 * la * lb)));
2057            setAngle(newangle);
2058        }
2059    }
2060
2061    /*
2062    * Calculate the initally parameters for drawing a circular track segment.
2063     */
2064    protected void calculateTrackSegmentAngle() {
2065        Point2D pt1, pt2;
2066        if (isFlip()) {
2067            pt1 = layoutEditor.getCoords(getConnect2(), getType2());
2068            pt2 = layoutEditor.getCoords(getConnect1(), getType1());
2069        } else {
2070            pt1 = layoutEditor.getCoords(getConnect1(), getType1());
2071            pt2 = layoutEditor.getCoords(getConnect2(), getType2());
2072        }
2073        if ((getTmpPt1() != pt1) || (getTmpPt2() != pt2) || trackNeedsRedraw()) {
2074            setTmpPt1(pt1);
2075            setTmpPt2(pt2);
2076
2077            double pt1x = pt1.getX();
2078            double pt1y = pt1.getY();
2079            double pt2x = pt2.getX();
2080            double pt2y = pt2.getY();
2081
2082            if (getAngle() == 0.0D) {
2083                setTmpAngle(90.0D);
2084            } else {
2085                setTmpAngle(getAngle());
2086            }
2087            // Convert angle to radiants in order to speed up math
2088            double halfAngleRAD = Math.toRadians(getTmpAngle()) / 2.D;
2089
2090            // Compute arc's chord
2091            double a = pt2x - pt1x;
2092            double o = pt2y - pt1y;
2093            double chord = Math.hypot(a, o);
2094            setChordLength(chord);
2095
2096            // Make sure chord is not null
2097            // In such a case (ep1 == ep2), there is no arc to draw
2098            if (chord > 0.D) {
2099                double radius = (chord / 2.D) / Math.sin(halfAngleRAD);
2100                // Circle
2101                double startRad = Math.atan2(a, o) - halfAngleRAD;
2102                setStartAdj(Math.toDegrees(startRad));
2103                if (isCircle()) {
2104                    // Circle - Compute center
2105                    setCentreX(pt2x - Math.cos(startRad) * radius);
2106                    setCentreY(pt2y + Math.sin(startRad) * radius);
2107
2108                    // Circle - Compute rectangle required by Arc2D.Double
2109                    setCW(radius * 2.0D);
2110                    setCH(radius * 2.0D);
2111                    setCX(getCentreX() - radius);
2112                    setCY(getCentreY() - radius);
2113
2114                    // Compute where to locate the control circle on the circle segment
2115                    Point2D offset = new Point2D.Double(
2116                            +radius * Math.cos(startRad + halfAngleRAD),
2117                            -radius * Math.sin(startRad + halfAngleRAD));
2118                    setCentreSeg(MathUtil.add(getCentre(), offset));
2119                } else {
2120                    // Ellipse - Round start angle to the closest multiple of 90
2121                    setStartAdj(Math.round(getStartAdj() / 90.0D) * 90.0D);
2122                    // Ellipse - Compute rectangle required by Arc2D.Double
2123                    setCW(Math.abs(a) * 2.0D);
2124                    setCH(Math.abs(o) * 2.0D);
2125                    // Ellipse - Adjust rectangle corner, depending on quadrant
2126                    if (o * a < 0.0D) {
2127                        a = -a;
2128                    } else {
2129                        o = -o;
2130                    }
2131                    setCX(Math.min(pt1x, pt2x) - Math.max(a, 0.0D));
2132                    setCY(Math.min(pt1y, pt2y) - Math.max(o, 0.0D));
2133                }
2134            }
2135        }
2136    }   // calculateTrackSegmentAngle
2137
2138    /**
2139     * {@inheritDoc}
2140     */
2141    @Override
2142    protected void draw1(Graphics2D g2, boolean isMain, boolean isBlock) {
2143//   if (getName().equals("T15")) {
2144//       log.debug("STOP");
2145//   }
2146        if (!isBlock && isDashed() && getLayoutBlock() != null) {
2147            // Skip the dashed rail layer, the block layer will display the dashed track
2148            // This removes random rail fragments from between the block dashes
2149            return;
2150        }
2151        if (isMain == trackSegment.isMainline()) {
2152            if (isBlock) {
2153                setColorForTrackBlock(g2, getLayoutBlock());
2154            }
2155            if (isArc()) {
2156                calculateTrackSegmentAngle();
2157                g2.draw(new Arc2D.Double(getCX(), getCY(), getCW(), getCH(), getStartAdj(), getTmpAngle(), Arc2D.OPEN));
2158                trackRedrawn();
2159            } else if (isBezier()) {
2160                Point2D[] points = getBezierPoints();
2161                MathUtil.drawBezier(g2, points);
2162            } else {
2163                Point2D end1 = layoutEditor.getCoords(
2164                        getConnect1(),
2165                        getType1());
2166                Point2D end2 = layoutEditor.getCoords(
2167                        getConnect2(),
2168                        getType2());
2169
2170                g2.draw(new Line2D.Double(end1, end2));
2171            }
2172        }
2173    }
2174
2175    /**
2176     * {@inheritDoc}
2177     */
2178    @Override
2179    protected void draw2(Graphics2D g2, boolean isMain, float railDisplacement) {
2180//   if (getName().equals("T5")) {
2181//       log.debug("STOP");
2182//   }
2183        if (isDashed() && getLayoutBlock() != null) {
2184            // Skip the dashed rail layer, the block layer will display the dashed track
2185            // This removes random rail fragments from between the block dashes
2186            return;
2187        }
2188        if (isMain == trackSegment.isMainline()) {
2189            if (isArc()) {
2190                calculateTrackSegmentAngle();
2191                Rectangle2D cRectangle2D = new Rectangle2D.Double(
2192                        getCX(), getCY(), getCW(), getCH());
2193                Rectangle2D tRectangle2D = MathUtil.inset(cRectangle2D, -railDisplacement);
2194                double startAdj = getStartAdj(), tmpAngle = getTmpAngle();
2195                g2.draw(new Arc2D.Double(tRectangle2D.getX(), tRectangle2D.getY(),
2196                        tRectangle2D.getWidth(), tRectangle2D.getHeight(),
2197                        startAdj, tmpAngle, Arc2D.OPEN));
2198                tRectangle2D = MathUtil.inset(cRectangle2D, +railDisplacement);
2199                g2.draw(new Arc2D.Double(tRectangle2D.getX(), tRectangle2D.getY(),
2200                        tRectangle2D.getWidth(), tRectangle2D.getHeight(),
2201                        startAdj, tmpAngle, Arc2D.OPEN));
2202                trackRedrawn();
2203            } else if (isBezier()) {
2204                Point2D[] points = getBezierPoints();
2205                MathUtil.drawBezier(g2, points, -railDisplacement);
2206                MathUtil.drawBezier(g2, points, +railDisplacement);
2207            } else {
2208                Point2D end1 = layoutEditor.getCoords(getConnect1(), getType1());
2209                Point2D end2 = layoutEditor.getCoords(getConnect2(), getType2());
2210
2211                Point2D delta = MathUtil.subtract(end2, end1);
2212                Point2D vector = MathUtil.normalize(delta, railDisplacement);
2213                vector = MathUtil.orthogonal(vector);
2214
2215                Point2D ep1L = MathUtil.add(end1, vector);
2216                Point2D ep2L = MathUtil.add(end2, vector);
2217                g2.draw(new Line2D.Double(ep1L, ep2L));
2218
2219                Point2D ep1R = MathUtil.subtract(end1, vector);
2220                Point2D ep2R = MathUtil.subtract(end2, vector);
2221                g2.draw(new Line2D.Double(ep1R, ep2R));
2222            }
2223        }
2224    }
2225
2226    /**
2227     * {@inheritDoc}
2228     */
2229    @Override
2230    protected void highlightUnconnected(Graphics2D g2, HitPointType selectedType) {
2231        // TrackSegments are always connected
2232        // nothing to see here... move along...
2233    }
2234
2235    @Override
2236    protected void drawEditControls(Graphics2D g2) {
2237        g2.setColor(Color.black);
2238        if (isShowConstructionLines()) {
2239            Point2D ep1 = layoutEditor.getCoords(getConnect1(), getType1());
2240            Point2D ep2 = layoutEditor.getCoords(getConnect2(), getType2());
2241            if (isCircle()) {
2242                // draw radiuses
2243                Point2D circleCenterPoint = getCoordsCenterCircle();
2244                g2.draw(new Line2D.Double(circleCenterPoint, ep1));
2245                g2.draw(new Line2D.Double(circleCenterPoint, ep2));
2246                // Draw a circle and square at the circles centre, that
2247                // allows the user to change the angle by dragging the mouse.
2248                g2.draw(trackEditControlCircleAt(circleCenterPoint));
2249                g2.draw(layoutEditor.layoutEditorControlRectAt(circleCenterPoint));
2250            } else if (isBezier()) {
2251                // draw construction lines and control circles
2252                Point2D lastPt = ep1;
2253                for (Point2D bcp : bezierControlPoints) {
2254                    g2.draw(new Line2D.Double(lastPt, bcp));
2255                    lastPt = bcp;
2256                    g2.draw(layoutEditor.layoutEditorControlRectAt(bcp));
2257                }
2258                g2.draw(new Line2D.Double(lastPt, ep2));
2259            }
2260        }
2261        g2.draw(trackEditControlCircleAt(getCentreSeg()));
2262    }   // drawEditControls
2263
2264    @Override
2265    protected void drawTurnoutControls(Graphics2D g2) {
2266        // TrackSegments don't have turnout controls...
2267        // nothing to see here... move along...
2268    }
2269
2270    /**
2271     * {@inheritDoc}
2272     */
2273    @Override
2274    public void reCheckBlockBoundary() {
2275        // nothing to see here... move along...
2276    }
2277
2278    /**
2279     * {@inheritDoc}
2280     */
2281    @Override
2282    protected void drawDecorations(Graphics2D g2) {
2283
2284        log.trace("TrackSegmentView: drawDecorations arrowStyle {}", arrowStyle);
2285// get end points and calculate start/stop angles (in radians)
2286        Point2D ep1 = layoutEditor.getCoords(getConnect1(), getType1());
2287        Point2D ep2 = layoutEditor.getCoords(getConnect2(), getType2());
2288        Point2D p1, p2, p3, p4, p5, p6, p7;
2289        Point2D p1P = ep1, p2P = ep2, p3P, p4P, p5P, p6P, p7P;
2290        double startAngleRAD, stopAngleRAD;
2291        if (isArc()) {
2292            calculateTrackSegmentAngle();
2293            double startAngleDEG = getStartAdj(), extentAngleDEG = getTmpAngle();
2294            startAngleRAD = (Math.PI / 2.D) - Math.toRadians(startAngleDEG);
2295            stopAngleRAD = (Math.PI / 2.D) - Math.toRadians(startAngleDEG + extentAngleDEG);
2296            if (isFlip()) {
2297                startAngleRAD += Math.PI;
2298                stopAngleRAD += Math.PI;
2299            } else {
2300                double temp = startAngleRAD;
2301                startAngleRAD = stopAngleRAD;
2302                stopAngleRAD = temp;
2303            }
2304        } else if (isBezier()) {
2305            Point2D cp0 = bezierControlPoints.get(0);
2306            Point2D cpN = bezierControlPoints.get(bezierControlPoints.size() - 1);
2307            startAngleRAD = (Math.PI / 2.D) - MathUtil.computeAngleRAD(cp0, ep1);
2308            stopAngleRAD = (Math.PI / 2.D) - MathUtil.computeAngleRAD(ep2, cpN);
2309        } else {
2310            startAngleRAD = (Math.PI / 2.D) - MathUtil.computeAngleRAD(ep2, ep1);
2311            stopAngleRAD = startAngleRAD;
2312        }
2313
2314//
2315// arrow decorations
2316//
2317        if (arrowStyle > 0) {
2318            log.trace("arrowstyle>0 with width {}", arrowLineWidth);
2319            g2.setStroke(new BasicStroke(arrowLineWidth,
2320                    BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.F));
2321            g2.setColor(arrowColor);
2322
2323            // draw the start arrows
2324            int offset = 1;
2325            if (arrowEndStart) {
2326                if (arrowDirIn) {
2327                    offset = drawArrow(g2, ep1, Math.PI + startAngleRAD, false, offset);
2328                }
2329                if (arrowDirOut) {
2330                    offset = drawArrow(g2, ep1, Math.PI + startAngleRAD, true, offset);
2331                }
2332            }
2333
2334            // draw the stop arrows
2335            offset = 1;
2336            if (arrowEndStop) {
2337                if (arrowDirIn) {
2338                    offset = drawArrow(g2, ep2, stopAngleRAD, false, offset);
2339                }
2340                if (arrowDirOut) {
2341                    offset = drawArrow(g2, ep2, stopAngleRAD, true, offset);
2342                }
2343            }
2344        }   // arrow decoration
2345
2346//
2347// bridge decorations
2348//
2349        if (bridgeSideLeft || bridgeSideRight) {
2350            float halfWidth = bridgeDeckWidth / 2.F;
2351
2352            log.trace("bridgeleft/right with width {}", bridgeLineWidth);
2353            g2.setStroke(new BasicStroke(bridgeLineWidth,
2354                    BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.F));
2355            g2.setColor(bridgeColor);
2356
2357            if (isArc()) {
2358                calculateTrackSegmentAngle();
2359                Rectangle2D cRectangle2D = new Rectangle2D.Double(
2360                        getCX(), getCY(), getCW(), getCH());
2361                double startAdj = getStartAdj(), tmpAngle = getTmpAngle();
2362                if (bridgeSideLeft) {
2363                    Rectangle2D tRectangle2D = MathUtil.inset(cRectangle2D, -halfWidth);
2364                    g2.draw(new Arc2D.Double(tRectangle2D.getX(), tRectangle2D.getY(),
2365                            tRectangle2D.getWidth(), tRectangle2D.getHeight(),
2366                            startAdj, tmpAngle, Arc2D.OPEN));
2367                }
2368                if (bridgeSideRight) {
2369                    Rectangle2D tRectangle2D = MathUtil.inset(cRectangle2D, +halfWidth);
2370                    g2.draw(new Arc2D.Double(tRectangle2D.getX(), tRectangle2D.getY(),
2371                            tRectangle2D.getWidth(), tRectangle2D.getHeight(),
2372                            startAdj, tmpAngle, Arc2D.OPEN));
2373                }
2374            } else if (isBezier()) {
2375                Point2D[] points = getBezierPoints();
2376                if (bridgeSideLeft) {
2377                    MathUtil.drawBezier(g2, points, -halfWidth);
2378                }
2379                if (bridgeSideRight) {
2380                    MathUtil.drawBezier(g2, points, +halfWidth);
2381                }
2382            } else {
2383                Point2D delta = MathUtil.subtract(ep2, ep1);
2384                Point2D vector = MathUtil.normalize(delta, halfWidth);
2385                vector = MathUtil.orthogonal(vector);
2386
2387                if (bridgeSideRight) {
2388                    Point2D ep1R = MathUtil.add(ep1, vector);
2389                    Point2D ep2R = MathUtil.add(ep2, vector);
2390                    g2.draw(new Line2D.Double(ep1R, ep2R));
2391                }
2392
2393                if (bridgeSideLeft) {
2394                    Point2D ep1L = MathUtil.subtract(ep1, vector);
2395                    Point2D ep2L = MathUtil.subtract(ep2, vector);
2396                    g2.draw(new Line2D.Double(ep1L, ep2L));
2397                }
2398            }   // if isArc() {} else if isBezier() {} else...
2399
2400            if (isFlip()) {
2401                boolean temp = bridgeSideRight;
2402                bridgeSideRight = bridgeSideLeft;
2403                bridgeSideLeft = temp;
2404            }
2405
2406            if (bridgeHasEntry) {
2407                if (bridgeSideRight) {
2408                    p1 = new Point2D.Double(-bridgeApproachWidth, +bridgeApproachWidth + halfWidth);
2409                    p2 = new Point2D.Double(0.0, +halfWidth);
2410                    p1P = MathUtil.add(MathUtil.rotateRAD(p1, startAngleRAD), ep1);
2411                    p2P = MathUtil.add(MathUtil.rotateRAD(p2, startAngleRAD), ep1);
2412                    g2.draw(new Line2D.Double(p1P, p2P));
2413                }
2414                if (bridgeSideLeft) {
2415                    p1 = new Point2D.Double(-bridgeApproachWidth, -bridgeApproachWidth - halfWidth);
2416                    p2 = new Point2D.Double(0.0, -halfWidth);
2417                    p1P = MathUtil.add(MathUtil.rotateRAD(p1, startAngleRAD), ep1);
2418                    p2P = MathUtil.add(MathUtil.rotateRAD(p2, startAngleRAD), ep1);
2419                    g2.draw(new Line2D.Double(p1P, p2P));
2420                }
2421            }
2422            if (bridgeHasExit) {
2423                if (bridgeSideRight) {
2424                    p1 = new Point2D.Double(+bridgeApproachWidth, +bridgeApproachWidth + halfWidth);
2425                    p2 = new Point2D.Double(0.0, +halfWidth);
2426                    p1P = MathUtil.add(MathUtil.rotateRAD(p1, stopAngleRAD), ep2);
2427                    p2P = MathUtil.add(MathUtil.rotateRAD(p2, stopAngleRAD), ep2);
2428                    g2.draw(new Line2D.Double(p1P, p2P));
2429                }
2430                if (bridgeSideLeft) {
2431                    p1 = new Point2D.Double(+bridgeApproachWidth, -bridgeApproachWidth - halfWidth);
2432                    p2 = new Point2D.Double(0.0, -halfWidth);
2433                    p1P = MathUtil.add(MathUtil.rotateRAD(p1, stopAngleRAD), ep2);
2434                    p2P = MathUtil.add(MathUtil.rotateRAD(p2, stopAngleRAD), ep2);
2435                    g2.draw(new Line2D.Double(p1P, p2P));
2436                }
2437            }
2438
2439            // if necessary flip these back
2440            if (isFlip()) {
2441                boolean temp = bridgeSideRight;
2442                bridgeSideRight = bridgeSideLeft;
2443                bridgeSideLeft = temp;
2444            }
2445        }
2446
2447//
2448// end bumper decorations
2449//
2450        if (bumperEndStart || bumperEndStop) {
2451            log.trace("bumper end/start with width {}", bumperLineWidth);
2452            g2.setStroke(new BasicStroke(bumperLineWidth,
2453                    BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.F));
2454            g2.setColor(bumperColor);
2455
2456            float halfLength = bumperLength / 2.F;
2457
2458            if (bumperFlipped) {
2459                double temp = startAngleRAD;
2460                startAngleRAD = stopAngleRAD;
2461                stopAngleRAD = temp;
2462            }
2463
2464            // common points
2465            p1 = new Point2D.Double(0.F, -halfLength);
2466            p2 = new Point2D.Double(0.F, +halfLength);
2467
2468            if (bumperEndStart) {
2469                p1P = MathUtil.add(MathUtil.rotateRAD(p1, startAngleRAD), ep1);
2470                p2P = MathUtil.add(MathUtil.rotateRAD(p2, startAngleRAD), ep1);
2471                // draw cross tie
2472                g2.draw(new Line2D.Double(p1P, p2P));
2473            }
2474            if (bumperEndStop) {
2475                p1P = MathUtil.add(MathUtil.rotateRAD(p1, stopAngleRAD), ep2);
2476                p2P = MathUtil.add(MathUtil.rotateRAD(p2, stopAngleRAD), ep2);
2477                // draw cross tie
2478                g2.draw(new Line2D.Double(p1P, p2P));
2479            }
2480        }   // if (bumperEndStart || bumperEndStop)
2481
2482//
2483// tunnel decorations
2484//
2485        if (tunnelSideRight || tunnelSideLeft) {
2486            log.trace("tunnel left/right with width {}", tunnelLineWidth);
2487            float halfWidth = tunnelFloorWidth / 2.F;
2488            g2.setStroke(new BasicStroke(tunnelLineWidth,
2489                    BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10.F,
2490                    new float[]{6.F, 4.F}, 0));
2491            g2.setColor(tunnelColor);
2492
2493            if (isArc()) {
2494                calculateTrackSegmentAngle();
2495                Rectangle2D cRectangle2D = new Rectangle2D.Double(
2496                        getCX(), getCY(), getCW(), getCH());
2497                double startAngleDEG = getStartAdj(), extentAngleDEG = getTmpAngle();
2498                if (tunnelSideRight) {
2499                    Rectangle2D tRectangle2D = MathUtil.inset(cRectangle2D, +halfWidth);
2500                    g2.draw(new Arc2D.Double(tRectangle2D.getX(), tRectangle2D.getY(),
2501                            tRectangle2D.getWidth(), tRectangle2D.getHeight(),
2502                            startAngleDEG, extentAngleDEG, Arc2D.OPEN));
2503                }
2504                if (tunnelSideLeft) {
2505                    Rectangle2D tRectangle2D = MathUtil.inset(cRectangle2D, -halfWidth);
2506                    g2.draw(new Arc2D.Double(tRectangle2D.getX(), tRectangle2D.getY(),
2507                            tRectangle2D.getWidth(), tRectangle2D.getHeight(),
2508                            startAngleDEG, extentAngleDEG, Arc2D.OPEN));
2509                }
2510                trackRedrawn();
2511            } else if (isBezier()) {
2512                Point2D[] points = getBezierPoints();
2513                if (tunnelSideRight) {
2514                    MathUtil.drawBezier(g2, points, +halfWidth);
2515                }
2516                if (tunnelSideLeft) {
2517                    MathUtil.drawBezier(g2, points, -halfWidth);
2518                }
2519            } else {
2520                Point2D delta = MathUtil.subtract(ep2, ep1);
2521                Point2D vector = MathUtil.normalize(delta, halfWidth);
2522                vector = MathUtil.orthogonal(vector);
2523
2524                if (tunnelSideRight) {
2525                    Point2D ep1L = MathUtil.add(ep1, vector);
2526                    Point2D ep2L = MathUtil.add(ep2, vector);
2527                    g2.draw(new Line2D.Double(ep1L, ep2L));
2528                }
2529                if (tunnelSideLeft) {
2530                    Point2D ep1R = MathUtil.subtract(ep1, vector);
2531                    Point2D ep2R = MathUtil.subtract(ep2, vector);
2532                    g2.draw(new Line2D.Double(ep1R, ep2R));
2533                }
2534            }   // if isArc() {} else if isBezier() {} else...
2535
2536            g2.setStroke(new BasicStroke(tunnelLineWidth,
2537                    BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.F));
2538            g2.setColor(tunnelColor);
2539
2540            // don't let tunnelEntranceWidth be less than tunnelFloorWidth + 6
2541            tunnelEntranceWidth = Math.max(tunnelEntranceWidth, tunnelFloorWidth + 6);
2542
2543            double halfEntranceWidth = tunnelEntranceWidth / 2.0;
2544            double halfFloorWidth = tunnelFloorWidth / 2.0;
2545            double halfDiffWidth = halfEntranceWidth - halfFloorWidth;
2546
2547            if (isFlip()) {
2548                boolean temp = tunnelSideRight;
2549                tunnelSideRight = tunnelSideLeft;
2550                tunnelSideLeft = temp;
2551            }
2552
2553            if (tunnelHasEntry) {
2554                if (tunnelSideRight) {
2555                    p1 = new Point2D.Double(0.0, 0.0);
2556                    p2 = new Point2D.Double(0.0, +halfFloorWidth);
2557                    p3 = new Point2D.Double(0.0, +halfEntranceWidth);
2558                    p4 = new Point2D.Double(-halfEntranceWidth - halfFloorWidth, +halfEntranceWidth);
2559                    p5 = new Point2D.Double(-halfEntranceWidth - halfFloorWidth, +halfEntranceWidth - halfDiffWidth);
2560                    p6 = new Point2D.Double(-halfFloorWidth, +halfEntranceWidth - halfDiffWidth);
2561                    p7 = new Point2D.Double(-halfDiffWidth, 0.0);
2562
2563                    p1P = MathUtil.add(MathUtil.rotateRAD(p1, startAngleRAD), ep1);
2564                    p2P = MathUtil.add(MathUtil.rotateRAD(p2, startAngleRAD), ep1);
2565                    p3P = MathUtil.add(MathUtil.rotateRAD(p3, startAngleRAD), ep1);
2566                    p4P = MathUtil.add(MathUtil.rotateRAD(p4, startAngleRAD), ep1);
2567                    p5P = MathUtil.add(MathUtil.rotateRAD(p5, startAngleRAD), ep1);
2568                    p6P = MathUtil.add(MathUtil.rotateRAD(p6, startAngleRAD), ep1);
2569                    p7P = MathUtil.add(MathUtil.rotateRAD(p7, startAngleRAD), ep1);
2570
2571                    GeneralPath path = new GeneralPath();
2572                    path.moveTo(p1P.getX(), p1P.getY());
2573                    path.lineTo(p2P.getX(), p2P.getY());
2574                    path.quadTo(p3P.getX(), p3P.getY(), p4P.getX(), p4P.getY());
2575                    path.lineTo(p5P.getX(), p5P.getY());
2576                    path.quadTo(p6P.getX(), p6P.getY(), p7P.getX(), p7P.getY());
2577                    path.closePath();
2578                    g2.draw(path);
2579                }
2580                if (tunnelSideLeft) {
2581                    p1 = new Point2D.Double(0.0, 0.0);
2582                    p2 = new Point2D.Double(0.0, -halfFloorWidth);
2583                    p3 = new Point2D.Double(0.0, -halfEntranceWidth);
2584                    p4 = new Point2D.Double(-halfEntranceWidth - halfFloorWidth, -halfEntranceWidth);
2585                    p5 = new Point2D.Double(-halfEntranceWidth - halfFloorWidth, -halfEntranceWidth + halfDiffWidth);
2586                    p6 = new Point2D.Double(-halfFloorWidth, -halfEntranceWidth + halfDiffWidth);
2587                    p7 = new Point2D.Double(-halfDiffWidth, 0.0);
2588
2589                    p1P = MathUtil.add(MathUtil.rotateRAD(p1, startAngleRAD), ep1);
2590                    p2P = MathUtil.add(MathUtil.rotateRAD(p2, startAngleRAD), ep1);
2591                    p3P = MathUtil.add(MathUtil.rotateRAD(p3, startAngleRAD), ep1);
2592                    p4P = MathUtil.add(MathUtil.rotateRAD(p4, startAngleRAD), ep1);
2593                    p5P = MathUtil.add(MathUtil.rotateRAD(p5, startAngleRAD), ep1);
2594                    p6P = MathUtil.add(MathUtil.rotateRAD(p6, startAngleRAD), ep1);
2595                    p7P = MathUtil.add(MathUtil.rotateRAD(p7, startAngleRAD), ep1);
2596
2597                    GeneralPath path = new GeneralPath();
2598                    path.moveTo(p1P.getX(), p1P.getY());
2599                    path.lineTo(p2P.getX(), p2P.getY());
2600                    path.quadTo(p3P.getX(), p3P.getY(), p4P.getX(), p4P.getY());
2601                    path.lineTo(p5P.getX(), p5P.getY());
2602                    path.quadTo(p6P.getX(), p6P.getY(), p7P.getX(), p7P.getY());
2603                    path.closePath();
2604                    g2.draw(path);
2605                }
2606            }
2607            if (tunnelHasExit) {
2608                if (tunnelSideRight) {
2609                    p1 = new Point2D.Double(0.0, 0.0);
2610                    p2 = new Point2D.Double(0.0, +halfFloorWidth);
2611                    p3 = new Point2D.Double(0.0, +halfEntranceWidth);
2612                    p4 = new Point2D.Double(halfEntranceWidth + halfFloorWidth, +halfEntranceWidth);
2613                    p5 = new Point2D.Double(halfEntranceWidth + halfFloorWidth, +halfEntranceWidth - halfDiffWidth);
2614                    p6 = new Point2D.Double(halfFloorWidth, +halfEntranceWidth - halfDiffWidth);
2615                    p7 = new Point2D.Double(halfDiffWidth, 0.0);
2616
2617                    p1P = MathUtil.add(MathUtil.rotateRAD(p1, stopAngleRAD), ep2);
2618                    p2P = MathUtil.add(MathUtil.rotateRAD(p2, stopAngleRAD), ep2);
2619                    p3P = MathUtil.add(MathUtil.rotateRAD(p3, stopAngleRAD), ep2);
2620                    p4P = MathUtil.add(MathUtil.rotateRAD(p4, stopAngleRAD), ep2);
2621                    p5P = MathUtil.add(MathUtil.rotateRAD(p5, stopAngleRAD), ep2);
2622                    p6P = MathUtil.add(MathUtil.rotateRAD(p6, stopAngleRAD), ep2);
2623                    p7P = MathUtil.add(MathUtil.rotateRAD(p7, stopAngleRAD), ep2);
2624
2625                    GeneralPath path = new GeneralPath();
2626                    path.moveTo(p1P.getX(), p1P.getY());
2627                    path.lineTo(p2P.getX(), p2P.getY());
2628                    path.quadTo(p3P.getX(), p3P.getY(), p4P.getX(), p4P.getY());
2629                    path.lineTo(p5P.getX(), p5P.getY());
2630                    path.quadTo(p6P.getX(), p6P.getY(), p7P.getX(), p7P.getY());
2631                    path.closePath();
2632                    g2.draw(path);
2633                }
2634                if (tunnelSideLeft) {
2635                    p1 = new Point2D.Double(0.0, 0.0);
2636                    p2 = new Point2D.Double(0.0, -halfFloorWidth);
2637                    p3 = new Point2D.Double(0.0, -halfEntranceWidth);
2638                    p4 = new Point2D.Double(halfEntranceWidth + halfFloorWidth, -halfEntranceWidth);
2639                    p5 = new Point2D.Double(halfEntranceWidth + halfFloorWidth, -halfEntranceWidth + halfDiffWidth);
2640                    p6 = new Point2D.Double(halfFloorWidth, -halfEntranceWidth + halfDiffWidth);
2641                    p7 = new Point2D.Double(halfDiffWidth, 0.0);
2642
2643                    p1P = MathUtil.add(MathUtil.rotateRAD(p1, stopAngleRAD), ep2);
2644                    p2P = MathUtil.add(MathUtil.rotateRAD(p2, stopAngleRAD), ep2);
2645                    p3P = MathUtil.add(MathUtil.rotateRAD(p3, stopAngleRAD), ep2);
2646                    p4P = MathUtil.add(MathUtil.rotateRAD(p4, stopAngleRAD), ep2);
2647                    p5P = MathUtil.add(MathUtil.rotateRAD(p5, stopAngleRAD), ep2);
2648                    p6P = MathUtil.add(MathUtil.rotateRAD(p6, stopAngleRAD), ep2);
2649                    p7P = MathUtil.add(MathUtil.rotateRAD(p7, stopAngleRAD), ep2);
2650
2651                    GeneralPath path = new GeneralPath();
2652                    path.moveTo(p1P.getX(), p1P.getY());
2653                    path.lineTo(p2P.getX(), p2P.getY());
2654                    path.quadTo(p3P.getX(), p3P.getY(), p4P.getX(), p4P.getY());
2655                    path.lineTo(p5P.getX(), p5P.getY());
2656                    path.quadTo(p6P.getX(), p6P.getY(), p7P.getX(), p7P.getY());
2657                    path.closePath();
2658                    g2.draw(path);
2659                }
2660            }
2661
2662            // if necessary, put these back
2663            if (isFlip()) {
2664                boolean temp = tunnelSideRight;
2665                tunnelSideRight = tunnelSideLeft;
2666                tunnelSideLeft = temp;
2667            }
2668        }
2669    }   // drawDecorations
2670
2671    /*
2672    * getBezierPoints
2673    * @return the points to pass to MathUtil.drawBezier(...)
2674     */
2675    @Nonnull
2676    private Point2D[] getBezierPoints() {
2677        Point2D ep1 = layoutEditor.getCoords(getConnect1(), getType1());
2678        Point2D ep2 = layoutEditor.getCoords(getConnect2(), getType2());
2679        int cnt = bezierControlPoints.size() + 2;
2680        Point2D[] points = new Point2D[cnt];
2681        points[0] = ep1;
2682        for (int idx = 0; idx < cnt - 2; idx++) {
2683            points[idx + 1] = bezierControlPoints.get(idx);
2684        }
2685        points[cnt - 1] = ep2;
2686        return points;
2687    }
2688
2689    private int drawArrow(
2690            Graphics2D g2,
2691            Point2D ep,
2692            double angleRAD,
2693            boolean dirOut,
2694            int offset) {
2695        Point2D p1, p2, p3, p4, p5, p6;
2696        log.trace("drawArrow in TrackSegmentView");
2697        switch (arrowStyle) {
2698            default: {
2699                arrowStyle = 0;
2700                break;
2701            }
2702            case 0: {
2703                break;
2704            }
2705            case 1: {
2706                if (dirOut) {
2707                    p1 = new Point2D.Double(offset, -arrowLength);
2708                    p2 = new Point2D.Double(offset + arrowLength, 0.0);
2709                    p3 = new Point2D.Double(offset, +arrowLength);
2710                } else {
2711                    p1 = new Point2D.Double(offset + arrowLength, -arrowLength);
2712                    p2 = new Point2D.Double(offset, 0.0);
2713                    p3 = new Point2D.Double(offset + arrowLength, +arrowLength);
2714                }
2715                p1 = MathUtil.add(MathUtil.rotateRAD(p1, angleRAD), ep);
2716                p2 = MathUtil.add(MathUtil.rotateRAD(p2, angleRAD), ep);
2717                p3 = MathUtil.add(MathUtil.rotateRAD(p3, angleRAD), ep);
2718
2719                g2.draw(new Line2D.Double(p1, p2));
2720                g2.draw(new Line2D.Double(p2, p3));
2721                offset += arrowLength + arrowGap;
2722                break;
2723            }
2724            case 2: {
2725                if (dirOut) {
2726                    p1 = new Point2D.Double(offset, -arrowLength);
2727                    p2 = new Point2D.Double(offset + arrowLength, 0.0);
2728                    p3 = new Point2D.Double(offset, +arrowLength);
2729                    p4 = new Point2D.Double(offset + arrowLineWidth + arrowGap, -arrowLength);
2730                    p5 = new Point2D.Double(offset + arrowLineWidth + arrowGap + arrowLength, 0.0);
2731                    p6 = new Point2D.Double(offset + arrowLineWidth + arrowGap, +arrowLength);
2732                } else {
2733                    p1 = new Point2D.Double(offset + arrowLength, -arrowLength);
2734                    p2 = new Point2D.Double(offset, 0.0);
2735                    p3 = new Point2D.Double(offset + arrowLength, +arrowLength);
2736                    p4 = new Point2D.Double(offset + arrowLineWidth + arrowGap + arrowLength, -arrowLength);
2737                    p5 = new Point2D.Double(offset + arrowLineWidth + arrowGap, 0.0);
2738                    p6 = new Point2D.Double(offset + arrowLineWidth + arrowGap + arrowLength, +arrowLength);
2739                }
2740                p1 = MathUtil.add(MathUtil.rotateRAD(p1, angleRAD), ep);
2741                p2 = MathUtil.add(MathUtil.rotateRAD(p2, angleRAD), ep);
2742                p3 = MathUtil.add(MathUtil.rotateRAD(p3, angleRAD), ep);
2743                p4 = MathUtil.add(MathUtil.rotateRAD(p4, angleRAD), ep);
2744                p5 = MathUtil.add(MathUtil.rotateRAD(p5, angleRAD), ep);
2745                p6 = MathUtil.add(MathUtil.rotateRAD(p6, angleRAD), ep);
2746
2747                g2.draw(new Line2D.Double(p1, p2));
2748                g2.draw(new Line2D.Double(p2, p3));
2749                g2.draw(new Line2D.Double(p4, p5));
2750                g2.draw(new Line2D.Double(p5, p6));
2751                offset += arrowLength + (2 * (arrowLineWidth + arrowGap));
2752                break;
2753            }
2754            case 3: {
2755                if (dirOut) {
2756                    p1 = new Point2D.Double(offset, -arrowLength);
2757                    p2 = new Point2D.Double(offset + arrowLength, 0.0);
2758                    p3 = new Point2D.Double(offset, +arrowLength);
2759                } else {
2760                    p1 = new Point2D.Double(offset + arrowLength, -arrowLength);
2761                    p2 = new Point2D.Double(offset, 0.0);
2762                    p3 = new Point2D.Double(offset + arrowLength, +arrowLength);
2763                }
2764                p1 = MathUtil.add(MathUtil.rotateRAD(p1, angleRAD), ep);
2765                p2 = MathUtil.add(MathUtil.rotateRAD(p2, angleRAD), ep);
2766                p3 = MathUtil.add(MathUtil.rotateRAD(p3, angleRAD), ep);
2767
2768                GeneralPath path = new GeneralPath();
2769                path.moveTo(p1.getX(), p1.getY());
2770                path.lineTo(p2.getX(), p2.getY());
2771                path.lineTo(p3.getX(), p3.getY());
2772                path.closePath();
2773                if (arrowLineWidth > 1) {
2774                    g2.fill(path);
2775                } else {
2776                    g2.draw(path);
2777                }
2778                offset += arrowLength + arrowGap;
2779                break;
2780            }
2781            case 4: {
2782                if (dirOut) {
2783                    p1 = new Point2D.Double(offset, 0.0);
2784                    p2 = new Point2D.Double(offset + (2 * arrowLength), -arrowLength);
2785                    p3 = new Point2D.Double(offset + (3 * arrowLength), 0.0);
2786                    p4 = new Point2D.Double(offset + (2 * arrowLength), +arrowLength);
2787                } else {
2788                    p1 = new Point2D.Double(offset, 0.0);
2789                    p2 = new Point2D.Double(offset + (4 * arrowLength), -arrowLength);
2790                    p3 = new Point2D.Double(offset + (3 * arrowLength), 0.0);
2791                    p4 = new Point2D.Double(offset + (4 * arrowLength), +arrowLength);
2792                }
2793                p1 = MathUtil.add(MathUtil.rotateRAD(p1, angleRAD), ep);
2794                p2 = MathUtil.add(MathUtil.rotateRAD(p2, angleRAD), ep);
2795                p3 = MathUtil.add(MathUtil.rotateRAD(p3, angleRAD), ep);
2796                p4 = MathUtil.add(MathUtil.rotateRAD(p4, angleRAD), ep);
2797
2798                g2.draw(new Line2D.Double(p1, p3));
2799                g2.draw(new Line2D.Double(p2, p3));
2800                g2.draw(new Line2D.Double(p3, p4));
2801
2802                offset += (3 * arrowLength) + arrowGap;
2803                break;
2804            }
2805            case 5: {
2806                if (dirOut) {
2807                    p1 = new Point2D.Double(offset, 0.0);
2808                    p2 = new Point2D.Double(offset + (2 * arrowLength), -arrowLength);
2809                    p3 = new Point2D.Double(offset + (3 * arrowLength), 0.0);
2810                    p4 = new Point2D.Double(offset + (2 * arrowLength), +arrowLength);
2811                } else {
2812                    p1 = new Point2D.Double(offset, 0.0);
2813                    p2 = new Point2D.Double(offset + (4 * arrowLength), -arrowLength);
2814                    p3 = new Point2D.Double(offset + (3 * arrowLength), 0.0);
2815                    p4 = new Point2D.Double(offset + (4 * arrowLength), +arrowLength);
2816                }
2817                p1 = MathUtil.add(MathUtil.rotateRAD(p1, angleRAD), ep);
2818                p2 = MathUtil.add(MathUtil.rotateRAD(p2, angleRAD), ep);
2819                p3 = MathUtil.add(MathUtil.rotateRAD(p3, angleRAD), ep);
2820                p4 = MathUtil.add(MathUtil.rotateRAD(p4, angleRAD), ep);
2821
2822                GeneralPath path = new GeneralPath();
2823                path.moveTo(p4.getX(), p4.getY());
2824                path.lineTo(p2.getX(), p2.getY());
2825                path.lineTo(p3.getX(), p3.getY());
2826                path.closePath();
2827                if (arrowLineWidth > 1) {
2828                    g2.fill(path);
2829                } else {
2830                    g2.draw(path);
2831                }
2832                g2.draw(new Line2D.Double(p1, p3));
2833
2834                offset += (3 * arrowLength) + arrowGap;
2835                break;
2836            }
2837        }
2838        return offset;
2839    }   // drawArrow
2840
2841    /*======================*\
2842    |* decoration accessors *|
2843    \*======================*/
2844    // Although the superclass LayoutTrack stores decorators in a Map,
2845    // here we store them in specific variables like arrowStyle, bridgeSideRight, etc.
2846    // We convert to and from the map during the getDecorations, setDecorations
2847    // and hasDecorations calls.
2848    /**
2849     * {@inheritDoc}
2850     */
2851    @Override
2852    public boolean hasDecorations() {
2853        return ((arrowStyle > 0)
2854                || (bridgeSideLeft || bridgeSideRight)
2855                || (bumperEndStart || bumperEndStop)
2856                || (tunnelSideLeft || tunnelSideRight));
2857    }
2858
2859    /**
2860     * {@inheritDoc}
2861     */
2862    @Override
2863    public Map<String, String> getDecorations() {
2864        if (decorations == null) {
2865            decorations = new HashMap<>();
2866        } // if (decorathions != null)
2867
2868        //
2869        // arrow decorations
2870        //
2871        if (arrowStyle > 0) {
2872            //<decoration name="arrow" value="double;both;linewidth=1;length=12;gap=1" />
2873            List<String> arrowValues = new ArrayList<>();
2874
2875            arrowValues.add("style=" + arrowStyle);
2876
2877            if (arrowEndStart && arrowEndStop) {
2878                // default behaviour is both
2879            } else if (arrowEndStop) {
2880                arrowValues.add("stop");
2881            } else {
2882                arrowEndStart = true;
2883                arrowValues.add("start");
2884            }
2885
2886            if (arrowDirIn && !arrowDirOut) {
2887                arrowValues.add("in");
2888            } else if (!arrowDirIn && arrowDirOut) {
2889                arrowValues.add("out");
2890            } else {
2891                arrowDirIn = true;
2892                arrowDirOut = true;
2893                arrowValues.add("both");
2894            }
2895            arrowValues.add("color=" + ColorUtil.colorToHexString(arrowColor));
2896            arrowValues.add("linewidth=" + arrowLineWidth);
2897            arrowValues.add("length=" + arrowLength);
2898            arrowValues.add("gap=" + arrowGap);
2899            decorations.put("arrow", String.join(";", arrowValues));
2900        }   // if (arrowCount > 0)
2901
2902        //
2903        // bridge decorations
2904        //
2905        if (bridgeSideLeft || bridgeSideRight) {
2906            //<decoration name="bridge" value="both;linewidth=2;deckwidth=8" />
2907            List<String> bridgeValues = new ArrayList<>();
2908
2909            if (bridgeHasEntry && !bridgeHasExit) {
2910                bridgeValues.add("entry");
2911            } else if (!bridgeHasEntry && bridgeHasExit) {
2912                bridgeValues.add("exit");
2913            } else if (bridgeHasEntry && bridgeHasExit) {
2914                bridgeValues.add("both");
2915            }
2916            if (bridgeSideLeft && !bridgeSideRight) {
2917                bridgeValues.add("left");
2918            } else if (!bridgeSideLeft && bridgeSideRight) {
2919                bridgeValues.add("right");
2920            }
2921            bridgeValues.add("color=" + ColorUtil.colorToHexString(bridgeColor));
2922            bridgeValues.add("linewidth=" + bridgeLineWidth);
2923            bridgeValues.add("approachwidth=" + bridgeApproachWidth);
2924            bridgeValues.add("deckwidth=" + bridgeDeckWidth);
2925
2926            decorations.put("bridge", String.join(";", bridgeValues));
2927        }   // if (bridgeSideLeft || bridgeSideRight)
2928
2929        //
2930        // end bumper decorations
2931        //
2932        if (bumperEndStart || bumperEndStop) {
2933            //<decoration name="bumper" value="double;linewidth=2;length=6;gap=2;flipped" />
2934            List<String> bumperValues = new ArrayList<>();
2935            if (bumperEndStart) {
2936                bumperValues.add("start");
2937            } else if (bumperEndStop) {
2938                bumperValues.add("stop");
2939            }
2940
2941            if (bumperFlipped) {
2942                bumperValues.add("flip");
2943            }
2944            bumperValues.add("color=" + ColorUtil.colorToHexString(bumperColor));
2945            bumperValues.add("length=" + bumperLength);
2946            bumperValues.add("linewidth=" + bumperLineWidth);
2947
2948            decorations.put("bumper", String.join(";", bumperValues));
2949        }   // if (bumperCount > 0)
2950
2951        //
2952        // tunnel decorations
2953        //
2954        if (tunnelSideLeft || tunnelSideRight) {
2955            //<decoration name="tunnel" value="both;linewidth=2;floorwidth=8" />
2956            List<String> tunnelValues = new ArrayList<>();
2957
2958            if (tunnelHasEntry && !tunnelHasExit) {
2959                tunnelValues.add("entry");
2960            } else if (!tunnelHasEntry && tunnelHasExit) {
2961                tunnelValues.add("exit");
2962            } else if (tunnelHasEntry && tunnelHasExit) {
2963                tunnelValues.add("both");
2964            }
2965
2966            if (tunnelSideLeft && !tunnelSideRight) {
2967                tunnelValues.add("left");
2968            } else if (tunnelSideLeft && !tunnelSideRight) {
2969                tunnelValues.add("right");
2970            }
2971            tunnelValues.add("color=" + ColorUtil.colorToHexString(tunnelColor));
2972            tunnelValues.add("linewidth=" + tunnelLineWidth);
2973            tunnelValues.add("entrancewidth=" + tunnelEntranceWidth);
2974            tunnelValues.add("floorwidth=" + tunnelFloorWidth);
2975
2976            decorations.put("tunnel", String.join(";", tunnelValues));
2977        }   // if (tunnelSideLeft || tunnelSideRight)
2978        return decorations;
2979    }
2980
2981    /**
2982     * {@inheritDoc}
2983     */
2984    @Override
2985    public void setDecorations(@Nonnull Map<String, String> decorations) {
2986        Color defaultTrackColor = layoutEditor.getDefaultTrackColorColor();
2987        super.setDecorations(decorations);
2988        if (decorations != null) {
2989            for (Map.Entry<String, String> entry : decorations.entrySet()) {
2990                log.debug("Key = ''{}'', Value = ''{}''", entry.getKey(), entry.getValue());
2991                String key = entry.getKey();
2992                //
2993                // arrow decorations
2994                //
2995                if (key.equals("arrow")) {
2996                    String arrowValue = entry.getValue();
2997                    //<decoration name="arrow" value="double;both;linewidth=1;length=12;gap=1" />
2998                    boolean atStart = true, atStop = true;
2999                    boolean hasIn = false, hasOut = false;
3000                    int lineWidth = 1, length = 3, gap = 1, count = 1;
3001                    Color color = defaultTrackColor;
3002                    String[] values = arrowValue.split(";");
3003                    for (String value : values) {
3004                        if (value.equals("single")) {
3005                            count = 1;
3006                        } else if (value.equals("double")) {
3007                            count = 2;
3008                        } else if (value.equals("triple")) {
3009                            count = 3;
3010                        } else if (value.startsWith("style=")) {
3011                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3012                            count = Integer.parseInt(valueString);
3013                        } else if (value.equals("start")) {
3014                            atStop = false;
3015                        } else if (value.equals("stop")) {
3016                            atStart = false;
3017                        } else if (value.equals("in")) {
3018                            hasIn = true;
3019                        } else if (value.equals("out")) {
3020                            hasOut = true;
3021                        } else if (value.equals("both")) {
3022                            hasIn = true;
3023                            hasOut = true;
3024                        } else if (value.startsWith("color=")) {
3025                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3026                            color = Color.decode(valueString);
3027                        } else if (value.startsWith("linewidth=")) {
3028                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3029                            lineWidth = Integer.parseInt(valueString);
3030                        } else if (value.startsWith("length=")) {
3031                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3032                            length = Integer.parseInt(valueString);
3033                        } else if (value.startsWith("gap=")) {
3034                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3035                            gap = Integer.parseInt(valueString);
3036                        } else {
3037                            log.debug("arrow value ignored: {}", value);
3038                        }
3039                    }
3040                    hasIn |= !hasOut;   // if hasOut is false make hasIn true
3041                    if (!atStart && !atStop) {   // if both false
3042                        atStart = true; // set both true
3043                        atStop = true;
3044                    }
3045                    setArrowEndStart(atStart);
3046                    setArrowEndStop(atStop);
3047                    setArrowDirIn(hasIn);
3048                    setArrowDirOut(hasOut);
3049                    setArrowColor(color);
3050                    setArrowLineWidth(lineWidth);
3051                    setArrowLength(length);
3052                    setArrowGap(gap);
3053                    // set count last so it will fix ends and dir (if necessary)
3054                    setArrowStyle(count);
3055                } // if (key.equals("arrow")) {
3056                //
3057                // bridge decorations
3058                //
3059                else if (key.equals("bridge")) {
3060                    String bridgeValue = entry.getValue();
3061                    //<decoration name="bridge" value="both;linewidth=2;deckwidth=8" />
3062                    // right/left default true; in/out default false
3063                    boolean hasLeft = true, hasRight = true, hasEntry = false, hasExit = false;
3064                    int approachWidth = 4, lineWidth = 1, deckWidth = 2;
3065                    Color color = defaultTrackColor;
3066                    String[] values = bridgeValue.split(";");
3067                    for (String value : values) {
3068                        // log.info("value[{}]: ''{}''", i, value);
3069                        if (value.equals("left")) {
3070                            hasRight = false;
3071                        } else if (value.equals("right")) {
3072                            hasLeft = false;
3073                        } else if (value.equals("entry")) {
3074                            hasEntry = true;
3075                        } else if (value.equals("exit")) {
3076                            hasExit = true;
3077                        } else if (value.equals("both")) {
3078                            hasEntry = true;
3079                            hasExit = true;
3080                        } else if (value.startsWith("color=")) {
3081                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3082                            color = Color.decode(valueString);
3083                        } else if (value.startsWith("approachwidth=")) {
3084                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3085                            approachWidth = Integer.parseInt(valueString);
3086                        } else if (value.startsWith("linewidth=")) {
3087                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3088                            lineWidth = Integer.parseInt(valueString);
3089                        } else if (value.startsWith("deckwidth=")) {
3090                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3091                            deckWidth = Integer.parseInt(valueString);
3092                        } else {
3093                            log.debug("bridge value ignored: {}", value);
3094                        }
3095                    }
3096                    // these both can't be false
3097                    if (!hasLeft && !hasRight) {
3098                        hasLeft = true;
3099                        hasRight = true;
3100                    }
3101                    setBridgeSideRight(hasRight);
3102                    setBridgeSideLeft(hasLeft);
3103                    setBridgeHasEntry(hasEntry);
3104                    setBridgeHasExit(hasExit);
3105                    setBridgeColor(color);
3106                    setBridgeDeckWidth(deckWidth);
3107                    setBridgeLineWidth(lineWidth);
3108                    setBridgeApproachWidth(approachWidth);
3109                } // if (key.equals("bridge")) {
3110                //
3111                // bumper decorations
3112                //
3113                else if (key.equals("bumper")) {
3114                    String bumperValue = entry.getValue();
3115//               if (getName().equals("T15")) {
3116//                   log.debug("STOP");
3117//               }
3118//<decoration name="bumper" value="double;linewidth=2;length=6;gap=2;flipped" />
3119                    int lineWidth = 1, length = 4;
3120                    boolean isFlipped = false, atStart = true, atStop = true;
3121                    Color color = defaultTrackColor;
3122                    String[] values = bumperValue.split(";");
3123                    for (String value : values) {
3124                        // log.info("value[{}]: ''{}''", i, value);
3125                        if (value.equals("start")) {
3126                            atStop = false;
3127                        } else if (value.equals("stop")) {
3128                            atStart = false;
3129                        } else if (value.equals("both")) {
3130                            // this is the default behaviour; parameter ignored
3131                        } else if (value.equals("flip")) {
3132                            isFlipped = true;
3133                        } else if (value.startsWith("color=")) {
3134                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3135                            color = Color.decode(valueString);
3136                        } else if (value.startsWith("linewidth=")) {
3137                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3138                            lineWidth = Integer.parseInt(valueString);
3139                        } else if (value.startsWith("length=")) {
3140                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3141                            length = Integer.parseInt(valueString);
3142                        } else {
3143                            log.debug("bumper value ignored: {}", value);
3144                        }
3145                    }
3146                    atStop |= !atStart;   // if atStart is false make atStop true
3147                    setBumperEndStart(atStart);
3148                    setBumperEndStop(atStop);
3149                    setBumperColor(color);
3150                    setBumperLineWidth(lineWidth);
3151                    setBumperLength(length);
3152                    setBumperFlipped(isFlipped);
3153                } // if (key.equals("bumper")) {
3154                //
3155                // tunnel decorations
3156                //
3157                else if (key.equals("tunnel")) {
3158                    String tunnelValue = entry.getValue();
3159                    //<decoration name="tunnel" value="both;linewidth=2;floorwidth=8" />
3160                    // right/left default true; in/out default false
3161                    boolean hasLeft = true, hasRight = true, hasIn = false, hasOut = false;
3162                    int entranceWidth = 4, lineWidth = 1, floorWidth = 2;
3163                    Color color = defaultTrackColor;
3164                    String[] values = tunnelValue.split(";");
3165                    for (String value : values) {
3166                        // log.info("value[{}]: ''{}''", i, value);
3167                        if (value.equals("left")) {
3168                            hasRight = false;
3169                        } else if (value.equals("right")) {
3170                            hasLeft = false;
3171                        } else if (value.equals("entry")) {
3172                            hasIn = true;
3173                        } else if (value.equals("exit")) {
3174                            hasOut = true;
3175                        } else if (value.equals("both")) {
3176                            hasIn = true;
3177                            hasOut = true;
3178                        } else if (value.startsWith("color=")) {
3179                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3180                            color = Color.decode(valueString);
3181                        } else if (value.startsWith("entrancewidth=")) {
3182                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3183                            entranceWidth = Integer.parseInt(valueString);
3184                        } else if (value.startsWith("linewidth=")) {
3185                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3186                            lineWidth = Integer.parseInt(valueString);
3187                        } else if (value.startsWith("floorwidth=")) {
3188                            String valueString = value.substring(value.lastIndexOf("=") + 1);
3189                            floorWidth = Integer.parseInt(valueString);
3190                        } else {
3191                            log.debug("tunnel value ignored: {}", value);
3192                        }
3193                    }
3194                    // these both can't be false
3195                    if (!hasLeft && !hasRight) {
3196                        hasLeft = true;
3197                        hasRight = true;
3198                    }
3199                    setTunnelSideRight(hasRight);
3200                    setTunnelSideLeft(hasLeft);
3201                    setTunnelHasEntry(hasIn);
3202                    setTunnelHasExit(hasOut);
3203                    setTunnelColor(color);
3204                    setTunnelEntranceWidth(entranceWidth);
3205                    setTunnelLineWidth(lineWidth);
3206                    setTunnelFloorWidth(floorWidth);
3207                } // if (tunnelValue != null)
3208                else {
3209                    log.debug("Unknown decoration key: {}, value: {}", key, entry.getValue());
3210                }
3211            }   // for (Map.Entry<String, String> entry : decorations.entrySet())
3212        } // if (decorathions != null)
3213    }   // setDirections
3214
3215    /**
3216     * Arrow decoration accessor. The 0 (none) and 1 through 5 arrow decorations
3217     * are keyed to files like
3218     * program:resources/icons/decorations/ArrowStyle1.png et al.
3219     *
3220     * @return Style number
3221     */
3222    public int getArrowStyle() {
3223        return arrowStyle;
3224    }
3225
3226    /**
3227     * Set the arrow decoration. The 0 (none) and 1 through 5 arrow decorations
3228     * are keyed to files like
3229     * program:resources/icons/decorations/ArrowStyle1.png et al.
3230     *
3231     * @param newVal the new style number
3232     */
3233    public void setArrowStyle(int newVal) {
3234        log.trace("TrackSegmentView:setArrowStyle {} {} {}", newVal, arrowEndStart, arrowEndStop);
3235        if (arrowStyle != newVal) {
3236            if (newVal > 0) {
3237                if (!arrowEndStart && !arrowEndStop) {
3238                    arrowEndStart = true;
3239                    arrowEndStop = true;
3240                }
3241                if (!arrowDirIn && !arrowDirOut) {
3242                    arrowDirOut = true;
3243                }
3244            } else {
3245                newVal = 0; // only positive styles allowed!
3246            }
3247            arrowStyle = newVal;
3248            layoutEditor.redrawPanel();
3249            layoutEditor.setDirty();
3250        }
3251    }
3252    private int arrowStyle = 0;
3253
3254    public boolean isArrowEndStart() {
3255        return arrowEndStart;
3256    }
3257
3258    public void setArrowEndStart(boolean newVal) {
3259        if (arrowEndStart != newVal) {
3260            arrowEndStart = newVal;
3261            if (!arrowEndStart && !arrowEndStop) {
3262                arrowStyle = 0;
3263            } else if (arrowStyle == 0) {
3264                arrowStyle = 1;
3265            }
3266            layoutEditor.redrawPanel();
3267            layoutEditor.setDirty();
3268        }
3269    }
3270    private boolean arrowEndStart = false;
3271
3272    public boolean isArrowEndStop() {
3273        return arrowEndStop;
3274    }
3275
3276    public void setArrowEndStop(boolean newVal) {
3277        if (arrowEndStop != newVal) {
3278            arrowEndStop = newVal;
3279            if (!arrowEndStart && !arrowEndStop) {
3280                arrowStyle = 0;
3281            } else if (arrowStyle == 0) {
3282                arrowStyle = 1;
3283            }
3284            layoutEditor.redrawPanel();
3285            layoutEditor.setDirty();
3286        }
3287    }
3288    private boolean arrowEndStop = false;
3289
3290    public boolean isArrowDirIn() {
3291        return arrowDirIn;
3292    }
3293
3294    public void setArrowDirIn(boolean newVal) {
3295        if (arrowDirIn != newVal) {
3296            arrowDirIn = newVal;
3297            if (!arrowDirIn && !arrowDirOut) {
3298                arrowStyle = 0;
3299            } else if (arrowStyle == 0) {
3300                arrowStyle = 1;
3301            }
3302            layoutEditor.redrawPanel();
3303            layoutEditor.setDirty();
3304        }
3305    }
3306    private boolean arrowDirIn = false;
3307
3308    public boolean isArrowDirOut() {
3309        return arrowDirOut;
3310    }
3311
3312    public void setArrowDirOut(boolean newVal) {
3313        if (arrowDirOut != newVal) {
3314            arrowDirOut = newVal;
3315            if (!arrowDirIn && !arrowDirOut) {
3316                arrowStyle = 0;
3317            } else if (arrowStyle == 0) {
3318                arrowStyle = 1;
3319            }
3320            layoutEditor.redrawPanel();
3321            layoutEditor.setDirty();
3322        }
3323    }
3324    private boolean arrowDirOut = false;
3325
3326    public Color getArrowColor() {
3327        return arrowColor;
3328    }
3329
3330    public void setArrowColor(Color newVal) {
3331        if (arrowColor != newVal) {
3332            arrowColor = newVal;
3333            JmriColorChooser.addRecentColor(newVal);
3334            layoutEditor.redrawPanel();
3335            layoutEditor.setDirty();
3336        }
3337    }
3338    private Color arrowColor = Color.BLACK;
3339
3340    public int getArrowLineWidth() {
3341        return arrowLineWidth;
3342    }
3343
3344    public void setArrowLineWidth(int newVal) {
3345        if (arrowLineWidth != newVal) {
3346            arrowLineWidth = MathUtil.pin(newVal, 1, MAX_ARROW_LINE_WIDTH);
3347            layoutEditor.redrawPanel();
3348            layoutEditor.setDirty();
3349        }
3350    }
3351    private int arrowLineWidth = 4;
3352
3353    public int getArrowLength() {
3354        return arrowLength;
3355    }
3356
3357    public void setArrowLength(int newVal) {
3358        if (arrowLength != newVal) {
3359            arrowLength = MathUtil.pin(newVal, 2, MAX_ARROW_LENGTH);
3360            layoutEditor.redrawPanel();
3361            layoutEditor.setDirty();
3362        }
3363    }
3364    private int arrowLength = 4;
3365
3366    public int getArrowGap() {
3367        return arrowGap;
3368    }
3369
3370    public void setArrowGap(int newVal) {
3371        if (arrowGap != newVal) {
3372            arrowGap = MathUtil.pin(newVal, 0, MAX_ARROW_GAP);
3373            layoutEditor.redrawPanel();
3374            layoutEditor.setDirty();
3375        }
3376    }
3377    private int arrowGap = 1;
3378
3379    //
3380    // bridge decoration accessors
3381    //
3382    public boolean isBridgeSideRight() {
3383        return bridgeSideRight;
3384    }
3385
3386    public void setBridgeSideRight(boolean newVal) {
3387        if (bridgeSideRight != newVal) {
3388            bridgeSideRight = newVal;
3389            layoutEditor.redrawPanel();
3390            layoutEditor.setDirty();
3391        }
3392    }
3393    private boolean bridgeSideRight = false;
3394
3395    public boolean isBridgeSideLeft() {
3396        return bridgeSideLeft;
3397    }
3398
3399    public void setBridgeSideLeft(boolean newVal) {
3400        if (bridgeSideLeft != newVal) {
3401            bridgeSideLeft = newVal;
3402            layoutEditor.redrawPanel();
3403            layoutEditor.setDirty();
3404        }
3405    }
3406    private boolean bridgeSideLeft = false;
3407
3408    public boolean isBridgeHasEntry() {
3409        return bridgeHasEntry;
3410    }
3411
3412    public void setBridgeHasEntry(boolean newVal) {
3413        if (bridgeHasEntry != newVal) {
3414            bridgeHasEntry = newVal;
3415            layoutEditor.redrawPanel();
3416            layoutEditor.setDirty();
3417        }
3418    }
3419    private boolean bridgeHasEntry = false;
3420
3421    public boolean isBridgeHasExit() {
3422        return bridgeHasExit;
3423    }
3424
3425    public void setBridgeHasExit(boolean newVal) {
3426        if (bridgeHasExit != newVal) {
3427            bridgeHasExit = newVal;
3428            layoutEditor.redrawPanel();
3429            layoutEditor.setDirty();
3430        }
3431    }
3432    private boolean bridgeHasExit = false;
3433
3434    public Color getBridgeColor() {
3435        return bridgeColor;
3436    }
3437
3438    public void setBridgeColor(Color newVal) {
3439        if (bridgeColor != newVal) {
3440            bridgeColor = newVal;
3441            JmriColorChooser.addRecentColor(newVal);
3442            layoutEditor.redrawPanel();
3443            layoutEditor.setDirty();
3444        }
3445    }
3446    private Color bridgeColor = Color.BLACK;
3447
3448    public int getBridgeDeckWidth() {
3449        return bridgeDeckWidth;
3450    }
3451
3452    public void setBridgeDeckWidth(int newVal) {
3453        if (bridgeDeckWidth != newVal) {
3454            bridgeDeckWidth = Math.max(MIN_BRIDGE_DECK_WIDTH, newVal);   // don't let value be less than MIN
3455            layoutEditor.redrawPanel();
3456            layoutEditor.setDirty();
3457        }
3458    }
3459    private int bridgeDeckWidth = 10;
3460
3461    public int getBridgeLineWidth() {
3462        return bridgeLineWidth;
3463    }
3464
3465    public void setBridgeLineWidth(int newVal) {
3466        if (bridgeLineWidth != newVal) {
3467            bridgeLineWidth = Math.max(MIN_BRIDGE_LINE_WIDTH, newVal);   // don't let value be less than MIN
3468            layoutEditor.redrawPanel();
3469            layoutEditor.setDirty();
3470        }
3471    }
3472    private int bridgeLineWidth = 1;
3473
3474    public int getBridgeApproachWidth() {
3475        return bridgeApproachWidth;
3476    }
3477
3478    public void setBridgeApproachWidth(int newVal) {
3479        if (bridgeApproachWidth != newVal) {
3480            bridgeApproachWidth = Math.max(MIN_BRIDGE_APPROACH_WIDTH, newVal);   // don't let value be less than MIN
3481            layoutEditor.redrawPanel();
3482            layoutEditor.setDirty();
3483        }
3484    }
3485    private int bridgeApproachWidth = 4;
3486
3487    //
3488    // bumper decoration accessors
3489    //
3490    public boolean isBumperEndStart() {
3491        return bumperEndStart;
3492    }
3493
3494    public void setBumperEndStart(boolean newVal) {
3495        if (bumperEndStart != newVal) {
3496            bumperEndStart = newVal;
3497            layoutEditor.redrawPanel();
3498            layoutEditor.setDirty();
3499        }
3500    }
3501    private boolean bumperEndStart = false;
3502
3503    public boolean isBumperEndStop() {
3504        return bumperEndStop;
3505    }
3506
3507    public void setBumperEndStop(boolean newVal) {
3508        if (bumperEndStop != newVal) {
3509            bumperEndStop = newVal;
3510            layoutEditor.redrawPanel();
3511            layoutEditor.setDirty();
3512        }
3513    }
3514    private boolean bumperEndStop = false;
3515
3516    public Color getBumperColor() {
3517        return bumperColor;
3518    }
3519
3520    public void setBumperColor(Color newVal) {
3521        if (bumperColor != newVal) {
3522            bumperColor = newVal;
3523            JmriColorChooser.addRecentColor(newVal);
3524            layoutEditor.redrawPanel();
3525            layoutEditor.setDirty();
3526        }
3527    }
3528    private Color bumperColor = Color.BLACK;
3529
3530    public int getBumperLineWidth() {
3531        return bumperLineWidth;
3532    }
3533
3534    public void setBumperLineWidth(int newVal) {
3535        if (bumperLineWidth != newVal) {
3536            bumperLineWidth = MathUtil.pin(newVal, 1, MAX_BUMPER_LINE_WIDTH);
3537            layoutEditor.redrawPanel();
3538            layoutEditor.setDirty();
3539        }
3540    }
3541
3542    private int bumperLineWidth = 3;
3543
3544    public int getBumperLength() {
3545        return bumperLength;
3546    }
3547
3548    public void setBumperLength(int newVal) {
3549        if (bumperLength != newVal) {
3550            bumperLength = Math.max(MIN_BUMPER_LENGTH, newVal);   // don't let value be less than MIN
3551            layoutEditor.redrawPanel();
3552            layoutEditor.setDirty();
3553        }
3554    }
3555    private int bumperLength = 20;
3556
3557    public boolean isBumperFlipped() {
3558        return bumperFlipped;
3559    }
3560
3561    public void setBumperFlipped(boolean newVal) {
3562        if (bumperFlipped != newVal) {
3563            bumperFlipped = newVal;
3564            layoutEditor.redrawPanel();
3565            layoutEditor.setDirty();
3566        }
3567    }
3568    private boolean bumperFlipped = false;
3569
3570    private void setupDefaultBumperSizes(@Nonnull LayoutEditor layoutEditor) {
3571        LayoutTrackDrawingOptions ltdo = layoutEditor.getLayoutTrackDrawingOptions();
3572
3573        // use these as default sizes for end bumpers
3574        int tieLength = ltdo.getSideTieLength();
3575        int tieWidth = ltdo.getSideTieWidth();
3576        int railWidth = ltdo.getSideRailWidth();
3577        int railGap = ltdo.getSideRailGap();
3578        if (trackSegment.isMainline()) {
3579            tieLength = ltdo.getMainTieLength();
3580            tieWidth = ltdo.getMainTieWidth();
3581            railWidth = ltdo.getMainRailWidth();
3582            railGap = ltdo.getMainRailGap();
3583        }
3584
3585        bumperLineWidth = Math.max(railWidth, ltdo.getMainBlockLineWidth()) * 2;
3586        bumperLength = railGap + (2 * railWidth);
3587        if ((tieLength > 0) && (tieWidth > 0)) {
3588            bumperLineWidth = tieWidth;
3589            bumperLength = tieLength * 3 / 2;
3590        }
3591        bumperLineWidth = Math.max(MIN_BUMPER_LINE_WIDTH, bumperLineWidth); // don't let value be less than MIN
3592        bumperLength = Math.max(MIN_BUMPER_LENGTH, bumperLength);// don't let value be less than MIN
3593    }
3594
3595    //
3596    // tunnel decoration accessors
3597    //
3598    public boolean isTunnelSideRight() {
3599        return tunnelSideRight;
3600    }
3601
3602    public void setTunnelSideRight(boolean newVal) {
3603        if (tunnelSideRight != newVal) {
3604            tunnelSideRight = newVal;
3605            layoutEditor.redrawPanel();
3606            layoutEditor.setDirty();
3607        }
3608    }
3609    private boolean tunnelSideRight = false;
3610
3611    public boolean isTunnelSideLeft() {
3612        return tunnelSideLeft;
3613    }
3614
3615    public void setTunnelSideLeft(boolean newVal) {
3616        if (tunnelSideLeft != newVal) {
3617            tunnelSideLeft = newVal;
3618            layoutEditor.redrawPanel();
3619            layoutEditor.setDirty();
3620        }
3621    }
3622    private boolean tunnelSideLeft = false;
3623
3624    public boolean isTunnelHasEntry() {
3625        return tunnelHasEntry;
3626    }
3627
3628    public void setTunnelHasEntry(boolean newVal) {
3629        if (tunnelHasEntry != newVal) {
3630            tunnelHasEntry = newVal;
3631            layoutEditor.redrawPanel();
3632            layoutEditor.setDirty();
3633        }
3634    }
3635    private boolean tunnelHasEntry = false;
3636
3637    public boolean isTunnelHasExit() {
3638        return tunnelHasExit;
3639    }
3640
3641    public void setTunnelHasExit(boolean newVal) {
3642        if (tunnelHasExit != newVal) {
3643            tunnelHasExit = newVal;
3644            layoutEditor.redrawPanel();
3645            layoutEditor.setDirty();
3646        }
3647    }
3648    private boolean tunnelHasExit = false;
3649
3650    public Color getTunnelColor() {
3651        return tunnelColor;
3652    }
3653
3654    public void setTunnelColor(Color newVal) {
3655        if (tunnelColor != newVal) {
3656            tunnelColor = newVal;
3657            JmriColorChooser.addRecentColor(newVal);
3658            layoutEditor.redrawPanel();
3659            layoutEditor.setDirty();
3660        }
3661    }
3662    private Color tunnelColor = Color.BLACK;
3663
3664    public int getTunnelFloorWidth() {
3665        return tunnelFloorWidth;
3666    }
3667
3668    public void setTunnelFloorWidth(int newVal) {
3669        if (tunnelFloorWidth != newVal) {
3670            tunnelFloorWidth = Math.max(MIN_TUNNEL_FLOOR_WIDTH, newVal);   // don't let value be less than MIN
3671            layoutEditor.redrawPanel();
3672            layoutEditor.setDirty();
3673        }
3674    }
3675    private int tunnelFloorWidth = 10;
3676
3677    public int getTunnelLineWidth() {
3678        return tunnelLineWidth;
3679    }
3680
3681    public void setTunnelLineWidth(int newVal) {
3682        if (tunnelLineWidth != newVal) {
3683            tunnelLineWidth = Math.max(MIN_TUNNEL_LINE_WIDTH, newVal);   // don't let value be less than MIN
3684            layoutEditor.redrawPanel();
3685            layoutEditor.setDirty();
3686        }
3687    }
3688    private int tunnelLineWidth = 1;
3689
3690    public int getTunnelEntranceWidth() {
3691        return tunnelEntranceWidth;
3692    }
3693
3694    public void setTunnelEntranceWidth(int newVal) {
3695        if (tunnelEntranceWidth != newVal) {
3696            tunnelEntranceWidth = Math.max(MIN_TUNNEL_ENTRANCE_WIDTH, newVal);   // don't let value be less than 1
3697            layoutEditor.redrawPanel();
3698            layoutEditor.setDirty();
3699        }
3700    }
3701    private int tunnelEntranceWidth = 16;
3702
3703    /**
3704     * {@inheritDoc}
3705     */
3706    @Override
3707    @Nonnull
3708    protected List<LayoutConnectivity> getLayoutConnectivity() {
3709        return trackSegment.getLayoutConnectivity();
3710    }
3711
3712    /**
3713     * {@inheritDoc}
3714     */
3715    @Override
3716    @Nonnull
3717    public List<HitPointType> checkForFreeConnections() {
3718        return new ArrayList<>();
3719    }
3720
3721    /**
3722     * {@inheritDoc}
3723     */
3724    @Override
3725    public boolean checkForUnAssignedBlocks() {
3726        return (getLayoutBlock() != null);
3727    }
3728
3729    /**
3730     * {@inheritDoc}
3731     */
3732    @Override
3733    public void checkForNonContiguousBlocks(
3734            @Nonnull HashMap<String, List<Set<String>>> blockNamesToTrackNameSetsMap) {
3735        /*
3736        * For each (non-null) blocks of this track do:
3737        * #1) If it's got an entry in the blockNamesToTrackNameSetMap then
3738        * #2) If this track is already in the TrackNameSet for this block
3739        *     then return (done!)
3740        * #3) else add a new set (with this block/track) to
3741        *     blockNamesToTrackNameSetMap and
3742        * #4) collect all the connections in this block
3743        * <p>
3744        *     Basically, we're maintaining contiguous track sets for each block found
3745        *     (in blockNamesToTrackNameSetMap)
3746         */
3747        List<Set<String>> TrackNameSets = null;
3748        Set<String> TrackNameSet = null;    // assume not found (pessimist!)
3749        String blockName = getBlockName();
3750        if (!blockName.isEmpty()) {
3751            TrackNameSets = blockNamesToTrackNameSetsMap.get(blockName);
3752            if (TrackNameSets != null) { //(#1)
3753                for (Set<String> checkTrackNameSet : TrackNameSets) {
3754                    if (checkTrackNameSet.contains(getName())) { //(#2)
3755                        TrackNameSet = checkTrackNameSet;
3756                        break;
3757                    }
3758                }
3759            } else {    //(#3)
3760                log.debug("*New block (''{}'') trackNameSets", blockName);
3761                TrackNameSets = new ArrayList<>();
3762                blockNamesToTrackNameSetsMap.put(blockName, TrackNameSets);
3763            }
3764            if (TrackNameSet == null) {
3765                TrackNameSet = new LinkedHashSet<>();
3766                TrackNameSets.add(TrackNameSet);
3767            }
3768            if (TrackNameSet.add(getName())) {
3769                log.debug("*    Add track ''{}'' to TrackNameSets for block ''{}''", getName(), blockName);
3770            }
3771            //(#4)
3772            if (getConnect1() != null) {
3773                getConnect1().collectContiguousTracksNamesInBlockNamed(blockName, TrackNameSet);
3774            }
3775            if (getConnect2() != null) { //(#4)
3776                getConnect2().collectContiguousTracksNamesInBlockNamed(blockName, TrackNameSet);
3777            }
3778        }
3779    }
3780
3781    /**
3782     * {@inheritDoc}
3783     */
3784    @Override
3785    public void collectContiguousTracksNamesInBlockNamed(@Nonnull String blockName,
3786            @Nonnull Set<String> TrackNameSet) {
3787        if (!TrackNameSet.contains(getName())) {
3788            // is this the blockName we're looking for?
3789            if (getBlockName().equals(blockName)) {
3790                // if we are added to the TrackNameSet
3791                if (TrackNameSet.add(getName())) {
3792                    log.debug("*    Add track ''{}''for block ''{}''", getName(), blockName);
3793                }
3794                // these should never be null... but just in case...
3795                // it's time to play... flood your neighbours!
3796                if (getConnect1() != null) {
3797                    getConnect1().collectContiguousTracksNamesInBlockNamed(blockName, TrackNameSet);
3798                }
3799                if (getConnect2() != null) {
3800                    getConnect2().collectContiguousTracksNamesInBlockNamed(blockName, TrackNameSet);
3801                }
3802            }
3803        }
3804    }
3805
3806    /**
3807     * {@inheritDoc}
3808     */
3809    @Override
3810    public void setAllLayoutBlocks(LayoutBlock layoutBlock) {
3811        setLayoutBlock(layoutBlock);
3812    }
3813
3814    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TrackSegmentView.class);
3815}