001package jmri.jmrit.display.layoutEditor;
002
003import java.awt.Color;
004import java.awt.Font;
005import java.awt.Graphics2D;
006import java.awt.event.ActionEvent;
007import java.awt.geom.*;
008import static java.lang.Float.POSITIVE_INFINITY;
009import java.text.ParseException;
010import java.util.*;
011
012import javax.annotation.CheckForNull;
013import javax.annotation.Nonnull;
014import javax.swing.*;
015
016import jmri.*;
017import jmri.jmrit.display.layoutEditor.LayoutTurnout.Geometry;
018import jmri.jmrit.display.layoutEditor.LayoutTurnout.LinkType;
019import jmri.jmrit.display.layoutEditor.LayoutTurnout.TurnoutType;
020import jmri.jmrit.display.layoutEditor.blockRoutingTable.LayoutBlockRouteTableAction;
021import jmri.util.IntlUtilities;
022import jmri.util.MathUtil;
023import jmri.util.swing.JmriJOptionPane;
024import jmri.util.swing.JmriMouseEvent;
025
026/**
027 * MVC View component for the LayoutTurnout class.
028 *
029 * @author Bob Jacobsen Copyright (c) 2020
030 *
031 */
032public class LayoutTurnoutView extends LayoutTrackView {
033
034    public LayoutTurnoutView(@Nonnull LayoutTurnout turnout,
035            @Nonnull Point2D c, double rot,
036            @Nonnull LayoutEditor layoutEditor) {
037        this(turnout, c, rot, 1.0, 1.0, layoutEditor);
038    }
039
040    /**
041     * Constructor method.
042     *
043     * @param turnout      the layout turnout to create the view for.
044     * @param c            where to put it
045     * @param rot          for display
046     * @param xFactor      for display
047     * @param yFactor      for display
048     * @param layoutEditor what layout editor panel to put it in
049     */
050    public LayoutTurnoutView(@Nonnull LayoutTurnout turnout,
051            @Nonnull Point2D c, double rot,
052            double xFactor, double yFactor,
053            @Nonnull LayoutEditor layoutEditor) {
054        super(turnout, c, layoutEditor);
055        this.turnout = turnout;
056
057        setIdent(turnout.getName());
058
059        int version = turnout.getVersion();
060
061        // adjust initial coordinates
062        if (turnout.getTurnoutType() == TurnoutType.LH_TURNOUT) {
063            dispB = new Point2D.Double(layoutEditor.getTurnoutBX(), 0.0);
064            dispA = new Point2D.Double(layoutEditor.getTurnoutCX(), -layoutEditor.getTurnoutWid());
065        } else if (turnout.getTurnoutType() == TurnoutType.RH_TURNOUT) {
066            dispB = new Point2D.Double(layoutEditor.getTurnoutBX(), 0.0);
067            dispA = new Point2D.Double(layoutEditor.getTurnoutCX(), layoutEditor.getTurnoutWid());
068        } else if (turnout.getTurnoutType() == TurnoutType.WYE_TURNOUT) {
069            dispB = new Point2D.Double(layoutEditor.getTurnoutBX(), 0.5 * layoutEditor.getTurnoutWid());
070            dispA = new Point2D.Double(layoutEditor.getTurnoutBX(), -0.5 * layoutEditor.getTurnoutWid());
071        } else if (turnout.getTurnoutType() == TurnoutType.DOUBLE_XOVER) {
072            if (version == 2) {
073                super.setCoordsCenter(new Point2D.Double(layoutEditor.getXOverLong(), layoutEditor.getXOverHWid()));
074                pointB = new Point2D.Double(layoutEditor.getXOverLong() * 2, 0);
075                pointC = new Point2D.Double(layoutEditor.getXOverLong() * 2, (layoutEditor.getXOverHWid() * 2));
076                pointD = new Point2D.Double(0, (layoutEditor.getXOverHWid() * 2));
077                super.setCoordsCenter(c);
078            } else {
079                dispB = new Point2D.Double(layoutEditor.getXOverLong(), -layoutEditor.getXOverHWid());
080                dispA = new Point2D.Double(layoutEditor.getXOverLong(), layoutEditor.getXOverHWid());
081            }
082        } else if (turnout.getTurnoutType() == TurnoutType.RH_XOVER) {
083            if (version == 2) {
084                super.setCoordsCenter(new Point2D.Double(layoutEditor.getXOverLong(), layoutEditor.getXOverHWid()));
085                pointB = new Point2D.Double((layoutEditor.getXOverShort() + layoutEditor.getXOverLong()), 0);
086                pointC = new Point2D.Double(layoutEditor.getXOverLong() * 2, (layoutEditor.getXOverHWid() * 2));
087                pointD = new Point2D.Double((getCoordsCenter().getX() - layoutEditor.getXOverShort()), (layoutEditor.getXOverHWid() * 2));
088                super.setCoordsCenter(c);
089            } else {
090                dispB = new Point2D.Double(layoutEditor.getXOverShort(), -layoutEditor.getXOverHWid());
091                dispA = new Point2D.Double(layoutEditor.getXOverLong(), layoutEditor.getXOverHWid());
092            }
093        } else if (turnout.getTurnoutType() == TurnoutType.LH_XOVER) {
094            if (version == 2) {
095                super.setCoordsCenter(new Point2D.Double(layoutEditor.getXOverLong(), layoutEditor.getXOverHWid()));
096
097                pointA = new Point2D.Double((getCoordsCenter().getX() - layoutEditor.getXOverShort()), 0);
098                pointB = new Point2D.Double((layoutEditor.getXOverLong() * 2), 0);
099                pointC = new Point2D.Double(layoutEditor.getXOverLong() + layoutEditor.getXOverShort(), (layoutEditor.getXOverHWid() * 2));
100                pointD = new Point2D.Double(0, (layoutEditor.getXOverHWid() * 2));
101
102                super.setCoordsCenter(c);
103            } else {
104                dispB = new Point2D.Double(layoutEditor.getXOverLong(), -layoutEditor.getXOverHWid());
105                dispA = new Point2D.Double(layoutEditor.getXOverShort(), layoutEditor.getXOverHWid());
106            }
107        }
108
109        rotateCoords(rot);
110
111        // adjust size of new turnout
112        Point2D pt = new Point2D.Double(Math.round(dispB.getX() * xFactor),
113                Math.round(dispB.getY() * yFactor));
114        dispB = pt;
115        pt = new Point2D.Double(Math.round(dispA.getX() * xFactor),
116                Math.round(dispA.getY() * yFactor));
117        dispA = pt;
118
119        editor = new jmri.jmrit.display.layoutEditor.LayoutEditorDialogs.LayoutTurnoutEditor(layoutEditor);
120    }
121
122    /**
123     * Returns true if this is a turnout (not a crossover or slip)
124     *
125     * @param type the turnout type
126     * @return boolean true if this is a turnout
127     */
128    public static boolean isTurnoutTypeTurnout(TurnoutType type) {
129        return LayoutTurnout.isTurnoutTypeTurnout(type);
130    }
131
132    /**
133     * Returns true if this is a turnout (not a crossover or slip)
134     *
135     * @return boolean true if this is a turnout
136     */
137    public boolean isTurnoutTypeTurnout() {
138        return turnout.isTurnoutTypeTurnout();
139    }
140
141    /**
142     * Returns true if this is a crossover
143     *
144     * @param type the turnout type
145     * @return boolean true if this is a crossover
146     */
147    public static boolean isTurnoutTypeXover(TurnoutType type) {
148        return LayoutTurnout.isTurnoutTypeXover(type);
149    }
150
151    /**
152     * Returns true if this is a crossover
153     *
154     * @return boolean true if this is a crossover
155     */
156    public boolean isTurnoutTypeXover() {
157        return turnout.isTurnoutTypeXover();
158    }
159
160    /**
161     * Returns true if this is a slip
162     *
163     * @param type the turnout type
164     * @return boolean true if this is a slip
165     */
166    public static boolean isTurnoutTypeSlip(TurnoutType type) {
167        return LayoutTurnout.isTurnoutTypeSlip(type);
168    }
169
170    /**
171     * Returns true if this is a slip
172     *
173     * @return boolean true if this is a slip
174     */
175    public boolean isTurnoutTypeSlip() {
176        return turnout.isTurnoutTypeSlip();
177    }
178
179    /**
180     * Returns true if this has a single-track entrance end. (turnout or wye)
181     *
182     * @param type the turnout type
183     * @return boolean true if single track entrance
184     */
185    public static boolean hasEnteringSingleTrack(TurnoutType type) {
186        return LayoutTurnout.hasEnteringSingleTrack(type);
187    }
188
189    /**
190     * Returns true if this has a single-track entrance end. (turnout or wye)
191     *
192     * @return boolean true if single track entrance
193     */
194    public boolean hasEnteringSingleTrack() {
195        return LayoutTurnout.hasEnteringSingleTrack(getTurnoutType());
196    }
197
198    /**
199     * Returns true if this has double track on the entrance end (crossover or
200     * slip)
201     *
202     * @param type the turnout type
203     * @return boolean true if double track entrance
204     */
205    public static boolean hasEnteringDoubleTrack(TurnoutType type) {
206        return LayoutTurnout.hasEnteringDoubleTrack(type);
207    }
208
209    /**
210     * Returns true if this has double track on the entrance end (crossover or
211     * slip)
212     *
213     * @return boolean true if double track entrance
214     */
215    public boolean hasEnteringDoubleTrack() {
216        return turnout.hasEnteringDoubleTrack();
217    }
218
219    // operational instance variables (not saved between sessions)
220    public static final int UNKNOWN = Turnout.UNKNOWN;
221    public static final int INCONSISTENT = Turnout.INCONSISTENT;
222    public static final int STATE_AC = 0x02;
223    public static final int STATE_BD = 0x04;
224    public static final int STATE_AD = 0x06;
225    public static final int STATE_BC = 0x08;
226
227    // program default turnout size parameters
228    public static final double turnoutBXDefault = 20.0;  // RH, LH, WYE
229    public static final double turnoutCXDefault = 20.0;
230    public static final double turnoutWidDefault = 10.0;
231    public static final double xOverLongDefault = 30.0;   // DOUBLE_XOVER, RH_XOVER, LH_XOVER
232    public static final double xOverHWidDefault = 10.0;
233    public static final double xOverShortDefault = 10.0;
234
235    // operational instance variables (not saved between sessions)
236    protected NamedBeanHandle<Turnout> namedTurnout = null;
237    // Second turnout is used to either throw a second turnout in a cross over or if one turnout address is used to throw two physical ones
238    protected NamedBeanHandle<Turnout> secondNamedTurnout = null;
239
240    // default is package protected
241    protected NamedBeanHandle<LayoutBlock> namedLayoutBlockA = null;
242    protected NamedBeanHandle<LayoutBlock> namedLayoutBlockB = null;  // Xover - second block, if there is one
243    protected NamedBeanHandle<LayoutBlock> namedLayoutBlockC = null;  // Xover - third block, if there is one
244    protected NamedBeanHandle<LayoutBlock> namedLayoutBlockD = null;  // Xover - forth block, if there is one
245
246    protected NamedBeanHandle<SignalHead> signalA1HeadNamed = null; // signal 1 (continuing) (throat for RH, LH, WYE)
247    protected NamedBeanHandle<SignalHead> signalA2HeadNamed = null; // signal 2 (diverging) (throat for RH, LH, WYE)
248    protected NamedBeanHandle<SignalHead> signalA3HeadNamed = null; // signal 3 (second diverging) (3-way turnouts only)
249    protected NamedBeanHandle<SignalHead> signalB1HeadNamed = null; // continuing (RH, LH, WYE) signal 1 (double crossover)
250    protected NamedBeanHandle<SignalHead> signalB2HeadNamed = null; // LH_Xover and double crossover only
251    protected NamedBeanHandle<SignalHead> signalC1HeadNamed = null; // diverging (RH, LH, WYE) signal 1 (double crossover)
252    protected NamedBeanHandle<SignalHead> signalC2HeadNamed = null; // RH_Xover and double crossover only
253    protected NamedBeanHandle<SignalHead> signalD1HeadNamed = null; // single or double crossover only
254    protected NamedBeanHandle<SignalHead> signalD2HeadNamed = null; // LH_Xover and double crossover only
255
256    public Point2D dispB = new Point2D.Double(20.0, 0.0);
257    public Point2D dispA = new Point2D.Double(20.0, 10.0);
258    public Point2D pointA = new Point2D.Double(0, 0);
259    public Point2D pointB = new Point2D.Double(40, 0);
260    public Point2D pointC = new Point2D.Double(60, 20);
261    public Point2D pointD = new Point2D.Double(20, 20);
262
263    public boolean showUnknown = false; // if true, show "?" when state is UNKNOWN
264    
265    private int version = 1;
266
267    private final boolean useBlockSpeed = false;
268
269    // temporary reference to the Editor that will eventually be part of View
270    protected jmri.jmrit.display.layoutEditor.LayoutEditorDialogs.LayoutTurnoutEditor editor;
271
272    final private LayoutTurnout turnout;
273
274    public final LayoutTurnout getLayoutTurnout() {
275        return turnout;
276    }  // getTurnout() gets the real Turnout in the LayoutTurnout
277
278    /**
279     * {@inheritDoc}
280     */
281    // this should only be used for debugging...
282    @Override
283    @Nonnull
284    public String toString() {
285        return "LayoutTurnout " + getName();
286    }
287
288    //
289    // Accessor methods
290    //
291    public int getVersion() {
292        return version;
293    }
294
295    public void setVersion(int v) {
296        version = v;
297    }
298
299    public boolean useBlockSpeed() {
300        return useBlockSpeed;
301    }
302
303    // @CheckForNull - can this be null? or ""?
304    public String getTurnoutName() {
305        return turnout.getTurnoutName();
306    }
307
308    // @CheckForNull - can this be null? or ""?
309    public String getSecondTurnoutName() {
310        return turnout.getSecondTurnoutName();
311    }
312
313    @Nonnull
314    public String getBlockName() {
315        return turnout.getBlockName();
316    }
317
318    @Nonnull
319    public String getBlockBName() {
320        return turnout.getBlockBName();
321    }
322
323    @Nonnull
324    public String getBlockCName() {
325        return turnout.getBlockCName();
326    }
327
328    @Nonnull
329    public String getBlockDName() {
330        return turnout.getBlockDName();
331    }
332
333    @CheckForNull
334    public SignalHead getSignalHead(Geometry loc) {
335        return turnout.getSignalHead(loc);
336    }
337
338    @CheckForNull
339    public SignalHead getSignalA1() {
340        return turnout.getSignalA1();
341    }
342
343    @Nonnull
344    public String getSignalA1Name() {
345        return turnout.getSignalA1Name();
346    }
347
348    public void setSignalA1Name(@CheckForNull String signalHead) {
349        turnout.setSignalA1Name(signalHead);
350    }
351
352    @CheckForNull
353    public SignalHead getSignalA2() {
354        return turnout.getSignalA2();
355    }
356
357    @Nonnull
358    public String getSignalA2Name() {
359        return turnout.getSignalA2Name();
360    }
361
362    public void setSignalA2Name(@CheckForNull String signalHead) {
363        turnout.setSignalA2Name(signalHead);
364    }
365
366    @CheckForNull
367    public SignalHead getSignalA3() {
368        return turnout.getSignalA3();
369    }
370
371    @Nonnull
372    public String getSignalA3Name() {
373        return turnout.getSignalA3Name();
374    }
375
376    public void setSignalA3Name(@CheckForNull String signalHead) {
377        turnout.setSignalA3Name(signalHead);
378    }
379
380    @CheckForNull
381    public SignalHead getSignalB1() {
382        return turnout.getSignalB1();
383    }
384
385    @Nonnull
386    public String getSignalB1Name() {
387        return turnout.getSignalB1Name();
388    }
389
390    public void setSignalB1Name(@CheckForNull String signalHead) {
391        turnout.setSignalB1Name(signalHead);
392    }
393
394    @CheckForNull
395    public SignalHead getSignalB2() {
396        return turnout.getSignalB2();
397    }
398
399    @Nonnull
400    public String getSignalB2Name() {
401        return turnout.getSignalB2Name();
402    }
403
404    public void setSignalB2Name(@CheckForNull String signalHead) {
405        turnout.setSignalB2Name(signalHead);
406    }
407
408    @CheckForNull
409    public SignalHead getSignalC1() {
410        return turnout.getSignalC1();
411    }
412
413    @Nonnull
414    public String getSignalC1Name() {
415        return turnout.getSignalC1Name();
416    }
417
418    public void setSignalC1Name(@CheckForNull String signalHead) {
419        turnout.setSignalC1Name(signalHead);
420    }
421
422    @CheckForNull
423    public SignalHead getSignalC2() {
424        return turnout.getSignalC2();
425    }
426
427    @Nonnull
428    public String getSignalC2Name() {
429        return turnout.getSignalC2Name();
430    }
431
432    public void setSignalC2Name(@CheckForNull String signalHead) {
433        turnout.setSignalC2Name(signalHead);
434    }
435
436    @CheckForNull
437    public SignalHead getSignalD1() {
438        return turnout.getSignalD1();
439    }
440
441    @Nonnull
442    public String getSignalD1Name() {
443        return turnout.getSignalD1Name();
444    }
445
446    public void setSignalD1Name(@CheckForNull String signalHead) {
447        turnout.setSignalD1Name(signalHead);
448    }
449
450    @CheckForNull
451    public SignalHead getSignalD2() {
452        return turnout.getSignalD2();
453    }
454
455    @Nonnull
456    public String getSignalD2Name() {
457        return turnout.getSignalD2Name();
458    }
459
460    public void setSignalD2Name(@CheckForNull String signalHead) {
461        turnout.setSignalD2Name(signalHead);
462    }
463
464    public void removeBeanReference(@CheckForNull jmri.NamedBean nb) {
465        turnout.removeBeanReference(nb);
466    }
467
468    public void setShowUnknown(boolean show) {
469        showUnknown = show;
470    }
471    
472    public boolean getShowUnknown() {
473        return showUnknown;
474    }
475    
476    /**
477     * {@inheritDoc}
478     */
479    @Override
480    public boolean canRemove() {
481        return turnout.canRemove();
482    }
483
484    /**
485     * Build a list of sensors, signal heads, and signal masts attached to a
486     * turnout point.
487     *
488     * @param pointName Specify the point (A-D) or all (All) points.
489     * @return a list of bean reference names.
490     */
491    @Nonnull
492    public ArrayList<String> getBeanReferences(String pointName) {
493        throw new IllegalArgumentException("should be called on LayoutTurnout");
494    }
495
496    @Nonnull
497    public String getSignalAMastName() {
498        return turnout.getSignalAMastName();
499    }
500
501    @CheckForNull
502    public SignalMast getSignalAMast() {
503        return turnout.getSignalAMast();
504    }
505
506    public void setSignalAMast(@CheckForNull String signalMast) {
507        turnout.setSignalAMast(signalMast);
508    }
509
510    @Nonnull
511    public String getSignalBMastName() {
512        return turnout.getSignalBMastName();
513    }
514
515    @CheckForNull
516    public SignalMast getSignalBMast() {
517        return turnout.getSignalBMast();
518    }
519
520    public void setSignalBMast(@CheckForNull String signalMast) {
521        turnout.setSignalBMast(signalMast);
522    }
523
524    @Nonnull
525    public String getSignalCMastName() {
526        return turnout.getSignalCMastName();
527    }
528
529    @CheckForNull
530    public SignalMast getSignalCMast() {
531        return turnout.getSignalCMast();
532    }
533
534    public void setSignalCMast(@CheckForNull String signalMast) {
535        turnout.setSignalCMast(signalMast);
536    }
537
538    @Nonnull
539    public String getSignalDMastName() {
540        return turnout.getSignalDMastName();
541    }
542
543    @CheckForNull
544    public SignalMast getSignalDMast() {
545        return turnout.getSignalDMast();
546    }
547
548    public void setSignalDMast(@CheckForNull String signalMast) {
549        turnout.setSignalDMast(signalMast);
550    }
551
552    @Nonnull
553    public String getSensorAName() {
554        return turnout.getSensorAName();
555    }
556
557    @CheckForNull
558    public Sensor getSensorA() {
559        return turnout.getSensorA();
560    }
561
562    public void setSensorA(@CheckForNull String sensorName) {
563        turnout.setSensorA(sensorName);
564    }
565
566    @Nonnull
567    public String getSensorBName() {
568        return turnout.getSensorBName();
569    }
570
571    @CheckForNull
572    public Sensor getSensorB() {
573        return turnout.getSensorB();
574    }
575
576    public void setSensorB(@CheckForNull String sensorName) {
577        turnout.setSensorB(sensorName);
578    }
579
580    @Nonnull
581    public String getSensorCName() {
582        return turnout.getSensorCName();
583    }
584
585    @CheckForNull
586    public Sensor getSensorC() {
587        return turnout.getSensorC();
588    }
589
590    public void setSensorC(@CheckForNull String sensorName) {
591        turnout.setSensorC(sensorName);
592    }
593
594    @Nonnull
595    public String getSensorDName() {
596        return turnout.getSensorDName();
597    }
598
599    @CheckForNull
600    public Sensor getSensorD() {
601        return turnout.getSensorD();
602    }
603
604    public void setSensorD(@CheckForNull String sensorName) {
605        turnout.setSensorD(sensorName);
606    }
607
608    public String getLinkedTurnoutName() {
609        return turnout.getLinkedTurnoutName();
610    }
611
612    public void setLinkedTurnoutName(@Nonnull String s) {
613        turnout.setSensorD(s);
614    }  // Could be done with changing over to a NamedBeanHandle
615
616    public LinkType getLinkType() {
617        return turnout.getLinkType();
618    }
619
620    public void setLinkType(LinkType ltype) {
621        turnout.setLinkType(ltype);
622    }
623
624    public TurnoutType getTurnoutType() {
625        return turnout.getTurnoutType();
626    }
627
628    public LayoutTrack getConnectA() {
629        return turnout.getConnectA();
630    }
631
632    public LayoutTrack getConnectB() {
633        return turnout.getConnectB();
634    }
635
636    public LayoutTrack getConnectC() {
637        return turnout.getConnectC();
638    }
639
640    public LayoutTrack getConnectD() {
641        return turnout.getConnectD();
642    }
643
644    /**
645     * @return null if no turnout set // temporary? Might want to run all calls
646     *         through this class; but this is getModel equiv
647     */
648    // @CheckForNull  temporary
649    public Turnout getTurnout() {
650        return turnout.getTurnout();
651    }
652
653    public int getContinuingSense() {
654        return turnout.getContinuingSense();
655    }
656
657    /**
658     *
659     * @return true is the continuingSense matches the known state
660     */
661    public boolean isInContinuingSenseState() {
662        return turnout.isInContinuingSenseState();
663    }
664
665    public void setTurnout(@Nonnull String tName) {
666        turnout.setTurnout(tName);
667    }
668
669    // @CheckForNull - need to have a better way to handle null case
670    public Turnout getSecondTurnout() {
671        return turnout.getSecondTurnout();
672    }
673
674    public void setSecondTurnout(@Nonnull String tName) {
675        turnout.setSecondTurnout(tName);
676    }
677
678    public void setSecondTurnoutInverted(boolean inverted) {
679        turnout.setSecondTurnoutInverted(inverted);
680    }
681
682    public void setContinuingSense(int sense) {
683        turnout.setContinuingSense(sense);
684    }
685
686    public void setDisabled(boolean state) {
687        turnout.setDisabled(state);
688        if (layoutEditor != null) {
689            layoutEditor.redrawPanel();
690        }
691    }
692
693    public boolean isDisabled() {
694        return turnout.isDisabled();
695    }
696
697    public void setDisableWhenOccupied(boolean state) {
698        turnout.setDisableWhenOccupied(state);
699        if (layoutEditor != null) {
700            layoutEditor.redrawPanel();
701        }
702    }
703
704    public boolean isDisabledWhenOccupied() {
705        return turnout.isDisabledWhenOccupied();
706    }
707
708    /**
709     * {@inheritDoc}
710     */
711    @Override
712    @CheckForNull
713    public LayoutTrack getConnection(HitPointType connectionType) throws jmri.JmriException {
714        return turnout.getConnection(connectionType);
715    }
716
717    /**
718     * {@inheritDoc}
719     */
720    @Override
721    public void setConnection(HitPointType connectionType, @CheckForNull LayoutTrack o, HitPointType type) throws jmri.JmriException {
722        turnout.setConnection(connectionType, o, type);
723    }
724
725    public void setConnectA(@CheckForNull LayoutTrack o, HitPointType type) {
726        turnout.setConnectA(o, type);
727    }
728
729    public void setConnectB(@CheckForNull LayoutTrack o, HitPointType type) {
730        turnout.setConnectB(o, type);
731    }
732
733    public void setConnectC(@CheckForNull LayoutTrack o, HitPointType type) {
734        turnout.setConnectC(o, type);
735    }
736
737    public void setConnectD(@CheckForNull LayoutTrack o, HitPointType type) {
738        turnout.setConnectD(o, type);
739    }
740
741    // @CheckForNull - temporary while we work on centralized protection
742    public LayoutBlock getLayoutBlock() {
743        return turnout.getLayoutBlock();
744    }
745
746    // @CheckForNull - temporary while we work on centralized protection
747    public LayoutBlock getLayoutBlockB() {
748        return turnout.getLayoutBlockB();
749    }
750
751    // @CheckForNull - temporary while we work on centralized protection
752    public LayoutBlock getLayoutBlockC() {
753        return turnout.getLayoutBlockC();
754    }
755
756    // @CheckForNull - temporary while we work on centralized protection
757    public LayoutBlock getLayoutBlockD() {
758        return turnout.getLayoutBlockD();
759    }
760
761    @Nonnull
762    public Point2D getCoordsA() {
763        if (isTurnoutTypeXover()) {
764            if (version == 2) {
765                return pointA;
766            }
767            return MathUtil.subtract(getCoordsCenter(), dispA);
768        } else if (getTurnoutType() == TurnoutType.WYE_TURNOUT) {
769            return MathUtil.subtract(getCoordsCenter(), MathUtil.midPoint(dispB, dispA));
770        } else {
771            return MathUtil.subtract(getCoordsCenter(), dispB);
772        }
773    }
774
775    @Nonnull
776    public Point2D getCoordsB() {
777        if ((version == 2) && isTurnoutTypeXover()) {
778            return pointB;
779        }
780        return MathUtil.add(getCoordsCenter(), dispB);
781    }
782
783    @Nonnull
784    public Point2D getCoordsC() {
785        if ((version == 2) && isTurnoutTypeXover()) {
786            return pointC;
787        }
788        return MathUtil.add(getCoordsCenter(), dispA);
789    }
790
791    @Nonnull
792    public Point2D getCoordsD() {
793        if ((version == 2) && isTurnoutTypeXover()) {
794            return pointD;
795        }
796        // only allowed for single and double crossovers
797        return MathUtil.subtract(getCoordsCenter(), dispB);
798    }
799
800    /**
801     * {@inheritDoc}
802     */
803    @Override
804    @Nonnull
805    public Point2D getCoordsForConnectionType(HitPointType connectionType) {
806        Point2D result = getCoordsCenter();
807        switch (connectionType) {
808            case TURNOUT_CENTER:
809                break;
810            case TURNOUT_A:
811                result = getCoordsA();
812                break;
813            case TURNOUT_B:
814                result = getCoordsB();
815                break;
816            case TURNOUT_C:
817                result = getCoordsC();
818                break;
819            case TURNOUT_D:
820                result = getCoordsD();
821                break;
822            default:
823                log.error("{}.getCoordsForConnectionType({}); Invalid Connection Type",
824                        getName(), connectionType); // NOI18N
825        }
826        return result;
827    }
828
829    /**
830     * {@inheritDoc}
831     */
832    @Override
833    @Nonnull
834    public Rectangle2D getBounds() {
835        Rectangle2D result;
836
837        Point2D pointA = getCoordsA();
838        result = new Rectangle2D.Double(pointA.getX(), pointA.getY(), 0, 0);
839        result.add(getCoordsB());
840        result.add(getCoordsC());
841        if (isTurnoutTypeXover() || isTurnoutTypeSlip()) {
842            result.add(getCoordsD());
843        }
844        return result;
845    }
846
847    // updates connectivity for blocks assigned to this turnout and connected track segments
848    public void updateBlockInfo() {
849        turnout.updateBlockInfo();
850    }
851
852    /**
853     * Set default size parameters to correspond to this turnout's size.
854     * <p>
855     * note: only protected so LayoutTurnoutTest can call it
856     */
857    protected void setUpDefaultSize() {
858        // remove the overall scale factor
859        double bX = dispB.getX() / layoutEditor.gContext.getXScale();
860        double bY = dispB.getY() / layoutEditor.gContext.getYScale();
861        double cX = dispA.getX() / layoutEditor.gContext.getXScale();
862        double cY = dispA.getY() / layoutEditor.gContext.getYScale();
863        // calculate default parameters according to type of turnout
864        double lenB = Math.hypot(bX, bY);
865        double lenC = Math.hypot(cX, cY);
866        double distBC = Math.hypot(bX - cX, bY - cY);
867        if ((getTurnoutType() == TurnoutType.LH_TURNOUT)
868                || (getTurnoutType() == TurnoutType.RH_TURNOUT)) {
869
870            layoutEditor.setTurnoutBX(Math.round(lenB + 0.1));
871            double xc = ((bX * cX) + (bY * cY)) / lenB;
872            layoutEditor.setTurnoutCX(Math.round(xc + 0.1));
873            layoutEditor.setTurnoutWid(Math.round(Math.sqrt((lenC * lenC) - (xc * xc)) + 0.1));
874        } else if (getTurnoutType() == TurnoutType.WYE_TURNOUT) {
875            double xx = Math.sqrt((lenB * lenB) - (0.25 * (distBC * distBC)));
876            layoutEditor.setTurnoutBX(Math.round(xx + 0.1));
877            layoutEditor.setTurnoutCX(Math.round(xx + 0.1));
878            layoutEditor.setTurnoutWid(Math.round(distBC + 0.1));
879        } else {
880            if (version == 2) {
881                double aX = pointA.getX() / layoutEditor.gContext.getXScale();
882                double aY = pointA.getY() / layoutEditor.gContext.getYScale();
883                bX = pointB.getX() / layoutEditor.gContext.getXScale();
884                bY = pointB.getY() / layoutEditor.gContext.getYScale();
885                cX = pointC.getX() / layoutEditor.gContext.getXScale();
886                cY = pointC.getY() / layoutEditor.gContext.getYScale();
887                double lenAB = Math.hypot(bX - aX, bY - aY);
888                if (getTurnoutType() == TurnoutType.DOUBLE_XOVER) {
889                    double lenBC = Math.hypot(bX - cX, bY - cY);
890                    layoutEditor.setXOverLong(Math.round(lenAB / 2)); // set to half to be backwardly compatible
891                    layoutEditor.setXOverHWid(Math.round(lenBC / 2));
892                    layoutEditor.setXOverShort(Math.round((0.5 * lenAB) / 2));
893                } else if (getTurnoutType() == TurnoutType.RH_XOVER) {
894                    lenAB = lenAB / 3;
895                    layoutEditor.setXOverShort(Math.round(lenAB));
896                    layoutEditor.setXOverLong(Math.round(lenAB * 2));
897                    double opp = (aY - bY);
898                    double ang = Math.asin(opp / (lenAB * 3));
899                    opp = Math.sin(ang) * lenAB;
900                    bY = bY + opp;
901                    double adj = Math.cos(ang) * lenAB;
902                    bX = bX + adj;
903                    double lenBC = Math.hypot(bX - cX, bY - cY);
904                    layoutEditor.setXOverHWid(Math.round(lenBC / 2));
905                } else if (getTurnoutType() == TurnoutType.LH_XOVER) {
906                    double dY = pointD.getY() / layoutEditor.gContext.getYScale();
907                    lenAB = lenAB / 3;
908                    layoutEditor.setXOverShort(Math.round(lenAB));
909                    layoutEditor.setXOverLong(Math.round(lenAB * 2));
910                    double opp = (dY - cY);
911                    double ang = Math.asin(opp / (lenAB * 3)); // Length of AB should be the same as CD
912                    opp = Math.sin(ang) * lenAB;
913                    cY = cY + opp;
914                    double adj = Math.cos(ang) * lenAB;
915                    cX = cX + adj;
916                    double lenBC = Math.hypot(bX - cX, bY - cY);
917                    layoutEditor.setXOverHWid(Math.round(lenBC / 2));
918                }
919            } else if (getTurnoutType() == TurnoutType.DOUBLE_XOVER) {
920                double lng = Math.sqrt((lenB * lenB) - (0.25 * (distBC * distBC)));
921                layoutEditor.setXOverLong(Math.round(lng + 0.1));
922                layoutEditor.setXOverHWid(Math.round((0.5 * distBC) + 0.1));
923                layoutEditor.setXOverShort(Math.round((0.5 * lng) + 0.1));
924            } else if (getTurnoutType() == TurnoutType.RH_XOVER) {
925                double distDC = Math.hypot(bX + cX, bY + cY);
926                layoutEditor.setXOverShort(Math.round((0.25 * distDC) + 0.1));
927                layoutEditor.setXOverLong(Math.round((0.75 * distDC) + 0.1));
928                double hwid = Math.sqrt((lenC * lenC) - (0.5625 * distDC * distDC));
929                layoutEditor.setXOverHWid(Math.round(hwid + 0.1));
930            } else if (getTurnoutType() == TurnoutType.LH_XOVER) {
931                double distDC = Math.hypot(bX + cX, bY + cY);
932                layoutEditor.setXOverShort(Math.round((0.25 * distDC) + 0.1));
933                layoutEditor.setXOverLong(Math.round((0.75 * distDC) + 0.1));
934                double hwid = Math.sqrt((lenC * lenC) - (0.0625 * distDC * distDC));
935                layoutEditor.setXOverHWid(Math.round(hwid + 0.1));
936            }
937        }
938    }
939
940    /**
941     * Set up Layout Block(s) for this Turnout.
942     *
943     * @param newLayoutBlock See {@link LayoutTurnout#setLayoutBlock} for
944     *                       definition
945     */
946    public void setLayoutBlock(LayoutBlock newLayoutBlock) {
947        turnout.setLayoutBlock(newLayoutBlock);
948        // correct any graphical artifacts
949        setTrackSegmentBlocks();
950    }
951
952    public void setLayoutBlockB(LayoutBlock newLayoutBlock) {
953        turnout.setLayoutBlockB(newLayoutBlock);
954        // correct any graphical artifacts
955        setTrackSegmentBlocks();
956    }
957
958    public void setLayoutBlockC(LayoutBlock newLayoutBlock) {
959        turnout.setLayoutBlockC(newLayoutBlock);
960        // correct any graphical artifacts
961        setTrackSegmentBlocks();
962    }
963
964    public void setLayoutBlockD(LayoutBlock newLayoutBlock) {
965        turnout.setLayoutBlockD(newLayoutBlock);
966        // correct any graphical artifacts
967        setTrackSegmentBlocks();
968    }
969
970    public void setLayoutBlockByName(@Nonnull String name) {
971        turnout.setLayoutBlockByName(name);
972    }
973
974    public void setLayoutBlockBByName(@Nonnull String name) {
975        turnout.setLayoutBlockByName(name);
976    }
977
978    public void setLayoutBlockCByName(@Nonnull String name) {
979        turnout.setLayoutBlockByName(name);
980    }
981
982    public void setLayoutBlockDByName(@Nonnull String name) {
983        turnout.setLayoutBlockByName(name);
984    }
985
986    /**
987     * Check each connection point and update the block value for very short
988     * track segments.
989     *
990     * @since 4.11.6
991     */
992    void setTrackSegmentBlocks() {
993        setTrackSegmentBlock(HitPointType.TURNOUT_A, false);
994        setTrackSegmentBlock(HitPointType.TURNOUT_B, false);
995        setTrackSegmentBlock(HitPointType.TURNOUT_C, false);
996        if (hasEnteringDoubleTrack()) {
997            setTrackSegmentBlock(HitPointType.TURNOUT_D, false);
998        }
999    }
1000
1001    /**
1002     * Update the block for a track segment that provides a (graphically) short
1003     * connection between a turnout and another object, normally another
1004     * turnout. These are hard to see and are frequently missed.
1005     * <p>
1006     * Skip block changes if signal heads, masts or sensors have been assigned.
1007     * Only track segments with a length less than the turnout circle radius
1008     * will be changed.
1009     *
1010     * @since 4.11.6
1011     * @param pointType   The point type which indicates which turnout
1012     *                    connection.
1013     * @param isAutomatic True for the automatically generated track segment
1014     *                    created by the drag-n-drop process. False for existing
1015     *                    connections which require a track segment length
1016     *                    calculation.
1017     */
1018    void setTrackSegmentBlock(HitPointType pointType, boolean isAutomatic) {
1019        TrackSegment trkSeg;
1020        Point2D pointCoord;
1021        LayoutBlock blockA = getLayoutBlock();
1022        LayoutBlock blockB = getLayoutBlock();
1023        LayoutBlock blockC = getLayoutBlock();
1024        LayoutBlock blockD = getLayoutBlock();
1025        LayoutBlock currBlk = blockA;
1026
1027        switch (pointType) {
1028            case TURNOUT_A:
1029            case SLIP_A:
1030                if (signalA1HeadNamed != null) {
1031                    return;
1032                }
1033                if (signalA2HeadNamed != null) {
1034                    return;
1035                }
1036                if (signalA3HeadNamed != null) {
1037                    return;
1038                }
1039                if (getSignalAMast() != null) {
1040                    return;
1041                }
1042                if (getSensorA() != null) {
1043                    return;
1044                }
1045                trkSeg = (TrackSegment) getConnectA();
1046                pointCoord = getCoordsA();
1047                break;
1048            case TURNOUT_B:
1049            case SLIP_B:
1050                if (signalB1HeadNamed != null) {
1051                    return;
1052                }
1053                if (signalB2HeadNamed != null) {
1054                    return;
1055                }
1056                if (getSignalBMast() != null) {
1057                    return;
1058                }
1059                if (getSensorB() != null) {
1060                    return;
1061                }
1062                trkSeg = (TrackSegment) getConnectB();
1063                pointCoord = getCoordsB();
1064                if (isTurnoutTypeXover()) {
1065                    currBlk = blockB != null ? blockB : blockA;
1066                }
1067                break;
1068            case TURNOUT_C:
1069            case SLIP_C:
1070                if (signalC1HeadNamed != null) {
1071                    return;
1072                }
1073                if (signalC2HeadNamed != null) {
1074                    return;
1075                }
1076                if (getSignalCMast() != null) {
1077                    return;
1078                }
1079                if (getSensorC() != null) {
1080                    return;
1081                }
1082                trkSeg = (TrackSegment) getConnectC();
1083                pointCoord = getCoordsC();
1084                if (isTurnoutTypeXover()) {
1085                    currBlk = blockC != null ? blockC : blockA;
1086                }
1087                break;
1088            case TURNOUT_D:
1089            case SLIP_D:
1090                if (signalD1HeadNamed != null) {
1091                    return;
1092                }
1093                if (signalD2HeadNamed != null) {
1094                    return;
1095                }
1096                if (getSignalDMast() != null) {
1097                    return;
1098                }
1099                if (getSensorD() != null) {
1100                    return;
1101                }
1102                trkSeg = (TrackSegment) getConnectD();
1103                pointCoord = getCoordsD();
1104                if (isTurnoutTypeXover()) {
1105                    currBlk = blockD != null ? blockD : blockA;
1106                }
1107                break;
1108            default:
1109                log.error("{}.setTrackSegmentBlock({}, {}); Invalid pointType",
1110                        getName(), pointType, isAutomatic ? "AUTO" : "NON-AUTO");
1111                return;
1112        }
1113        if (trkSeg != null) {
1114            double chkSize = LayoutEditor.SIZE * layoutEditor.getTurnoutCircleSize();
1115            double segLength = 0;
1116            if (!isAutomatic) {
1117                Point2D segCenter = getCoordsCenter();
1118                segLength = MathUtil.distance(pointCoord, segCenter) * 2;
1119            }
1120            if (segLength < chkSize) {
1121
1122                log.debug("Set block:");
1123                log.debug("    seg: {}", trkSeg);
1124                log.debug("    cor: {}", pointCoord);
1125                log.debug("    blk: {}", (currBlk == null) ? "null" : currBlk.getDisplayName());
1126                log.debug("    len: {}", segLength);
1127
1128                trkSeg.setLayoutBlock(currBlk);
1129                layoutEditor.getLEAuxTools().setBlockConnectivityChanged();
1130            }
1131        }
1132    }
1133
1134    /**
1135     * Test if turnout legs are mainline track or not.
1136     *
1137     * @return true if connecting track segment is mainline; Defaults to not
1138     *         mainline if connecting track segment is missing
1139     */
1140    public boolean isMainlineA() {
1141        return turnout.isMainlineA();
1142    }
1143
1144    public boolean isMainlineB() {
1145        return turnout.isMainlineB();
1146    }
1147
1148    public boolean isMainlineC() {
1149        return turnout.isMainlineC();
1150    }
1151
1152    public boolean isMainlineD() {
1153        return turnout.isMainlineD();
1154    }
1155
1156    /**
1157     * {@inheritDoc}
1158     */
1159    @Override
1160    protected HitPointType findHitPointType(@Nonnull Point2D hitPoint, boolean useRectangles, boolean requireUnconnected) {
1161        HitPointType result = HitPointType.NONE;  // assume point not on connection
1162        // note: optimization here: instead of creating rectangles for all the
1163        // points to check below, we create a rectangle for the test point
1164        // and test if the points below are in that rectangle instead.
1165        Rectangle2D r = layoutEditor.layoutEditorControlCircleRectAt(hitPoint);
1166        Point2D p, minPoint = MathUtil.zeroPoint2D;
1167
1168        double circleRadius = LayoutEditor.SIZE * layoutEditor.getTurnoutCircleSize();
1169        double distance, minDistance = POSITIVE_INFINITY;
1170
1171        // check center coordinates
1172        if (!requireUnconnected) {
1173            p = getCoordsCenter();
1174            distance = MathUtil.distance(p, hitPoint);
1175            if (distance < minDistance) {
1176                minDistance = distance;
1177                minPoint = p;
1178                result = HitPointType.TURNOUT_CENTER;
1179            }
1180        }
1181
1182        // check the A connection point
1183        if (!requireUnconnected || (getConnectA() == null)) {
1184            p = getCoordsA();
1185            distance = MathUtil.distance(p, hitPoint);
1186            if (distance < minDistance) {
1187                minDistance = distance;
1188                minPoint = p;
1189                result = HitPointType.TURNOUT_A;
1190            }
1191        }
1192
1193        // check the B connection point
1194        if (!requireUnconnected || (getConnectB() == null)) {
1195            p = getCoordsB();
1196            distance = MathUtil.distance(p, hitPoint);
1197            if (distance < minDistance) {
1198                minDistance = distance;
1199                minPoint = p;
1200                result = HitPointType.TURNOUT_B;
1201            }
1202        }
1203
1204        // check the C connection point
1205        if (!requireUnconnected || (getConnectC() == null)) {
1206            p = getCoordsC();
1207            distance = MathUtil.distance(p, hitPoint);
1208            if (distance < minDistance) {
1209                minDistance = distance;
1210                minPoint = p;
1211                result = HitPointType.TURNOUT_C;
1212            }
1213        }
1214
1215        // check the D connection point
1216        if (isTurnoutTypeXover()) {
1217            if (!requireUnconnected || (getConnectD() == null)) {
1218                p = getCoordsD();
1219                distance = MathUtil.distance(p, hitPoint);
1220                if (distance < minDistance) {
1221                    minDistance = distance;
1222                    minPoint = p;
1223                    result = HitPointType.TURNOUT_D;
1224                }
1225            }
1226        }
1227        if ((useRectangles && !r.contains(minPoint))
1228                || (!useRectangles && (minDistance > circleRadius))) {
1229            result = HitPointType.NONE;
1230        }
1231        return result;
1232    }   // findHitPointType
1233
1234    /*
1235    * Modify coordinates methods
1236     */
1237    /**
1238     * {@inheritDoc}
1239     */
1240    @Override
1241    public void setCoordsCenter(@Nonnull Point2D p) {
1242        Point2D offset = MathUtil.subtract(p, getCoordsCenter());
1243        pointA = MathUtil.add(pointA, offset);
1244        pointB = MathUtil.add(pointB, offset);
1245        pointC = MathUtil.add(pointC, offset);
1246        pointD = MathUtil.add(pointD, offset);
1247        super.setCoordsCenter(p);
1248    }
1249
1250    // temporary should be private once LayoutTurnout no longer needs it
1251    void reCalculateCenter() {
1252        super.setCoordsCenter(MathUtil.midPoint(pointA, pointC));
1253    }
1254
1255    public void setCoordsA(@Nonnull Point2D p) {
1256        pointA = p;
1257        if (version == 2) {
1258            reCalculateCenter();
1259        }
1260        double x = getCoordsCenter().getX() - p.getX();
1261        double y = getCoordsCenter().getY() - p.getY();
1262        if (getTurnoutType() == TurnoutType.DOUBLE_XOVER) {
1263            dispA = new Point2D.Double(x, y);
1264            // adjust to maintain rectangle
1265            double oldLength = MathUtil.length(dispB);
1266            double newLength = Math.hypot(x, y);
1267            dispB = MathUtil.multiply(dispB, newLength / oldLength);
1268        } else if ((getTurnoutType() == TurnoutType.RH_XOVER)
1269                || (getTurnoutType() == TurnoutType.LH_XOVER)) {
1270            dispA = new Point2D.Double(x, y);
1271            // adjust to maintain the parallelogram
1272            double b = -y;
1273            double xi = 0.0;
1274            double yi = b;
1275            if ((dispB.getX() + x) != 0.0) {
1276                double a = (dispB.getY() + y) / (dispB.getX() + x);
1277                b = -y + (a * x);
1278                xi = -b / (a + (1.0 / a));
1279                yi = (a * xi) + b;
1280            }
1281            if (getTurnoutType() == TurnoutType.RH_XOVER) {
1282                x = xi - (0.333333 * (-x - xi));
1283                y = yi - (0.333333 * (-y - yi));
1284            } else if (getTurnoutType() == TurnoutType.LH_XOVER) {
1285                x = xi - (3.0 * (-x - xi));
1286                y = yi - (3.0 * (-y - yi));
1287            }
1288            dispB = new Point2D.Double(x, y);
1289        } else if (getTurnoutType() == TurnoutType.WYE_TURNOUT) {
1290            // modify both to maintain same angle at wye
1291            double temX = (dispB.getX() + dispA.getX());
1292            double temY = (dispB.getY() + dispA.getY());
1293            double temXx = (dispB.getX() - dispA.getX());
1294            double temYy = (dispB.getY() - dispA.getY());
1295            double tan = Math.sqrt(((temX * temX) + (temY * temY))
1296                    / ((temXx * temXx) + (temYy * temYy)));
1297            double xx = x + (y / tan);
1298            double yy = y - (x / tan);
1299            dispA = new Point2D.Double(xx, yy);
1300            xx = x - (y / tan);
1301            yy = y + (x / tan);
1302            dispB = new Point2D.Double(xx, yy);
1303        } else {
1304            dispB = new Point2D.Double(x, y);
1305        }
1306    }
1307
1308    public void setCoordsB(Point2D p) {
1309        pointB = p;
1310        double x = getCoordsCenter().getX() - p.getX();
1311        double y = getCoordsCenter().getY() - p.getY();
1312        dispB = new Point2D.Double(-x, -y);
1313        if ((getTurnoutType() == TurnoutType.DOUBLE_XOVER)
1314                || (getTurnoutType() == TurnoutType.WYE_TURNOUT)) {
1315            // adjust to maintain rectangle or wye shape
1316            double oldLength = MathUtil.length(dispA);
1317            double newLength = Math.hypot(x, y);
1318            dispA = MathUtil.multiply(dispA, newLength / oldLength);
1319        } else if ((getTurnoutType() == TurnoutType.RH_XOVER)
1320                || (getTurnoutType() == TurnoutType.LH_XOVER)) {
1321            // adjust to maintain the parallelogram
1322            double b = y;
1323            double xi = 0.0;
1324            double yi = b;
1325            if ((dispA.getX() - x) != 0.0) {
1326                if ((-dispA.getX() + x) == 0) {
1327                    /* we can in some situations eg 90' vertical end up with a 0 value,
1328                    so hence remove a small amount so that we
1329                    don't have a divide by zero issue */
1330                    x = x - 0.0000000001;
1331                }
1332                double a = (dispA.getY() - y) / (dispA.getX() - x);
1333                b = y - (a * x);
1334                xi = -b / (a + (1.0 / a));
1335                yi = (a * xi) + b;
1336            }
1337            if (getTurnoutType() == TurnoutType.LH_XOVER) {
1338                x = xi - (0.333333 * (x - xi));
1339                y = yi - (0.333333 * (y - yi));
1340            } else if (getTurnoutType() == TurnoutType.RH_XOVER) {
1341                x = xi - (3.0 * (x - xi));
1342                y = yi - (3.0 * (y - yi));
1343            }
1344            dispA = new Point2D.Double(x, y);
1345        }
1346    }
1347
1348    public void setCoordsC(Point2D p) {
1349        pointC = p;
1350        if (version == 2) {
1351            reCalculateCenter();
1352        }
1353        double x = getCoordsCenter().getX() - p.getX();
1354        double y = getCoordsCenter().getY() - p.getY();
1355        dispA = new Point2D.Double(-x, -y);
1356        if ((getTurnoutType() == TurnoutType.DOUBLE_XOVER)
1357                || (getTurnoutType() == TurnoutType.WYE_TURNOUT)) {
1358            // adjust to maintain rectangle or wye shape
1359            double oldLength = MathUtil.length(dispB);
1360            double newLength = Math.hypot(x, y);
1361            dispB = MathUtil.multiply(dispB, newLength / oldLength);
1362        } else if ((getTurnoutType() == TurnoutType.RH_XOVER)
1363                || (getTurnoutType() == TurnoutType.LH_XOVER)) {
1364            double b = -y;
1365            double xi = 0.0;
1366            double yi = b;
1367            if ((dispB.getX() + x) != 0.0) {
1368                if ((-dispB.getX() + x) == 0) {
1369                    /* we can in some situations eg 90' vertical end up with a 0 value,
1370                    so hence remove a small amount so that we
1371                    don't have a divide by zero issue */
1372
1373                    x = x - 0.0000000001;
1374                }
1375                double a = (-dispB.getY() + y) / (-dispB.getX() + x);
1376                b = -y + (a * x);
1377                xi = -b / (a + (1.0 / a));
1378                yi = (a * xi) + b;
1379            }
1380            if (getTurnoutType() == TurnoutType.RH_XOVER) {
1381                x = xi - (0.333333 * (-x - xi));
1382                y = yi - (0.333333 * (-y - yi));
1383            } else if (getTurnoutType() == TurnoutType.LH_XOVER) {
1384                x = xi - (3.0 * (-x - xi));
1385                y = yi - (3.0 * (-y - yi));
1386            }
1387            dispB = new Point2D.Double(-x, -y);
1388        }
1389    }
1390
1391    public void setCoordsD(Point2D p) {
1392        pointD = p;
1393
1394        // only used for crossovers
1395        double x = getCoordsCenter().getX() - p.getX();
1396        double y = getCoordsCenter().getY() - p.getY();
1397        dispB = new Point2D.Double(x, y);
1398        if (getTurnoutType() == TurnoutType.DOUBLE_XOVER) {
1399            // adjust to maintain rectangle
1400            double oldLength = MathUtil.length(dispA);
1401            double newLength = Math.hypot(x, y);
1402            dispA = MathUtil.multiply(dispA, newLength / oldLength);
1403        } else if ((getTurnoutType() == TurnoutType.RH_XOVER)
1404                || (getTurnoutType() == TurnoutType.LH_XOVER)) {
1405            // adjust to maintain the parallelogram
1406            double b = y;
1407            double xi = 0.0;
1408            double yi = b;
1409            if ((dispA.getX() + x) != 0.0) {
1410                double a = (dispA.getY() + y) / (dispA.getX() + x);
1411                b = -y + (a * x);
1412                xi = -b / (a + (1.0 / a));
1413                yi = (a * xi) + b;
1414            }
1415            if (getTurnoutType() == TurnoutType.LH_XOVER) {
1416                x = xi - (0.333333 * (-x - xi));
1417                y = yi - (0.333333 * (-y - yi));
1418            } else if (getTurnoutType() == TurnoutType.RH_XOVER) {
1419                x = xi - (3.0 * (-x - xi));
1420                y = yi - (3.0 * (-y - yi));
1421            }
1422            dispA = new Point2D.Double(x, y);
1423        }
1424    }
1425
1426    /**
1427     * {@inheritDoc}
1428     */
1429    @Override
1430    public void scaleCoords(double xFactor, double yFactor) {
1431        Point2D factor = new Point2D.Double(xFactor, yFactor);
1432        super.setCoordsCenter(MathUtil.granulize(MathUtil.multiply(getCoordsCenter(), factor), 1.0));
1433
1434        dispA = MathUtil.granulize(MathUtil.multiply(dispA, factor), 1.0);
1435        dispB = MathUtil.granulize(MathUtil.multiply(dispB, factor), 1.0);
1436
1437        pointA = MathUtil.granulize(MathUtil.multiply(pointA, factor), 1.0);
1438        pointB = MathUtil.granulize(MathUtil.multiply(pointB, factor), 1.0);
1439        pointC = MathUtil.granulize(MathUtil.multiply(pointC, factor), 1.0);
1440        pointD = MathUtil.granulize(MathUtil.multiply(pointD, factor), 1.0);
1441    }
1442
1443    /**
1444     * {@inheritDoc}
1445     */
1446    @Override
1447    public void translateCoords(double xFactor, double yFactor) {
1448        Point2D factor = new Point2D.Double(xFactor, yFactor);
1449        super.setCoordsCenter(MathUtil.add(getCoordsCenter(), factor));
1450        pointA = MathUtil.add(pointA, factor);
1451        pointB = MathUtil.add(pointB, factor);
1452        pointC = MathUtil.add(pointC, factor);
1453        pointD = MathUtil.add(pointD, factor);
1454    }
1455
1456    /**
1457     * {@inheritDoc}
1458     */
1459    @Override
1460    public void rotateCoords(double angleDEG) {
1461        // rotate coordinates
1462        double rotRAD = Math.toRadians(angleDEG);
1463        double sineRot = Math.sin(rotRAD);
1464        double cosineRot = Math.cos(rotRAD);
1465
1466        // rotate displacements around origin {0, 0}
1467        Point2D center_temp = getCoordsCenter();
1468        super.setCoordsCenter(MathUtil.zeroPoint2D);
1469        dispA = rotatePoint(dispA, sineRot, cosineRot);
1470        dispB = rotatePoint(dispB, sineRot, cosineRot);
1471        super.setCoordsCenter(center_temp);
1472
1473        pointA = rotatePoint(pointA, sineRot, cosineRot);
1474        pointB = rotatePoint(pointB, sineRot, cosineRot);
1475        pointC = rotatePoint(pointC, sineRot, cosineRot);
1476        pointD = rotatePoint(pointD, sineRot, cosineRot);
1477    }
1478
1479    public double getRotationDEG() {
1480        double result = 0;
1481        switch (getTurnoutType()) {
1482            case RH_TURNOUT:
1483            case LH_TURNOUT:
1484            case WYE_TURNOUT: {
1485                result = 90 - MathUtil.computeAngleDEG(getCoordsA(), getCoordsCenter());
1486                break;
1487            }
1488            case DOUBLE_XOVER:
1489            case RH_XOVER:
1490            case LH_XOVER: {
1491                result = 90 - MathUtil.computeAngleDEG(getCoordsA(), getCoordsB());
1492                break;
1493            }
1494            default: {
1495                break;
1496            }
1497        }
1498        return result;
1499    }
1500
1501    /**
1502     * Toggle turnout if clicked on, physical turnout exists, and not disabled.
1503     */
1504    public void toggleTurnout() {
1505        turnout.toggleTurnout();
1506    }
1507
1508    /**
1509     * Set the LayoutTurnout state. Used for sending the toggle command Checks
1510     * not disabled, disable when occupied Also sets secondary Turnout commanded
1511     * state
1512     *
1513     * @param state New state to set, eg Turnout.CLOSED
1514     */
1515    public void setState(int state) {
1516        turnout.setState(state);
1517    }
1518
1519    /**
1520     * Get the LayoutTurnout state
1521     * <p>
1522     * Ensures the secondary Turnout state matches the primary
1523     *
1524     * @return the state, eg Turnout.CLOSED or Turnout.INCONSISTENT
1525     */
1526    public int getState() {
1527        return turnout.getState();
1528    }
1529
1530    /**
1531     * Is this turnout occupied?
1532     *
1533     * @return true if occupied
1534     */
1535    private boolean isOccupied() {
1536        return turnout.isOccupied();
1537    }
1538
1539    // initialization instance variables (used when loading a LayoutEditor)
1540    public String connectAName = "";
1541    public String connectBName = "";
1542    public String connectCName = "";
1543    public String connectDName = "";
1544
1545    public String tBlockAName = "";
1546    public String tBlockBName = "";
1547    public String tBlockCName = "";
1548    public String tBlockDName = "";
1549
1550    private JPopupMenu popup = null;
1551
1552    /**
1553     * {@inheritDoc}
1554     */
1555    @Override
1556    @Nonnull
1557    protected JPopupMenu showPopup(@Nonnull JmriMouseEvent mouseEvent) {
1558        if (popup != null) {
1559            popup.removeAll();
1560        } else {
1561            popup = new JPopupMenu();
1562        }
1563
1564        if (layoutEditor.isEditable()) {
1565            String label = "";
1566            switch (getTurnoutType()) {
1567                case RH_TURNOUT:
1568                    label = Bundle.getMessage("RightTurnout");
1569                    break;
1570                case LH_TURNOUT:
1571                    label = Bundle.getMessage("LeftTurnout");
1572                    break;
1573                case WYE_TURNOUT:
1574                    label = Bundle.getMessage("WYETurnout");
1575                    break;
1576                case DOUBLE_XOVER:
1577                    label = Bundle.getMessage("DoubleCrossover");
1578                    break;
1579                case RH_XOVER:
1580                    label = Bundle.getMessage("RightCrossover");
1581                    break;
1582                case LH_XOVER:
1583                    label = Bundle.getMessage("LeftCrossover");
1584                    break;
1585                default:
1586                    break;
1587            }
1588            JMenuItem jmi = popup.add(Bundle.getMessage("MakeLabel", label) + getName());
1589            jmi.setEnabled(false);
1590
1591            if (getTurnout() == null) {
1592                jmi = popup.add(Bundle.getMessage("NoTurnout"));
1593            } else {
1594                String stateString = getTurnoutStateString(getTurnout().getKnownState());
1595                stateString = String.format(" (%s)", stateString);
1596                jmi = popup.add(Bundle.getMessage("BeanNameTurnout")
1597                        + ": " + getTurnoutName() + stateString);
1598            }
1599            jmi.setEnabled(false);
1600
1601            if (getSecondTurnout() != null) {
1602                String stateString = getTurnoutStateString(getSecondTurnout().getKnownState());
1603                stateString = String.format(" (%s)", stateString);
1604                jmi = popup.add(Bundle.getMessage("Supporting",
1605                        Bundle.getMessage("BeanNameTurnout"))
1606                        + ": " + getSecondTurnoutName() + stateString);
1607            }
1608            jmi.setEnabled(false);
1609
1610            if (getBlockName().isEmpty()) {
1611                jmi = popup.add(Bundle.getMessage("NoBlock"));
1612                jmi.setEnabled(false);
1613            } else {
1614                jmi = popup.add(Bundle.getMessage("MakeLabel", Bundle.getMessage("BeanNameBlock")) + getLayoutBlock().getDisplayName());
1615                jmi.setEnabled(false);
1616                if (isTurnoutTypeXover()) {
1617                    // check if extra blocks have been entered
1618                    if ((getLayoutBlockB() != null) && (getLayoutBlockB() != getLayoutBlock())) {
1619                        jmi = popup.add(Bundle.getMessage("MakeLabel", Bundle.getMessage("Block_ID", "B")) + getLayoutBlockB().getDisplayName());
1620                        jmi.setEnabled(false);
1621                    }
1622                    if ((getLayoutBlockC() != null) && (getLayoutBlockC() != getLayoutBlock())) {
1623                        jmi = popup.add(Bundle.getMessage("MakeLabel", Bundle.getMessage("Block_ID", "C")) + getLayoutBlockC().getDisplayName());
1624                        jmi.setEnabled(false);
1625                    }
1626                    if ((getLayoutBlockD() != null) && (getLayoutBlockD() != getLayoutBlock())) {
1627                        jmi = popup.add(Bundle.getMessage("MakeLabel", Bundle.getMessage("Block_ID", "D")) + getLayoutBlockD().getDisplayName());
1628                        jmi.setEnabled(false);
1629                    }
1630                }
1631            }
1632
1633            // if there are any track connections
1634            if ((getConnectA() != null) || (getConnectB() != null)
1635                    || (getConnectC() != null) || (getConnectD() != null)) {
1636                JMenu connectionsMenu = new JMenu(Bundle.getMessage("Connections")); // there is no pane opening (which is what ... implies)
1637                if (getConnectA() != null) {
1638                    connectionsMenu.add(new AbstractAction(Bundle.getMessage("MakeLabel", "A") + getConnectA().getName()) {
1639                        @Override
1640                        public void actionPerformed(ActionEvent e) {
1641                            LayoutEditorFindItems lf = layoutEditor.getFinder();
1642                            LayoutTrack lt = lf.findObjectByName(getConnectA().getName());
1643                            // this shouldn't ever be null... however...
1644                            if (lt != null) {
1645                                LayoutTrackView ltv = layoutEditor.getLayoutTrackView(lt);
1646                                layoutEditor.setSelectionRect(ltv.getBounds());
1647                                ltv.showPopup();
1648                            }
1649                        }
1650                    });
1651                }
1652                if (getConnectB() != null) {
1653                    connectionsMenu.add(new AbstractAction(Bundle.getMessage("MakeLabel", "B") + getConnectB().getName()) {
1654                        @Override
1655                        public void actionPerformed(ActionEvent e) {
1656                            LayoutEditorFindItems lf = layoutEditor.getFinder();
1657                            LayoutTrack lt = lf.findObjectByName(getConnectB().getName());
1658                            // this shouldn't ever be null... however...
1659                            if (lt != null) {
1660                                LayoutTrackView ltv = layoutEditor.getLayoutTrackView(lt);
1661                                layoutEditor.setSelectionRect(ltv.getBounds());
1662                                ltv.showPopup();
1663                            }
1664                        }
1665                    });
1666                }
1667                if (getConnectC() != null) {
1668                    connectionsMenu.add(new AbstractAction(Bundle.getMessage("MakeLabel", "C") + getConnectC().getName()) {
1669                        @Override
1670                        public void actionPerformed(ActionEvent e) {
1671                            LayoutEditorFindItems lf = layoutEditor.getFinder();
1672                            LayoutTrack lt = lf.findObjectByName(getConnectC().getName());
1673                            // this shouldn't ever be null... however...
1674                            if (lt != null) {
1675                                LayoutTrackView ltv = layoutEditor.getLayoutTrackView(lt);
1676                                layoutEditor.setSelectionRect(ltv.getBounds());
1677                                ltv.showPopup();
1678                            }
1679                        }
1680                    });
1681                }
1682                if (getConnectD() != null) {
1683                    connectionsMenu.add(new AbstractAction(Bundle.getMessage("MakeLabel", "D") + getConnectD().getName()) {
1684                        @Override
1685                        public void actionPerformed(ActionEvent e) {
1686                            LayoutEditorFindItems lf = layoutEditor.getFinder();
1687                            LayoutTrack lt = lf.findObjectByName(getConnectD().getName());
1688                            // this shouldn't ever be null... however...
1689                            if (lt != null) {
1690                                LayoutTrackView ltv = layoutEditor.getLayoutTrackView(lt);
1691                                layoutEditor.setSelectionRect(ltv.getBounds());
1692                                ltv.showPopup();
1693                            }
1694                        }
1695                    });
1696                }
1697                popup.add(connectionsMenu);
1698            }
1699            popup.add(new JSeparator(JSeparator.HORIZONTAL));
1700
1701            JCheckBoxMenuItem hiddenCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("Hidden"));
1702            hiddenCheckBoxMenuItem.setSelected(isHidden());
1703            popup.add(hiddenCheckBoxMenuItem);
1704            hiddenCheckBoxMenuItem.addActionListener( e1 -> {
1705                JCheckBoxMenuItem o = (JCheckBoxMenuItem) e1.getSource();
1706                setHidden(o.isSelected());
1707            });
1708
1709            JCheckBoxMenuItem showUnknownCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("ShowUnknown"));
1710            showUnknownCheckBoxMenuItem.setSelected(getShowUnknown());
1711            popup.add(showUnknownCheckBoxMenuItem);
1712            showUnknownCheckBoxMenuItem.addActionListener( e1 -> {
1713                JCheckBoxMenuItem o = (JCheckBoxMenuItem) e1.getSource();
1714                setShowUnknown(o.isSelected());
1715            });
1716            
1717            JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(Bundle.getMessage("Disabled"));
1718            cbmi.setSelected(isDisabled());
1719            popup.add(cbmi);
1720            cbmi.addActionListener( e2 -> {
1721                JCheckBoxMenuItem o = (JCheckBoxMenuItem) e2.getSource();
1722                setDisabled(o.isSelected());
1723            });
1724
1725            cbmi = new JCheckBoxMenuItem(Bundle.getMessage("DisabledWhenOccupied"));
1726            if (getTurnout() == null || getBlockName().isEmpty()) {
1727                cbmi.setEnabled(false);
1728            }
1729            cbmi.setSelected(isDisabledWhenOccupied());
1730            popup.add(cbmi);
1731            cbmi.addActionListener( e3 -> {
1732                JCheckBoxMenuItem o = (JCheckBoxMenuItem) e3.getSource();
1733                setDisableWhenOccupied(o.isSelected());
1734            });
1735
1736            // Rotate if there are no track connections
1737//            if ((getConnectA() == null) && (getConnectB() == null)
1738//                    && (getConnectC() == null)
1739//                    && (getConnectD() == null))
1740
1741            JMenuItem rotateItem = new JMenuItem(Bundle.getMessage("Rotate_",
1742                    String.format(Locale.getDefault(), "%.1f", getRotationDEG())) + "...");
1743            popup.add(rotateItem);
1744            rotateItem.addActionListener( event -> displayRotationDialog());
1745
1746            popup.add(new AbstractAction(Bundle.getMessage("UseSizeAsDefault")) {
1747                @Override
1748                public void actionPerformed(ActionEvent e) {
1749                    setUpDefaultSize();
1750                }
1751            });
1752
1753            popup.add(new AbstractAction(Bundle.getMessage("ButtonEdit")) {
1754                @Override
1755                public void actionPerformed(ActionEvent e) {
1756                    editor.editLayoutTrack(LayoutTurnoutView.this);
1757                }
1758            });
1759            popup.add(new AbstractAction(Bundle.getMessage("ButtonDelete")) {
1760                @Override
1761                public void actionPerformed(ActionEvent e) {
1762                    if (canRemove() && removeInlineLogixNG()
1763                            && layoutEditor.removeLayoutTurnout(turnout)) {
1764                        // Returned true if user did not cancel
1765                        remove();
1766                        dispose();
1767                    }
1768                }
1769            });
1770
1771            if (getTurnout() != null) {
1772                AbstractAction ssaa = new AbstractAction(Bundle.getMessage("SetSignals")) {
1773                    @Override
1774                    public void actionPerformed(ActionEvent e) {
1775                        LayoutEditorTools tools = layoutEditor.getLETools();
1776                        LayoutEditorToolBarPanel letbp = getLayoutEditorToolBarPanel();
1777                        if (isTurnoutTypeXover()) {
1778                            tools.setSignalsAtXoverTurnoutFromMenu(turnout,
1779                                    letbp.signalIconEditor, letbp.signalFrame);
1780                        } else if (getLinkType() == LinkType.NO_LINK) {
1781                            tools.setSignalsAtTurnoutFromMenu(turnout,
1782                                    letbp.signalIconEditor, letbp.signalFrame);
1783                        } else if (getLinkType() == LinkType.THROAT_TO_THROAT) {
1784                            tools.setSignalsAtThroatToThroatTurnoutsFromMenu(turnout, getLinkedTurnoutName(),
1785                                    letbp.signalIconEditor, letbp.signalFrame);
1786                        } else if (getLinkType() == LinkType.FIRST_3_WAY) {
1787                            tools.setSignalsAt3WayTurnoutFromMenu(getTurnoutName(), getLinkedTurnoutName(),
1788                                    letbp.signalIconEditor, letbp.signalFrame);
1789                        } else if (getLinkType() == LinkType.SECOND_3_WAY) {
1790                            tools.setSignalsAt3WayTurnoutFromMenu(getLinkedTurnoutName(), getTurnoutName(),
1791                                    letbp.signalIconEditor, letbp.signalFrame);
1792                        }
1793                    }
1794                };
1795
1796                JMenu jm = new JMenu(Bundle.getMessage("SignalHeads"));
1797                if (layoutEditor.getLETools().addLayoutTurnoutSignalHeadInfoToMenu(
1798                        getTurnoutName(), getLinkedTurnoutName(), jm)) {
1799                    jm.add(ssaa);
1800                    popup.add(jm);
1801                } else {
1802                    popup.add(ssaa);
1803                }
1804            }
1805            if (!getBlockName().isEmpty()) {
1806                final String[] boundaryBetween = getBlockBoundaries();
1807                boolean blockBoundaries = false;
1808                for (int i = 0; i < 4; i++) {
1809                    if (boundaryBetween[i] != null) {
1810                        blockBoundaries = true;
1811
1812                    }
1813                }
1814
1815                if (blockBoundaries) {
1816                    popup.add(new AbstractAction(Bundle.getMessage("SetSignalMasts")) {
1817                        @Override
1818                        public void actionPerformed(ActionEvent e) {
1819                            layoutEditor.getLETools().setSignalMastsAtTurnoutFromMenu(turnout,
1820                                    boundaryBetween);
1821                        }
1822                    });
1823                    popup.add(new AbstractAction(Bundle.getMessage("SetSensors")) {
1824                        @Override
1825                        public void actionPerformed(ActionEvent e) {
1826                            LayoutEditorToolBarPanel letbp = getLayoutEditorToolBarPanel();
1827                            layoutEditor.getLETools().setSensorsAtTurnoutFromMenu(
1828                                    turnout,
1829                                    boundaryBetween,
1830                                    letbp.sensorIconEditor,
1831                                    letbp.sensorFrame);
1832                        }
1833                    });
1834
1835                }
1836
1837                if (InstanceManager.getDefault(LayoutBlockManager.class
1838                ).isAdvancedRoutingEnabled()) {
1839                    Map<String, LayoutBlock> map = new HashMap<>();
1840                    if (!getBlockName().isEmpty()) {
1841                        map.put(getBlockName(), getLayoutBlock());
1842                    }
1843                    if (!getBlockBName().isEmpty()) {
1844                        map.put(getBlockBName(), getLayoutBlockB());
1845                    }
1846                    if (!getBlockCName().isEmpty()) {
1847                        map.put(getBlockCName(), getLayoutBlockC());
1848                    }
1849                    if (!getBlockDName().isEmpty()) {
1850                        map.put(getBlockDName(), getLayoutBlockD());
1851                    }
1852                    if (blockBoundaries) {
1853                        if (map.size() == 1) {
1854                            popup.add(new AbstractAction(Bundle.getMessage("ViewBlockRouting")) {
1855                                @Override
1856                                public void actionPerformed(ActionEvent e) {
1857                                    AbstractAction routeTableAction = new LayoutBlockRouteTableAction("ViewRouting", getLayoutBlock());
1858                                    routeTableAction.actionPerformed(e);
1859                                }
1860                            });
1861                        } else if (map.size() > 1) {
1862                            JMenu viewRouting = new JMenu(Bundle.getMessage("ViewBlockRouting"));
1863                            for (Map.Entry<String, LayoutBlock> entry : map.entrySet()) {
1864                                String blockName = entry.getKey();
1865                                LayoutBlock layoutBlock = entry.getValue();
1866                                viewRouting.add(new AbstractActionImpl(blockName, getBlockBName(), layoutBlock));
1867                            }
1868                            popup.add(viewRouting);
1869                        }
1870                    }   // if (blockBoundaries)
1871                }   // .isAdvancedRoutingEnabled()
1872            }   // getBlockName().isEmpty()
1873            setAdditionalEditPopUpMenu(popup);
1874            layoutEditor.setShowAlignmentMenu(popup);
1875            addCommonPopupItems(mouseEvent, popup);
1876            popup.show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
1877        } else if (!viewAdditionalMenu.isEmpty()) {
1878            setAdditionalViewPopUpMenu(popup);
1879            addCommonPopupItems(mouseEvent, popup);
1880            popup.show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
1881        }
1882        return popup;
1883    } // showPopup
1884
1885    private void displayRotationDialog(){
1886        boolean entering = true;
1887        while (entering) {
1888            // prompt for rotation angle
1889            String newAngle = JmriJOptionPane.showInputDialog(layoutEditor,
1890                    Bundle.getMessage("MakeLabel", Bundle.getMessage("EnterRotation")),"");
1891            if (newAngle==null || newAngle.isEmpty()) {
1892                return; // cancelled
1893            }
1894            try {
1895                double rot = IntlUtilities.doubleValue(newAngle);
1896                entering = false;
1897                if ( Double.compare(rot, 0.0d) != 0 ) { // i.e. rot != 0
1898                    rotateCoords(rot);
1899                    layoutEditor.redrawPanel();
1900                }
1901            } catch (ParseException e1) {
1902                JmriJOptionPane.showMessageDialog(layoutEditor, Bundle.getMessage("Error3")
1903                    + " " + e1, Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
1904            }
1905        }
1906    }
1907
1908    public String[] getBlockBoundaries() {
1909        return turnout.getBlockBoundaries();
1910    }
1911
1912    public ArrayList<LayoutBlock> getProtectedBlocks(jmri.NamedBean bean) {
1913        return turnout.getProtectedBlocks(bean);
1914    }
1915
1916    protected void removeSML(SignalMast signalMast) {
1917        turnout.removeSML(signalMast);
1918    }
1919
1920    /**
1921     * Clean up when this object is no longer needed. Should not be called while
1922     * the object is still displayed; see {@link #remove()}
1923     */
1924    public void dispose() {
1925        if (popup != null) {
1926            popup.removeAll();
1927        }
1928        popup = null;
1929    }
1930
1931    /**
1932     * Remove this object from display and persistance.
1933     */
1934    public void remove() {
1935        turnout.remove();
1936    }
1937
1938    /**
1939     * "active" means that the object is still displayed, and should be stored.
1940     *
1941     * @return true if active
1942     */
1943    public boolean isActive() {
1944        return turnout.isActive();
1945    }
1946
1947    ArrayList<JMenuItem> editAdditionalMenu = new ArrayList<>(0);
1948    ArrayList<JMenuItem> viewAdditionalMenu = new ArrayList<>(0);
1949
1950    public void addEditPopUpMenu(JMenuItem menu) {
1951        if (!editAdditionalMenu.contains(menu)) {
1952            editAdditionalMenu.add(menu);
1953        }
1954    }
1955
1956    public void addViewPopUpMenu(JMenuItem menu) {
1957        if (!viewAdditionalMenu.contains(menu)) {
1958            viewAdditionalMenu.add(menu);
1959        }
1960    }
1961
1962    public void setAdditionalEditPopUpMenu(JPopupMenu popup) {
1963        if (editAdditionalMenu.isEmpty()) {
1964            return;
1965        }
1966        popup.addSeparator();
1967        for (JMenuItem mi : editAdditionalMenu) {
1968            popup.add(mi);
1969        }
1970    }
1971
1972    public void setAdditionalViewPopUpMenu(JPopupMenu popup) {
1973        if (viewAdditionalMenu.isEmpty()) {
1974            return;
1975        }
1976        popup.addSeparator();
1977        for (JMenuItem mi : viewAdditionalMenu) {
1978            popup.add(mi);
1979        }
1980    }
1981
1982    /**
1983     * Draw track decorations.
1984     * <p>
1985     * This type of track has none, so this method is empty.
1986     */
1987    @Override
1988    protected void drawDecorations(Graphics2D g2) {
1989    }
1990
1991    /**
1992     * {@inheritDoc}
1993     */
1994    @Override
1995    protected void draw1(Graphics2D g2, boolean isMain, boolean isBlock) {
1996        if (isBlock && getLayoutBlock() == null) {
1997            // Skip the block layer if there is no block assigned.
1998            return;
1999        }
2000
2001        Point2D pA = getCoordsA();
2002        Point2D pB = getCoordsB();
2003        Point2D pC = getCoordsC();
2004        Point2D pD = getCoordsD();
2005
2006        boolean mainlineA = isMainlineA();
2007        boolean mainlineB = isMainlineB();
2008        boolean mainlineC = isMainlineC();
2009        boolean mainlineD = isMainlineD();
2010
2011        boolean drawUnselectedLeg = layoutEditor.isTurnoutDrawUnselectedLeg();
2012
2013        Color color = g2.getColor();
2014
2015        // if this isn't a block line all these will be the same color
2016        Color colorA = color;
2017        Color colorB = color;
2018        Color colorC = color;
2019        Color colorD = color;
2020
2021        if (isBlock) {
2022            LayoutBlock lb = getLayoutBlock();
2023            colorA = (lb == null) ? color : lb.getBlockColor();
2024            lb = getLayoutBlockB();
2025            colorB = (lb == null) ? color : lb.getBlockColor();
2026            lb = getLayoutBlockC();
2027            colorC = (lb == null) ? color : lb.getBlockColor();
2028            lb = getLayoutBlockD();
2029            colorD = (lb == null) ? color : lb.getBlockColor();
2030        }
2031
2032        // middles
2033        Point2D pM = getCoordsCenter();
2034        Point2D pABM = MathUtil.midPoint(pA, pB);
2035        Point2D pAM = MathUtil.lerp(pA, pABM, 5.0 / 8.0);
2036        Point2D pAMP = MathUtil.midPoint(pAM, pABM);
2037        Point2D pBM = MathUtil.lerp(pB, pABM, 5.0 / 8.0);
2038        Point2D pBMP = MathUtil.midPoint(pBM, pABM);
2039
2040        Point2D pCDM = MathUtil.midPoint(pC, pD);
2041        Point2D pCM = MathUtil.lerp(pC, pCDM, 5.0 / 8.0);
2042        Point2D pCMP = MathUtil.midPoint(pCM, pCDM);
2043        Point2D pDM = MathUtil.lerp(pD, pCDM, 5.0 / 8.0);
2044        Point2D pDMP = MathUtil.midPoint(pDM, pCDM);
2045
2046        Point2D pAF = MathUtil.midPoint(pAM, pM);
2047        Point2D pBF = MathUtil.midPoint(pBM, pM);
2048        Point2D pCF = MathUtil.midPoint(pCM, pM);
2049        Point2D pDF = MathUtil.midPoint(pDM, pM);
2050
2051        int state = UNKNOWN;
2052        if (layoutEditor.isAnimating()) {
2053            state = getState();
2054        }
2055
2056        TurnoutType type = getTurnoutType();
2057
2058        if (type == TurnoutType.DOUBLE_XOVER) {
2059            if (state != Turnout.THROWN && state != INCONSISTENT) { // unknown or continuing path - not crossed over
2060                if (isMain == mainlineA) {
2061                    g2.setColor(colorA);
2062                    g2.draw(new Line2D.Double(pA, pABM));
2063                    if (!isBlock || drawUnselectedLeg) {
2064                        g2.draw(new Line2D.Double(pAF, pM));
2065                    }
2066                }
2067                if (isMain == mainlineB) {
2068                    g2.setColor(colorB);
2069                    g2.draw(new Line2D.Double(pB, pABM));
2070                    if (!isBlock || drawUnselectedLeg) {
2071                        g2.draw(new Line2D.Double(pBF, pM));
2072                    }
2073                }
2074                if (isMain == mainlineC) {
2075                    g2.setColor(colorC);
2076                    g2.draw(new Line2D.Double(pC, pCDM));
2077                    if (!isBlock || drawUnselectedLeg) {
2078                        g2.draw(new Line2D.Double(pCF, pM));
2079                    }
2080                }
2081                if (isMain == mainlineD) {
2082                    g2.setColor(colorD);
2083                    g2.draw(new Line2D.Double(pD, pCDM));
2084                    if (!isBlock || drawUnselectedLeg) {
2085                        g2.draw(new Line2D.Double(pDF, pM));
2086                    }
2087                }
2088            }
2089            if (state != Turnout.CLOSED && state != INCONSISTENT) { // unknown or diverting path - crossed over
2090                if (isMain == mainlineA) {
2091                    g2.setColor(colorA);
2092                    g2.draw(new Line2D.Double(pA, pAM));
2093                    g2.draw(new Line2D.Double(pAM, pM));
2094                    if (!isBlock || drawUnselectedLeg) {
2095                        g2.draw(new Line2D.Double(pAMP, pABM));
2096                    }
2097                }
2098                if (isMain == mainlineB) {
2099                    g2.setColor(colorB);
2100                    g2.draw(new Line2D.Double(pB, pBM));
2101                    g2.draw(new Line2D.Double(pBM, pM));
2102                    if (!isBlock || drawUnselectedLeg) {
2103                        g2.draw(new Line2D.Double(pBMP, pABM));
2104                    }
2105                }
2106                if (isMain == mainlineC) {
2107                    g2.setColor(colorC);
2108                    g2.draw(new Line2D.Double(pC, pCM));
2109                    g2.draw(new Line2D.Double(pCM, pM));
2110                    if (!isBlock || drawUnselectedLeg) {
2111                        g2.draw(new Line2D.Double(pCMP, pCDM));
2112                    }
2113                }
2114                if (isMain == mainlineD) {
2115                    g2.setColor(colorD);
2116                    g2.draw(new Line2D.Double(pD, pDM));
2117                    g2.draw(new Line2D.Double(pDM, pM));
2118                    if (!isBlock || drawUnselectedLeg) {
2119                        g2.draw(new Line2D.Double(pDMP, pCDM));
2120                    }
2121                }
2122            }
2123            if (state == INCONSISTENT) {
2124                if (isMain == mainlineA) {
2125                    g2.setColor(colorA);
2126                    g2.draw(new Line2D.Double(pA, pAM));
2127                }
2128                if (isMain == mainlineB) {
2129                    g2.setColor(colorB);
2130                    g2.draw(new Line2D.Double(pB, pBM));
2131                }
2132                if (isMain == mainlineC) {
2133                    g2.setColor(colorC);
2134                    g2.draw(new Line2D.Double(pC, pCM));
2135                }
2136                if (isMain == mainlineD) {
2137                    g2.setColor(colorD);
2138                    g2.draw(new Line2D.Double(pD, pDM));
2139                }
2140                if (!isBlock || drawUnselectedLeg) {
2141                    if (isMain == mainlineA) {
2142                        g2.setColor(colorA);
2143                        g2.draw(new Line2D.Double(pAF, pM));
2144                    }
2145                    if (isMain == mainlineC) {
2146                        g2.setColor(colorC);
2147                        g2.draw(new Line2D.Double(pCF, pM));
2148                    }
2149                    if (isMain == mainlineB) {
2150                        g2.setColor(colorB);
2151                        g2.draw(new Line2D.Double(pBF, pM));
2152                    }
2153                    if (isMain == mainlineD) {
2154                        g2.setColor(colorD);
2155                        g2.draw(new Line2D.Double(pDF, pM));
2156                    }
2157                }
2158            }
2159        } else if ((type == TurnoutType.RH_XOVER)
2160                || (type == TurnoutType.LH_XOVER)) {    // draw (rh & lh) cross overs
2161            pAF = MathUtil.midPoint(pABM, pM);
2162            pBF = MathUtil.midPoint(pABM, pM);
2163            pCF = MathUtil.midPoint(pCDM, pM);
2164            pDF = MathUtil.midPoint(pCDM, pM);
2165            if (state != Turnout.THROWN && state != INCONSISTENT) { // unknown or continuing path - not crossed over
2166                if (isMain == mainlineA) {
2167                    g2.setColor(colorA);
2168                    g2.draw(new Line2D.Double(pA, pABM));
2169                }
2170                if (isMain == mainlineB) {
2171                    g2.setColor(colorB);
2172                    g2.draw(new Line2D.Double(pABM, pB));
2173                }
2174                if (isMain == mainlineC) {
2175                    g2.setColor(colorC);
2176                    g2.draw(new Line2D.Double(pC, pCDM));
2177                }
2178                if (isMain == mainlineD) {
2179                    g2.setColor(colorD);
2180                    g2.draw(new Line2D.Double(pCDM, pD));
2181                }
2182                if (!isBlock || drawUnselectedLeg) {
2183                    if (getTurnoutType() == TurnoutType.RH_XOVER) {
2184                        if (isMain == mainlineA) {
2185                            g2.setColor(colorA);
2186                            g2.draw(new Line2D.Double(pAF, pM));
2187                        }
2188                        if (isMain == mainlineC) {
2189                            g2.setColor(colorC);
2190                            g2.draw(new Line2D.Double(pCF, pM));
2191                        }
2192                    } else if (getTurnoutType() == TurnoutType.LH_XOVER) {
2193                        if (isMain == mainlineB) {
2194                            g2.setColor(colorB);
2195                            g2.draw(new Line2D.Double(pBF, pM));
2196                        }
2197                        if (isMain == mainlineD) {
2198                            g2.setColor(colorD);
2199                            g2.draw(new Line2D.Double(pDF, pM));
2200                        }
2201                    }
2202                }
2203            }
2204            if (state != Turnout.CLOSED && state != INCONSISTENT) { // unknown or diverting path - crossed over
2205                if (getTurnoutType() == TurnoutType.RH_XOVER) {
2206                    if (isMain == mainlineA) {
2207                        g2.setColor(colorA);
2208                        g2.draw(new Line2D.Double(pA, pABM));
2209                        g2.draw(new Line2D.Double(pABM, pM));
2210                    }
2211                    if (!isBlock || drawUnselectedLeg) {
2212                        if (isMain == mainlineB) {
2213                            g2.setColor(colorB);
2214                            g2.draw(new Line2D.Double(pBM, pB));
2215                        }
2216                    }
2217                    if (isMain == mainlineC) {
2218                        g2.setColor(colorC);
2219                        g2.draw(new Line2D.Double(pC, pCDM));
2220                        g2.draw(new Line2D.Double(pCDM, pM));
2221                    }
2222                    if (!isBlock || drawUnselectedLeg) {
2223                        if (isMain == mainlineD) {
2224                            g2.setColor(colorD);
2225                            g2.draw(new Line2D.Double(pDM, pD));
2226                        }
2227                    }
2228                } else if (getTurnoutType() == TurnoutType.LH_XOVER) {
2229                    if (!isBlock || drawUnselectedLeg) {
2230                        if (isMain == mainlineA) {
2231                            g2.setColor(colorA);
2232                            g2.draw(new Line2D.Double(pA, pAM));
2233                        }
2234                    }
2235                    if (isMain == mainlineB) {
2236                        g2.setColor(colorB);
2237                        g2.draw(new Line2D.Double(pB, pABM));
2238                        g2.draw(new Line2D.Double(pABM, pM));
2239                    }
2240                    if (!isBlock || drawUnselectedLeg) {
2241                        if (isMain == mainlineC) {
2242                            g2.setColor(colorC);
2243                            g2.draw(new Line2D.Double(pC, pCM));
2244                        }
2245                    }
2246                    if (isMain == mainlineD) {
2247                        g2.setColor(colorD);
2248                        g2.draw(new Line2D.Double(pD, pCDM));
2249                        g2.draw(new Line2D.Double(pCDM, pM));
2250                    }
2251                }
2252            }
2253            if (state == INCONSISTENT) {
2254                if (isMain == mainlineA) {
2255                    g2.setColor(colorA);
2256                    g2.draw(new Line2D.Double(pA, pAM));
2257                }
2258                if (isMain == mainlineB) {
2259                    g2.setColor(colorB);
2260                    g2.draw(new Line2D.Double(pB, pBM));
2261                }
2262                if (isMain == mainlineC) {
2263                    g2.setColor(colorC);
2264                    g2.draw(new Line2D.Double(pC, pCM));
2265                }
2266                if (isMain == mainlineD) {
2267                    g2.setColor(colorD);
2268                    g2.draw(new Line2D.Double(pD, pDM));
2269                }
2270                if (!isBlock || drawUnselectedLeg) {
2271                    if (getTurnoutType() == TurnoutType.RH_XOVER) {
2272                        if (isMain == mainlineA) {
2273                            g2.setColor(colorA);
2274                            g2.draw(new Line2D.Double(pAF, pM));
2275                        }
2276                        if (isMain == mainlineC) {
2277                            g2.setColor(colorC);
2278                            g2.draw(new Line2D.Double(pCF, pM));
2279                        }
2280                    } else if (getTurnoutType() == TurnoutType.LH_XOVER) {
2281                        if (isMain == mainlineB) {
2282                            g2.setColor(colorB);
2283                            g2.draw(new Line2D.Double(pBF, pM));
2284                        }
2285                        if (isMain == mainlineD) {
2286                            g2.setColor(colorD);
2287                            g2.draw(new Line2D.Double(pDF, pM));
2288                        }
2289                    }
2290                }
2291            }
2292        } else if (isTurnoutTypeSlip()) {
2293            log.error("{}.draw1(...); slips should be being drawn by LayoutSlip sub-class", getName());
2294        } else {    // LH, RH, or WYE Turnouts
2295                            
2296            // draw A<===>center
2297            if (isMain == mainlineA) {
2298                g2.setColor(colorA);
2299                g2.draw(new Line2D.Double(pA, pM));
2300            }
2301
2302            if (state == UNKNOWN || (getContinuingSense() == state && state != INCONSISTENT)) { // unknown or continuing path
2303                // draw center<===>B
2304                if (isMain == mainlineB) {
2305                    g2.setColor(colorB);
2306                    g2.draw(new Line2D.Double(pM, pB));
2307                }
2308            } else if (!isBlock || drawUnselectedLeg) {
2309                // draw center<--=>B
2310                if (isMain == mainlineB) {
2311                    g2.setColor(colorB);
2312                    g2.draw(new Line2D.Double(MathUtil.twoThirdsPoint(pM, pB), pB));
2313                }
2314            }
2315
2316            if (state == UNKNOWN || (getContinuingSense() != state && state != INCONSISTENT)) { // unknown or diverting path
2317                // draw center<===>C
2318                if (isMain == mainlineC) {
2319                    g2.setColor(colorC);
2320                    g2.draw(new Line2D.Double(pM, pC));
2321                }
2322            } else if (!isBlock || drawUnselectedLeg) {
2323                // draw center<--=>C
2324                if (isMain == mainlineC) {
2325                    g2.setColor(colorC);
2326                    g2.draw(new Line2D.Double(MathUtil.twoThirdsPoint(pM, pC), pC));
2327                }
2328            }
2329        }
2330
2331        // Overwrite with "?" if UNKNOWN and showUnknown requesting
2332        if (showUnknown && state == UNKNOWN) {
2333            // Draw the circle over the track drawing
2334            //Color previousColor = g2.getColor();
2335            //g2.setColor(g2.getBackground());
2336            g2.fill(trackControlCircleAt(getCoordsCenter()));
2337            //g2.setColor(previousColor);
2338
2339            drawForShowUnknown(g2, pM, g2.getColor(), true);
2340            return;
2341        }    
2342    }   // draw1
2343
2344    /** 
2345     * Draw a "?" for the UNKNOWN state.
2346     *
2347     * To be invoked if getShowUnknown() is true
2348     * 
2349     * @param g2 the graphics context to draw in
2350     * @param center where to center the "?"
2351     * @param color the base color for drawing
2352     * @param complement true means select black or white to complement the base color
2353     */
2354    private void drawForShowUnknown(Graphics2D g2, Point2D center, Color color, boolean complement) {
2355        var originalFont = g2.getFont();
2356        var originalColor = g2.getColor();
2357                
2358        // convert color to HSV to get intensity
2359        int v = Math.max(Math.max(color.getBlue(), color.getGreen()), color.getRed());
2360        
2361        Color drawColor = color; 
2362        
2363        if (complement) {
2364            drawColor = Color.BLACK;
2365            if ( v < 255*0.5) { 
2366                drawColor = Color.WHITE;
2367            }
2368        }
2369            
2370        g2.setColor(drawColor);
2371        
2372        int size = (int) layoutEditor.circleDiameter;
2373        
2374        g2.setFont(new Font("SansSerif", Font.BOLD, size));
2375        
2376        var metrics = g2.getFontMetrics();
2377        double x = center.getX() - ((double) metrics.charWidth('?'))/2; // - to move left
2378        double y = center.getY() + (metrics.getAscent()*0.9)/2;    // + to move down
2379        
2380        g2.drawString("?", (float) x, (float) y);
2381        
2382        g2.setColor(originalColor);
2383        g2.setFont(originalFont);
2384    }
2385    
2386    /**
2387     * {@inheritDoc}
2388     */
2389    @Override
2390    protected void draw2(Graphics2D g2, boolean isMain, float railDisplacement) {
2391        TurnoutType type = getTurnoutType();
2392
2393        Point2D pA = getCoordsA();
2394        Point2D pB = getCoordsB();
2395        Point2D pC = getCoordsC();
2396        Point2D pD = getCoordsD();
2397        Point2D pM = getCoordsCenter();
2398
2399        Point2D vAM = MathUtil.normalize(MathUtil.subtract(pM, pA));
2400        Point2D vAMo = MathUtil.orthogonal(MathUtil.normalize(vAM, railDisplacement));
2401
2402        Point2D pAL = MathUtil.subtract(pA, vAMo);
2403        Point2D pAR = MathUtil.add(pA, vAMo);
2404
2405        Point2D vBM = MathUtil.normalize(MathUtil.subtract(pB, pM));
2406        double dirBM_DEG = MathUtil.computeAngleDEG(vBM);
2407        Point2D vBMo = MathUtil.normalize(MathUtil.orthogonal(vBM), railDisplacement);
2408        Point2D pBL = MathUtil.subtract(pB, vBMo);
2409        Point2D pBR = MathUtil.add(pB, vBMo);
2410        Point2D pMR = MathUtil.add(pM, vBMo);
2411
2412        Point2D vCM = MathUtil.normalize(MathUtil.subtract(pC, pM));
2413        double dirCM_DEG = MathUtil.computeAngleDEG(vCM);
2414
2415        Point2D vCMo = MathUtil.normalize(MathUtil.orthogonal(vCM), railDisplacement);
2416        Point2D pCL = MathUtil.subtract(pC, vCMo);
2417        Point2D pCR = MathUtil.add(pC, vCMo);
2418        Point2D pML = MathUtil.subtract(pM, vBMo);
2419
2420        double deltaBMC_DEG = MathUtil.absDiffAngleDEG(dirBM_DEG, dirCM_DEG);
2421        double deltaBMC_RAD = Math.toRadians(deltaBMC_DEG);
2422
2423        double hypotF = railDisplacement / Math.sin(deltaBMC_RAD / 2.0);
2424
2425        Point2D vDisF = MathUtil.normalize(MathUtil.add(vAM, vCM), hypotF);
2426        if (type == TurnoutType.WYE_TURNOUT) {
2427            vDisF = MathUtil.normalize(vAM, hypotF);
2428        }
2429        Point2D pF = MathUtil.add(pM, vDisF);
2430
2431        Point2D pFR = MathUtil.add(pF, MathUtil.multiply(vBMo, 2.0));
2432        Point2D pFL = MathUtil.subtract(pF, MathUtil.multiply(vCMo, 2.0));
2433
2434        // Point2D pFPR = MathUtil.add(pF, MathUtil.normalize(vBMo, 2.0));
2435        // Point2D pFPL = MathUtil.subtract(pF, MathUtil.normalize(vCMo, 2.0));
2436        Point2D vDisAP = MathUtil.normalize(vAM, hypotF);
2437        Point2D pAP = MathUtil.subtract(pM, vDisAP);
2438        Point2D pAPR = MathUtil.add(pAP, vAMo);
2439        Point2D pAPL = MathUtil.subtract(pAP, vAMo);
2440
2441        // Point2D vSo = MathUtil.normalize(vAMo, 2.0);
2442        // Point2D pSL = MathUtil.add(pAPL, vSo);
2443        // Point2D pSR = MathUtil.subtract(pAPR, vSo);
2444        boolean mainlineA = isMainlineA();
2445        boolean mainlineB = isMainlineB();
2446        boolean mainlineC = isMainlineC();
2447        boolean mainlineD = isMainlineD();
2448
2449        int state = UNKNOWN;
2450        if (layoutEditor.isAnimating()) {
2451            state = getState();
2452        }
2453
2454        switch (type) {
2455            case RH_TURNOUT: {
2456                if (isMain == mainlineA) {
2457                    g2.draw(new Line2D.Double(pAL, pML));
2458                    g2.draw(new Line2D.Double(pAR, pAPR));
2459                }
2460                if (isMain == mainlineB) {
2461                    g2.draw(new Line2D.Double(pML, pBL));
2462                    g2.draw(new Line2D.Double(pF, pBR));
2463                    if (getContinuingSense() == state) {  // unknown or diverting path
2464//                         g2.draw(new Line2D.Double(pSR, pFPR));
2465//                     } else {
2466                        g2.draw(new Line2D.Double(pAPR, pF));
2467                    }
2468                }
2469                if (isMain == mainlineC) {
2470                    g2.draw(new Line2D.Double(pF, pCL));
2471                    g2.draw(new Line2D.Double(pFR, pCR));
2472                    GeneralPath path = new GeneralPath();
2473                    path.moveTo(pAPR.getX(), pAPR.getY());
2474                    path.quadTo(pMR.getX(), pMR.getY(), pFR.getX(), pFR.getY());
2475                    path.lineTo(pCR.getX(), pCR.getY());
2476                    g2.draw(path);
2477                    if (getContinuingSense() != state) {  // unknown or diverting path
2478                        path = new GeneralPath();
2479                        path.moveTo(pAPL.getX(), pAPL.getY());
2480                        path.quadTo(pML.getX(), pML.getY(), pF.getX(), pF.getY());
2481                        g2.draw(path);
2482//                     } else {
2483//                         path = new GeneralPath();
2484//                         path.moveTo(pSL.getX(), pSL.getY());
2485//                         path.quadTo(pML.getX(), pML.getY(), pFPL.getX(), pFPL.getY());
2486//                         g2.draw(path);
2487                    }
2488                }
2489                break;
2490            }   // case RH_TURNOUT
2491
2492            case LH_TURNOUT: {
2493                if (isMain == mainlineA) {
2494                    g2.draw(new Line2D.Double(pAR, pMR));
2495                    g2.draw(new Line2D.Double(pAL, pAPL));
2496                }
2497                if (isMain == mainlineB) {
2498                    g2.draw(new Line2D.Double(pMR, pBR));
2499                    g2.draw(new Line2D.Double(pF, pBL));
2500                    if (getContinuingSense() == state) {  // straight path
2501//                         g2.draw(new Line2D.Double(pSL, pFPL));  Offset problem
2502//                     } else {
2503                        g2.draw(new Line2D.Double(pAPL, pF));
2504                    }
2505                }
2506                if (isMain == mainlineC) {
2507                    g2.draw(new Line2D.Double(pF, pCR));
2508                    GeneralPath path = new GeneralPath();
2509                    path.moveTo(pAPL.getX(), pAPL.getY());
2510                    path.quadTo(pML.getX(), pML.getY(), pFL.getX(), pFL.getY());
2511                    path.lineTo(pCL.getX(), pCL.getY());
2512                    g2.draw(path);
2513                    if (getContinuingSense() != state) {  // unknown or diverting path
2514                        path = new GeneralPath();
2515                        path.moveTo(pAPR.getX(), pAPR.getY());
2516                        path.quadTo(pMR.getX(), pMR.getY(), pF.getX(), pF.getY());
2517                        g2.draw(path);
2518//                     } else {
2519//                         path = new GeneralPath();
2520//                         path.moveTo(pSR.getX(), pSR.getY());
2521//                         path.quadTo(pMR.getX(), pMR.getY(), pFPR.getX(), pFPR.getY());
2522//                         g2.draw(path);
2523                    }
2524                }
2525                break;
2526            }   // case LH_TURNOUT
2527
2528            case WYE_TURNOUT: {
2529                if (isMain == mainlineA) {
2530                    g2.draw(new Line2D.Double(pAL, pAPL));
2531                    g2.draw(new Line2D.Double(pAR, pAPR));
2532                }
2533                if (isMain == mainlineB) {
2534                    g2.draw(new Line2D.Double(pF, pBL));
2535                    GeneralPath path = new GeneralPath();
2536                    path.moveTo(pAPR.getX(), pAPR.getY());
2537                    path.quadTo(pMR.getX(), pMR.getY(), pFR.getX(), pFR.getY());
2538                    path.lineTo(pBR.getX(), pBR.getY());
2539                    g2.draw(path);
2540                    if (getContinuingSense() != state) {  // unknown or diverting path
2541                        path = new GeneralPath();
2542                        path.moveTo(pAPR.getX(), pAPR.getY());
2543                        path.quadTo(pMR.getX(), pMR.getY(), pF.getX(), pF.getY());
2544                        g2.draw(path);
2545//                     } else {
2546//                         path = new GeneralPath();
2547//                         path.moveTo(pSR.getX(), pSR.getY());
2548//                         path.quadTo(pMR.getX(), pMR.getY(), pFPR.getX(), pFPR.getY());
2549//                  bad    g2.draw(path);
2550                    }
2551                }
2552                if (isMain == mainlineC) {
2553                    pML = MathUtil.subtract(pM, vCMo);
2554                    GeneralPath path = new GeneralPath();
2555                    path.moveTo(pAPL.getX(), pAPL.getY());
2556                    path.quadTo(pML.getX(), pML.getY(), pFL.getX(), pFL.getY());
2557                    path.lineTo(pCL.getX(), pCL.getY());
2558                    g2.draw(path);
2559                    g2.draw(new Line2D.Double(pF, pCR));
2560                    if (getContinuingSense() != state) {  // unknown or diverting path
2561//                         path = new GeneralPath();
2562//                         path.moveTo(pSL.getX(), pSL.getY());
2563//                         path.quadTo(pML.getX(), pML.getY(), pFPL.getX(), pFPL.getY());
2564//           bad              g2.draw(path);
2565                    } else {
2566                        path = new GeneralPath();
2567                        path.moveTo(pAPL.getX(), pAPL.getY());
2568                        path.quadTo(pML.getX(), pML.getY(), pF.getX(), pF.getY());
2569                        g2.draw(path);
2570                    }
2571                }
2572                break;
2573            }   // case WYE_TURNOUT
2574
2575            case DOUBLE_XOVER: {
2576                // A, B, C, D end points (left and right)
2577                Point2D vAB = MathUtil.normalize(MathUtil.subtract(pB, pA), railDisplacement);
2578                double dirAB_DEG = MathUtil.computeAngleDEG(vAB);
2579                Point2D vABo = MathUtil.orthogonal(MathUtil.normalize(vAB, railDisplacement));
2580                pAL = MathUtil.subtract(pA, vABo);
2581                pAR = MathUtil.add(pA, vABo);
2582                pBL = MathUtil.subtract(pB, vABo);
2583                pBR = MathUtil.add(pB, vABo);
2584                Point2D vCD = MathUtil.normalize(MathUtil.subtract(pD, pC), railDisplacement);
2585                Point2D vCDo = MathUtil.orthogonal(MathUtil.normalize(vCD, railDisplacement));
2586                pCL = MathUtil.add(pC, vCDo);
2587                pCR = MathUtil.subtract(pC, vCDo);
2588                Point2D pDL = MathUtil.add(pD, vCDo);
2589                Point2D pDR = MathUtil.subtract(pD, vCDo);
2590
2591                // AB, CD mid points (left and right)
2592                Point2D pABM = MathUtil.midPoint(pA, pB);
2593                Point2D pABL = MathUtil.midPoint(pAL, pBL);
2594                Point2D pABR = MathUtil.midPoint(pAR, pBR);
2595                Point2D pCDM = MathUtil.midPoint(pC, pD);
2596                Point2D pCDL = MathUtil.midPoint(pCL, pDL);
2597                Point2D pCDR = MathUtil.midPoint(pCR, pDR);
2598
2599                // A, B, C, D mid points
2600                double halfParallelDistance = MathUtil.distance(pABM, pCDM) / 2.0;
2601                Point2D pAM = MathUtil.subtract(pABM, MathUtil.normalize(vAB, halfParallelDistance));
2602                Point2D pAML = MathUtil.subtract(pAM, vABo);
2603                Point2D pAMR = MathUtil.add(pAM, vABo);
2604                Point2D pBM = MathUtil.add(pABM, MathUtil.normalize(vAB, halfParallelDistance));
2605                Point2D pBML = MathUtil.subtract(pBM, vABo);
2606                Point2D pBMR = MathUtil.add(pBM, vABo);
2607                Point2D pCM = MathUtil.subtract(pCDM, MathUtil.normalize(vCD, halfParallelDistance));
2608                Point2D pCML = MathUtil.subtract(pCM, vABo);
2609                Point2D pCMR = MathUtil.add(pCM, vABo);
2610                Point2D pDM = MathUtil.add(pCDM, MathUtil.normalize(vCD, halfParallelDistance));
2611                Point2D pDML = MathUtil.subtract(pDM, vABo);
2612                Point2D pDMR = MathUtil.add(pDM, vABo);
2613
2614                // crossing points
2615                Point2D vACM = MathUtil.normalize(MathUtil.subtract(pCM, pAM), railDisplacement);
2616                Point2D vACMo = MathUtil.orthogonal(vACM);
2617                Point2D vBDM = MathUtil.normalize(MathUtil.subtract(pDM, pBM), railDisplacement);
2618                Point2D vBDMo = MathUtil.orthogonal(vBDM);
2619                Point2D pBDR = MathUtil.add(pM, vACM);
2620                Point2D pBDL = MathUtil.subtract(pM, vACM);
2621
2622                // crossing diamond point (no gaps)
2623                Point2D pVR = MathUtil.add(pBDL, vBDM);
2624                Point2D pKL = MathUtil.subtract(pBDL, vBDM);
2625                Point2D pKR = MathUtil.add(pBDR, vBDM);
2626                Point2D pVL = MathUtil.subtract(pBDR, vBDM);
2627
2628                // crossing diamond points (with gaps)
2629                Point2D vACM2 = MathUtil.normalize(vACM, 2.0);
2630                Point2D vBDM2 = MathUtil.normalize(vBDM, 2.0);
2631                // (syntax of "pKLtC" is "point LK toward C", etc.)
2632                Point2D pKLtC = MathUtil.add(pKL, vACM2);
2633                Point2D pKLtD = MathUtil.add(pKL, vBDM2);
2634                Point2D pVLtA = MathUtil.subtract(pVL, vACM2);
2635                Point2D pVLtD = MathUtil.add(pVL, vBDM2);
2636                Point2D pKRtA = MathUtil.subtract(pKR, vACM2);
2637                Point2D pKRtB = MathUtil.subtract(pKR, vBDM2);
2638                Point2D pVRtB = MathUtil.subtract(pVR, vBDM2);
2639                Point2D pVRtC = MathUtil.add(pVR, vACM2);
2640
2641                // A, B, C, D frog points
2642                vCM = MathUtil.normalize(MathUtil.subtract(pCM, pM));
2643                dirCM_DEG = MathUtil.computeAngleDEG(vCM);
2644                double deltaBAC_DEG = MathUtil.absDiffAngleDEG(dirAB_DEG, dirCM_DEG);
2645                double deltaBAC_RAD = Math.toRadians(deltaBAC_DEG);
2646                hypotF = railDisplacement / Math.sin(deltaBAC_RAD / 2.0);
2647                Point2D vACF = MathUtil.normalize(MathUtil.add(vACM, vAB), hypotF);
2648                Point2D pAFL = MathUtil.add(pAM, vACF);
2649                Point2D pCFR = MathUtil.subtract(pCM, vACF);
2650                Point2D vBDF = MathUtil.normalize(MathUtil.add(vBDM, vCD), hypotF);
2651                Point2D pBFL = MathUtil.add(pBM, vBDF);
2652                Point2D pDFR = MathUtil.subtract(pDM, vBDF);
2653
2654                // A, B, C, D frog points
2655                Point2D pAFR = MathUtil.add(MathUtil.add(pAFL, vACMo), vACMo);
2656                Point2D pBFR = MathUtil.subtract(MathUtil.subtract(pBFL, vBDMo), vBDMo);
2657                Point2D pCFL = MathUtil.subtract(MathUtil.subtract(pCFR, vACMo), vACMo);
2658                Point2D pDFL = MathUtil.add(MathUtil.add(pDFR, vBDMo), vBDMo);
2659
2660                // end of switch rails (closed)
2661                Point2D vABF = MathUtil.normalize(vAB, hypotF);
2662                pAP = MathUtil.subtract(pAM, vABF);
2663                pAPL = MathUtil.subtract(pAP, vABo);
2664                pAPR = MathUtil.add(pAP, vABo);
2665                Point2D pBP = MathUtil.add(pBM, vABF);
2666                Point2D pBPL = MathUtil.subtract(pBP, vABo);
2667                Point2D pBPR = MathUtil.add(pBP, vABo);
2668
2669                Point2D vCDF = MathUtil.normalize(vCD, hypotF);
2670                Point2D pCP = MathUtil.subtract(pCM, vCDF);
2671                Point2D pCPL = MathUtil.add(pCP, vCDo);
2672                Point2D pCPR = MathUtil.subtract(pCP, vCDo);
2673                Point2D pDP = MathUtil.add(pDM, vCDF);
2674                Point2D pDPL = MathUtil.add(pDP, vCDo);
2675                Point2D pDPR = MathUtil.subtract(pDP, vCDo);
2676
2677                // end of switch rails (open)
2678                Point2D vS = MathUtil.normalize(vABo, 2.0);
2679                Point2D pASL = MathUtil.add(pAPL, vS);
2680                // Point2D pASR = MathUtil.subtract(pAPR, vS);
2681                Point2D pBSL = MathUtil.add(pBPL, vS);
2682                // Point2D pBSR = MathUtil.subtract(pBPR, vS);
2683                Point2D pCSR = MathUtil.subtract(pCPR, vS);
2684                // Point2D pCSL = MathUtil.add(pCPL, vS);
2685                Point2D pDSR = MathUtil.subtract(pDPR, vS);
2686                // Point2D pDSL = MathUtil.add(pDPL, vS);
2687
2688                // end of switch rails (open at frogs)
2689                Point2D pAFS = MathUtil.subtract(pAFL, vS);
2690                Point2D pBFS = MathUtil.subtract(pBFL, vS);
2691                Point2D pCFS = MathUtil.add(pCFR, vS);
2692                Point2D pDFS = MathUtil.add(pDFR, vS);
2693
2694                // vSo = MathUtil.orthogonal(vS);
2695                // Point2D pAFSR = MathUtil.add(pAFL, vSo);
2696                // Point2D pBFSR = MathUtil.subtract(pBFL, vSo);
2697                // Point2D pCFSL = MathUtil.subtract(pCFR, vSo);
2698                // Point2D pDFSL = MathUtil.add(pDFR, vSo);
2699                if (isMain == mainlineA) {
2700                    g2.draw(new Line2D.Double(pAL, pABL));
2701                    g2.draw(new Line2D.Double(pVRtB, pKLtD));
2702                    g2.draw(new Line2D.Double(pAFL, pABR));
2703                    g2.draw(new Line2D.Double(pAFL, pKL));
2704                    GeneralPath path = new GeneralPath();
2705                    path.moveTo(pAR.getX(), pAR.getY());
2706                    path.lineTo(pAPR.getX(), pAPR.getY());
2707                    path.quadTo(pAMR.getX(), pAMR.getY(), pAFR.getX(), pAFR.getY());
2708                    path.lineTo(pVR.getX(), pVR.getY());
2709                    g2.draw(path);
2710                    if (state != Turnout.CLOSED) {  // unknown or diverting path
2711                        path = new GeneralPath();
2712                        path.moveTo(pAPL.getX(), pAPL.getY());
2713                        path.quadTo(pAML.getX(), pAML.getY(), pAFL.getX(), pAFL.getY());
2714                        g2.draw(path);
2715//                         g2.draw(new Line2D.Double(pASR, pAFSR));
2716                    } else {                        // continuing path
2717                        g2.draw(new Line2D.Double(pAPR, pAFL));
2718                        path = new GeneralPath();
2719                        path.moveTo(pASL.getX(), pASL.getY());
2720                        path.quadTo(pAML.getX(), pAML.getY(), pAFS.getX(), pAFS.getY());
2721//                         g2.draw(path);
2722                    }
2723                }
2724                if (isMain == mainlineB) {
2725                    g2.draw(new Line2D.Double(pABL, pBL));
2726                    g2.draw(new Line2D.Double(pKLtC, pVLtA));
2727                    g2.draw(new Line2D.Double(pBFL, pABR));
2728                    g2.draw(new Line2D.Double(pBFL, pKL));
2729                    GeneralPath path = new GeneralPath();
2730                    path.moveTo(pBR.getX(), pBR.getY());
2731                    path.lineTo(pBPR.getX(), pBPR.getY());
2732                    path.quadTo(pBMR.getX(), pBMR.getY(), pBFR.getX(), pBFR.getY());
2733                    path.lineTo(pVL.getX(), pVL.getY());
2734                    g2.draw(path);
2735                    if (state != Turnout.CLOSED) {  // unknown or diverting path
2736                        path = new GeneralPath();
2737                        path.moveTo(pBPL.getX(), pBPL.getY());
2738                        path.quadTo(pBML.getX(), pBML.getY(), pBFL.getX(), pBFL.getY());
2739                        g2.draw(path);
2740//                         g2.draw(new Line2D.Double(pBSR, pBFSR));
2741                    } else {
2742                        g2.draw(new Line2D.Double(pBPR, pBFL));
2743                        path = new GeneralPath();
2744                        path.moveTo(pBSL.getX(), pBSL.getY());
2745                        path.quadTo(pBML.getX(), pBML.getY(), pBFS.getX(), pBFS.getY());
2746//                         g2.draw(path);
2747                    }
2748                }
2749                if (isMain == mainlineC) {
2750                    g2.draw(new Line2D.Double(pCR, pCDR));
2751                    g2.draw(new Line2D.Double(pKRtB, pVLtD));
2752                    g2.draw(new Line2D.Double(pCFR, pCDL));
2753                    g2.draw(new Line2D.Double(pCFR, pKR));
2754                    GeneralPath path = new GeneralPath();
2755                    path.moveTo(pCL.getX(), pCL.getY());
2756                    path.lineTo(pCPL.getX(), pCPL.getY());
2757                    path.quadTo(pCML.getX(), pCML.getY(), pCFL.getX(), pCFL.getY());
2758                    path.lineTo(pVL.getX(), pVL.getY());
2759                    g2.draw(path);
2760                    if (state != Turnout.CLOSED) {  // unknown or diverting path
2761                        path = new GeneralPath();
2762                        path.moveTo(pCPR.getX(), pCPR.getY());
2763                        path.quadTo(pCMR.getX(), pCMR.getY(), pCFR.getX(), pCFR.getY());
2764                        g2.draw(path);
2765//                         g2.draw(new Line2D.Double(pCSL, pCFSL));
2766                    } else {
2767                        g2.draw(new Line2D.Double(pCPL, pCFR));
2768                        path = new GeneralPath();
2769                        path.moveTo(pCSR.getX(), pCSR.getY());
2770                        path.quadTo(pCMR.getX(), pCMR.getY(), pCFS.getX(), pCFS.getY());
2771//                         g2.draw(path);
2772                    }
2773                }
2774                if (isMain == mainlineD) {
2775                    g2.draw(new Line2D.Double(pCDR, pDR));
2776                    g2.draw(new Line2D.Double(pKRtA, pVRtC));
2777                    g2.draw(new Line2D.Double(pDFR, pCDL));
2778                    g2.draw(new Line2D.Double(pDFR, pKR));
2779                    GeneralPath path = new GeneralPath();
2780                    path.moveTo(pDL.getX(), pDL.getY());
2781                    path.lineTo(pDPL.getX(), pDPL.getY());
2782                    path.quadTo(pDML.getX(), pDML.getY(), pDFL.getX(), pDFL.getY());
2783                    path.lineTo(pVR.getX(), pVR.getY());
2784                    g2.draw(path);
2785                    if (state != Turnout.CLOSED) {  // unknown or diverting path
2786                        path = new GeneralPath();
2787                        path.moveTo(pDPR.getX(), pDPR.getY());
2788                        path.quadTo(pDMR.getX(), pDMR.getY(), pDFR.getX(), pDFR.getY());
2789                        g2.draw(path);
2790//                         g2.draw(new Line2D.Double(pDSL, pDFSL));
2791                    } else {
2792                        g2.draw(new Line2D.Double(pDPL, pDFR));
2793                        path = new GeneralPath();
2794                        path.moveTo(pDSR.getX(), pDSR.getY());
2795                        path.quadTo(pDMR.getX(), pDMR.getY(), pDFS.getX(), pDFS.getY());
2796//                         g2.draw(path);
2797                    }
2798                }
2799                break;
2800            }   // case DOUBLE_XOVER
2801
2802            case RH_XOVER: {
2803                // A, B, C, D end points (left and right)
2804                Point2D vAB = MathUtil.normalize(MathUtil.subtract(pB, pA), railDisplacement);
2805                double dirAB_DEG = MathUtil.computeAngleDEG(vAB);
2806                Point2D vABo = MathUtil.orthogonal(MathUtil.normalize(vAB, railDisplacement));
2807                pAL = MathUtil.subtract(pA, vABo);
2808                pAR = MathUtil.add(pA, vABo);
2809                pBL = MathUtil.subtract(pB, vABo);
2810                pBR = MathUtil.add(pB, vABo);
2811                Point2D vCD = MathUtil.normalize(MathUtil.subtract(pD, pC), railDisplacement);
2812                Point2D vCDo = MathUtil.orthogonal(MathUtil.normalize(vCD, railDisplacement));
2813                pCL = MathUtil.add(pC, vCDo);
2814                pCR = MathUtil.subtract(pC, vCDo);
2815                Point2D pDL = MathUtil.add(pD, vCDo);
2816                Point2D pDR = MathUtil.subtract(pD, vCDo);
2817
2818                // AB and CD mid points
2819                Point2D pABM = MathUtil.midPoint(pA, pB);
2820                Point2D pABL = MathUtil.subtract(pABM, vABo);
2821                Point2D pABR = MathUtil.add(pABM, vABo);
2822                Point2D pCDM = MathUtil.midPoint(pC, pD);
2823                Point2D pCDL = MathUtil.subtract(pCDM, vABo);
2824                Point2D pCDR = MathUtil.add(pCDM, vABo);
2825
2826                // directions
2827                Point2D vAC = MathUtil.normalize(MathUtil.subtract(pCDM, pABM), railDisplacement);
2828                Point2D vACo = MathUtil.orthogonal(MathUtil.normalize(vAC, railDisplacement));
2829                double dirAC_DEG = MathUtil.computeAngleDEG(vAC);
2830                double deltaBAC_DEG = MathUtil.absDiffAngleDEG(dirAB_DEG, dirAC_DEG);
2831                double deltaBAC_RAD = Math.toRadians(deltaBAC_DEG);
2832
2833                // AC mid points
2834                Point2D pACL = MathUtil.subtract(pM, vACo);
2835                Point2D pACR = MathUtil.add(pM, vACo);
2836
2837                // frogs
2838                hypotF = railDisplacement / Math.sin(deltaBAC_RAD / 2.0);
2839                Point2D vF = MathUtil.normalize(MathUtil.add(vAB, vAC), hypotF);
2840                Point2D pABF = MathUtil.add(pABM, vF);
2841                Point2D pCDF = MathUtil.subtract(pCDM, vF);
2842
2843                // frog primes
2844                Point2D pABFP = MathUtil.add(MathUtil.add(pABF, vACo), vACo);
2845                Point2D pCDFP = MathUtil.subtract(MathUtil.subtract(pCDF, vACo), vACo);
2846
2847                // end of switch rails (closed)
2848                Point2D vABF = MathUtil.normalize(vAB, hypotF);
2849                pAP = MathUtil.subtract(pABM, vABF);
2850                pAPL = MathUtil.subtract(pAP, vABo);
2851                pAPR = MathUtil.add(pAP, vABo);
2852                Point2D pCP = MathUtil.add(pCDM, vABF);
2853                Point2D pCPL = MathUtil.add(pCP, vCDo);
2854                Point2D pCPR = MathUtil.subtract(pCP, vCDo);
2855
2856                // end of switch rails (open)
2857                Point2D vS = MathUtil.normalize(vAB, 2.0);
2858                Point2D vSo = MathUtil.orthogonal(vS);
2859                Point2D pASL = MathUtil.add(pAPL, vSo);
2860                // Point2D pASR = MathUtil.subtract(pAPR, vSo);
2861                // Point2D pCSL = MathUtil.add(pCPL, vSo);
2862                Point2D pCSR = MathUtil.subtract(pCPR, vSo);
2863
2864                // end of switch rails (open at frogs)
2865                Point2D pABFS = MathUtil.subtract(pABF, vSo);
2866                // Point2D pABFSP = MathUtil.subtract(pABF, vS);
2867                Point2D pCDFS = MathUtil.add(pCDF, vSo);
2868                // Point2D pCDFSP = MathUtil.add(pCDF, vS);
2869
2870                if (isMain == mainlineA) {
2871                    g2.draw(new Line2D.Double(pAL, pABL));
2872                    GeneralPath path = new GeneralPath();
2873                    path.moveTo(pAR.getX(), pAR.getY());
2874                    path.lineTo(pAPR.getX(), pAPR.getY());
2875                    path.quadTo(pABR.getX(), pABR.getY(), pABFP.getX(), pABFP.getY());
2876                    path.lineTo(pACR.getX(), pACR.getY());
2877                    g2.draw(path);
2878                    g2.draw(new Line2D.Double(pABF, pACL));
2879                    if (state != Turnout.CLOSED) {  // unknown or diverting path
2880                        path = new GeneralPath();
2881                        path.moveTo(pAPL.getX(), pAPL.getY());
2882                        path.quadTo(pABL.getX(), pABL.getY(), pABF.getX(), pABF.getY());
2883                        g2.draw(path);
2884//                         g2.draw(new Line2D.Double(pASR, pABFSP));
2885                    } else {                        // continuing path
2886                        g2.draw(new Line2D.Double(pAPR, pABF));
2887                        path = new GeneralPath();
2888                        path.moveTo(pASL.getX(), pASL.getY());
2889                        path.quadTo(pABL.getX(), pABL.getY(), pABFS.getX(), pABFS.getY());
2890//                         g2.draw(path);
2891                    }
2892                }
2893                if (isMain == mainlineB) {
2894                    g2.draw(new Line2D.Double(pABL, pBL));
2895                    g2.draw(new Line2D.Double(pABF, pBR));
2896                }
2897                if (isMain == mainlineC) {
2898                    g2.draw(new Line2D.Double(pCR, pCDR));
2899                    GeneralPath path = new GeneralPath();
2900                    path.moveTo(pCL.getX(), pCL.getY());
2901                    path.lineTo(pCPL.getX(), pCPL.getY());
2902                    path.quadTo(pCDL.getX(), pCDL.getY(), pCDFP.getX(), pCDFP.getY());
2903                    path.lineTo(pACL.getX(), pACL.getY());
2904                    g2.draw(path);
2905                    g2.draw(new Line2D.Double(pCDF, pACR));
2906                    if (state != Turnout.CLOSED) {  // unknown or diverting path
2907                        path = new GeneralPath();
2908                        path.moveTo(pCPR.getX(), pCPR.getY());
2909                        path.quadTo(pCDR.getX(), pCDR.getY(), pCDF.getX(), pCDF.getY());
2910                        g2.draw(path);
2911//                         g2.draw(new Line2D.Double(pCSL, pCDFSP));
2912                    } else {                        // continuing path
2913                        g2.draw(new Line2D.Double(pCPL, pCDF));
2914                        path = new GeneralPath();
2915                        path.moveTo(pCSR.getX(), pCSR.getY());
2916                        path.quadTo(pCDR.getX(), pCDR.getY(), pCDFS.getX(), pCDFS.getY());
2917//                         g2.draw(path);
2918                    }
2919                }
2920                if (isMain == mainlineD) {
2921                    g2.draw(new Line2D.Double(pCDR, pDR));
2922                    g2.draw(new Line2D.Double(pCDF, pDL));
2923                }
2924                break;
2925            }   // case RH_XOVER
2926
2927            case LH_XOVER: {
2928                // B, A, D, C end points (left and right)
2929                Point2D vBA = MathUtil.normalize(MathUtil.subtract(pA, pB), railDisplacement);
2930                double dirBA_DEG = MathUtil.computeAngleDEG(vBA);
2931                Point2D vBAo = MathUtil.orthogonal(MathUtil.normalize(vBA, railDisplacement));
2932                pBL = MathUtil.add(pB, vBAo);
2933                pBR = MathUtil.subtract(pB, vBAo);
2934                pAL = MathUtil.add(pA, vBAo);
2935                pAR = MathUtil.subtract(pA, vBAo);
2936                Point2D vDC = MathUtil.normalize(MathUtil.subtract(pC, pD), railDisplacement);
2937                Point2D vDCo = MathUtil.orthogonal(MathUtil.normalize(vDC, railDisplacement));
2938                Point2D pDL = MathUtil.subtract(pD, vDCo);
2939                Point2D pDR = MathUtil.add(pD, vDCo);
2940                pCL = MathUtil.subtract(pC, vDCo);
2941                pCR = MathUtil.add(pC, vDCo);
2942
2943                // BA and DC mid points
2944                Point2D pBAM = MathUtil.midPoint(pB, pA);
2945                Point2D pBAL = MathUtil.add(pBAM, vBAo);
2946                Point2D pBAR = MathUtil.subtract(pBAM, vBAo);
2947                Point2D pDCM = MathUtil.midPoint(pD, pC);
2948                Point2D pDCL = MathUtil.add(pDCM, vBAo);
2949                Point2D pDCR = MathUtil.subtract(pDCM, vBAo);
2950
2951                // directions
2952                Point2D vBD = MathUtil.normalize(MathUtil.subtract(pDCM, pBAM), railDisplacement);
2953                Point2D vBDo = MathUtil.orthogonal(MathUtil.normalize(vBD, railDisplacement));
2954                double dirBD_DEG = MathUtil.computeAngleDEG(vBD);
2955                double deltaABD_DEG = MathUtil.absDiffAngleDEG(dirBA_DEG, dirBD_DEG);
2956                double deltaABD_RAD = Math.toRadians(deltaABD_DEG);
2957
2958                // BD mid points
2959                Point2D pBDL = MathUtil.add(pM, vBDo);
2960                Point2D pBDR = MathUtil.subtract(pM, vBDo);
2961
2962                // frogs
2963                hypotF = railDisplacement / Math.sin(deltaABD_RAD / 2.0);
2964                Point2D vF = MathUtil.normalize(MathUtil.add(vBA, vBD), hypotF);
2965                Point2D pBFL = MathUtil.add(pBAM, vF);
2966                Point2D pBF = MathUtil.subtract(pBFL, vBDo);
2967                Point2D pBFR = MathUtil.subtract(pBF, vBDo);
2968                Point2D pDFR = MathUtil.subtract(pDCM, vF);
2969                Point2D pDF = MathUtil.add(pDFR, vBDo);
2970                Point2D pDFL = MathUtil.add(pDF, vBDo);
2971
2972                // end of switch rails (closed)
2973                Point2D vBAF = MathUtil.normalize(vBA, hypotF);
2974                Point2D pBP = MathUtil.subtract(pBAM, vBAF);
2975                Point2D pBPL = MathUtil.add(pBP, vBAo);
2976                Point2D pBPR = MathUtil.subtract(pBP, vBAo);
2977                Point2D pDP = MathUtil.add(pDCM, vBAF);
2978                Point2D pDPL = MathUtil.subtract(pDP, vDCo);
2979                Point2D pDPR = MathUtil.add(pDP, vDCo);
2980
2981                // end of switch rails (open)
2982                Point2D vS = MathUtil.normalize(vBA, 2.0);
2983                Point2D vSo = MathUtil.orthogonal(vS);
2984                Point2D pBSL = MathUtil.subtract(pBPL, vSo);
2985                // Point2D pBSR = MathUtil.add(pBPR, vSo);
2986                // Point2D pDSL = MathUtil.subtract(pDPL, vSo);
2987                Point2D pDSR = MathUtil.add(pDPR, vSo);
2988
2989                // end of switch rails (open at frogs)
2990                Point2D pBAFS = MathUtil.add(pBFL, vSo);
2991                // Point2D pBAFSP = MathUtil.subtract(pBFL, vS);
2992                Point2D pDCFS = MathUtil.subtract(pDFR, vSo);
2993                // Point2D pDCFSP = MathUtil.add(pDFR, vS);
2994
2995                if (isMain == mainlineA) {
2996                    g2.draw(new Line2D.Double(pBAL, pAL));
2997                    g2.draw(new Line2D.Double(pBFL, pAR));
2998                }
2999                if (isMain == mainlineB) {
3000                    g2.draw(new Line2D.Double(pBL, pBAL));
3001                    GeneralPath path = new GeneralPath();
3002                    path.moveTo(pBR.getX(), pBR.getY());
3003                    path.lineTo(pBPR.getX(), pBPR.getY());
3004                    path.quadTo(pBAR.getX(), pBAR.getY(), pBFR.getX(), pBFR.getY());
3005                    path.lineTo(pBDR.getX(), pBDR.getY());
3006                    g2.draw(path);
3007                    g2.draw(new Line2D.Double(pBFL, pBDL));
3008                    if (state != Turnout.CLOSED) {  // unknown or diverting path
3009                        path = new GeneralPath();
3010                        path.moveTo(pBPL.getX(), pBPL.getY());
3011                        path.quadTo(pBAL.getX(), pBAL.getY(), pBFL.getX(), pBFL.getY());
3012                        g2.draw(path);
3013//                         g2.draw(new Line2D.Double(pBSR, pBAFSP));
3014                    } else {                        // continuing path
3015                        g2.draw(new Line2D.Double(pBPR, pBFL));
3016                        path = new GeneralPath();
3017                        path.moveTo(pBSL.getX(), pBSL.getY());
3018                        path.quadTo(pBAL.getX(), pBAL.getY(), pBAFS.getX(), pBAFS.getY());
3019//                         g2.draw(path);
3020                    }
3021                }
3022                if (isMain == mainlineC) {
3023                    g2.draw(new Line2D.Double(pDCR, pCR));
3024                    g2.draw(new Line2D.Double(pDFR, pCL));
3025                }
3026                if (isMain == mainlineD) {
3027                    g2.draw(new Line2D.Double(pDR, pDCR));
3028                    GeneralPath path = new GeneralPath();
3029                    path.moveTo(pDL.getX(), pDL.getY());
3030                    path.lineTo(pDPL.getX(), pDPL.getY());
3031                    path.quadTo(pDCL.getX(), pDCL.getY(), pDFL.getX(), pDFL.getY());
3032                    path.lineTo(pBDL.getX(), pBDL.getY());
3033                    g2.draw(path);
3034                    g2.draw(new Line2D.Double(pDFR, pBDR));
3035                    if (state != Turnout.CLOSED) {  // unknown or diverting path
3036                        path = new GeneralPath();
3037                        path.moveTo(pDPR.getX(), pDPR.getY());
3038                        path.quadTo(pDCR.getX(), pDCR.getY(), pDFR.getX(), pDFR.getY());
3039                        g2.draw(path);
3040//                         g2.draw(new Line2D.Double(pDSL, pDCFSP));
3041                    } else {                        // continuing path
3042                        g2.draw(new Line2D.Double(pDPL, pDFR));
3043                        path = new GeneralPath();
3044                        path.moveTo(pDSR.getX(), pDSR.getY());
3045                        path.quadTo(pDCR.getX(), pDCR.getY(), pDCFS.getX(), pDCFS.getY());
3046//                         g2.draw(path);
3047                    }
3048                }
3049                break;
3050            }   // case LH_XOVER
3051            case SINGLE_SLIP:
3052            case DOUBLE_SLIP: {
3053                log.error("{}.draw2(...); slips should be being drawn by LayoutSlip sub-class", getName());
3054                break;
3055            }
3056            default: {
3057                // this should never happen... but...
3058                log.error("{}.draw2(...); Unknown turnout type {}", getName(), type);
3059                break;
3060            }
3061        }
3062    }   // draw2
3063
3064    /**
3065     * {@inheritDoc}
3066     */
3067    @Override
3068    protected void highlightUnconnected(Graphics2D g2, HitPointType specificType) {
3069        if (((specificType == HitPointType.NONE) || (specificType == HitPointType.TURNOUT_A))
3070                && (getConnectA() == null)) {
3071            g2.fill(trackControlCircleAt(getCoordsA()));
3072        }
3073
3074        if (((specificType == HitPointType.NONE) || (specificType == HitPointType.TURNOUT_B))
3075                && (getConnectB() == null)) {
3076            g2.fill(trackControlCircleAt(getCoordsB()));
3077        }
3078
3079        if (((specificType == HitPointType.NONE) || (specificType == HitPointType.TURNOUT_C))
3080                && (getConnectC() == null)) {
3081            g2.fill(trackControlCircleAt(getCoordsC()));
3082        }
3083        if (isTurnoutTypeXover()) {
3084            if (((specificType == HitPointType.NONE) || (specificType == HitPointType.TURNOUT_D))
3085                    && (getConnectD() == null)) {
3086                g2.fill(trackControlCircleAt(getCoordsD()));
3087            }
3088        }
3089    }
3090
3091    /**
3092     * {@inheritDoc}
3093     */
3094    @Override
3095    protected void drawTurnoutControls(Graphics2D g2) {
3096        if (!isDisabled() && !(isDisabledWhenOccupied() && isOccupied())) {
3097            Color foregroundColor = g2.getColor();
3098
3099            if (getState() != Turnout.CLOSED) {
3100                // then switch to background (thrown) color
3101                g2.setColor(g2.getBackground());
3102            }
3103
3104            if (layoutEditor.isTurnoutFillControlCircles()) {
3105                g2.fill(trackControlCircleAt(getCoordsCenter()));
3106                // do we need to draw a ? for unknown over the circle?
3107                if (showUnknown && getState() == UNKNOWN) {
3108                    drawForShowUnknown(g2, getCoordsCenter(), g2.getBackground(), true);
3109                    return;
3110                }    
3111            } else {
3112                g2.draw(trackControlCircleAt(getCoordsCenter()));
3113            }
3114
3115            if (getState() != Turnout.CLOSED) {
3116                // then restore foreground color
3117                g2.setColor(foregroundColor);
3118            }
3119            
3120            
3121        }
3122    }
3123
3124    /**
3125     * {@inheritDoc}
3126     */
3127    @Override
3128    protected void drawEditControls(Graphics2D g2) {
3129        Point2D pt = getCoordsA();
3130        if (isTurnoutTypeXover() || isTurnoutTypeSlip()) {
3131            if (getConnectA() == null) {
3132                g2.setColor(Color.magenta);
3133            } else {
3134                g2.setColor(Color.blue);
3135            }
3136        } else {
3137            if (getConnectA() == null) {
3138                g2.setColor(Color.red);
3139            } else {
3140                g2.setColor(Color.green);
3141            }
3142        }
3143        g2.draw(layoutEditor.layoutEditorControlRectAt(pt));
3144
3145        pt = getCoordsB();
3146        if (getConnectB() == null) {
3147            g2.setColor(Color.red);
3148        } else {
3149            g2.setColor(Color.green);
3150        }
3151        g2.draw(layoutEditor.layoutEditorControlRectAt(pt));
3152
3153        pt = getCoordsC();
3154        if (getConnectC() == null) {
3155            g2.setColor(Color.red);
3156        } else {
3157            g2.setColor(Color.green);
3158        }
3159        g2.draw(layoutEditor.layoutEditorControlRectAt(pt));
3160
3161        if (isTurnoutTypeXover() || isTurnoutTypeSlip()) {
3162            pt = getCoordsD();
3163            if (getConnectD() == null) {
3164                g2.setColor(Color.red);
3165            } else {
3166                g2.setColor(Color.green);
3167            }
3168            g2.draw(layoutEditor.layoutEditorControlRectAt(pt));
3169        }
3170    }
3171
3172    /*
3173    * Used by ConnectivityUtil to determine the turnout state necessary to get
3174    * from prevLayoutBlock ==> currLayoutBlock ==> nextLayoutBlock
3175     */
3176    protected int getConnectivityStateForLayoutBlocks(
3177            LayoutBlock currLayoutBlock,
3178            LayoutBlock prevLayoutBlock,
3179            LayoutBlock nextLayoutBlock,
3180            boolean suppress) {
3181
3182        return turnout.getConnectivityStateForLayoutBlocks(currLayoutBlock,
3183                prevLayoutBlock,
3184                nextLayoutBlock,
3185                suppress);
3186    }
3187
3188    /**
3189     * {@inheritDoc}
3190     */
3191    // TODO: on the cross-overs, check the internal boundary details.
3192    @Override
3193    public void reCheckBlockBoundary() {
3194
3195        turnout.reCheckBlockBoundary();
3196
3197    }
3198
3199    /**
3200     * {@inheritDoc}
3201     */
3202    @Override
3203    protected List<LayoutConnectivity> getLayoutConnectivity() {
3204        return turnout.getLayoutConnectivity();
3205    }
3206
3207    /**
3208     * {@inheritDoc}
3209     */
3210    @Override
3211    public @Nonnull
3212    List<HitPointType> checkForFreeConnections() {
3213        return turnout.checkForFreeConnections();
3214    }
3215
3216    /**
3217     * {@inheritDoc}
3218     */
3219    @Override
3220    public boolean checkForUnAssignedBlocks() {
3221        // because getLayoutBlock[BCD] will return block [A] if they're null
3222        // we only need to test block [A]
3223        return turnout.checkForUnAssignedBlocks();
3224    }
3225
3226    /**
3227     * {@inheritDoc}
3228     */
3229    @Override
3230    public void checkForNonContiguousBlocks(
3231            @Nonnull HashMap<String, List<Set<String>>> blockNamesToTrackNameSetsMap) {
3232
3233        turnout.checkForNonContiguousBlocks(blockNamesToTrackNameSetsMap);
3234    }
3235
3236    /**
3237     * {@inheritDoc}
3238     */
3239    @Override
3240    public void collectContiguousTracksNamesInBlockNamed(
3241            @Nonnull String blockName,
3242            @Nonnull Set<String> TrackNameSet) {
3243
3244        turnout.collectContiguousTracksNamesInBlockNamed(blockName, TrackNameSet);
3245    }
3246
3247    /**
3248     * {@inheritDoc}
3249     */
3250    @Override
3251    public void setAllLayoutBlocks(LayoutBlock layoutBlock) {
3252        turnout.setAllLayoutBlocks(layoutBlock);
3253    }
3254
3255    private static class AbstractActionImpl extends AbstractAction {
3256
3257        private final String blockName;
3258        private final LayoutBlock layoutBlock;
3259
3260        AbstractActionImpl(String name, String blockName, LayoutBlock layoutBlock) {
3261            super(name);
3262            this.blockName = blockName;
3263            this.layoutBlock = layoutBlock;
3264        }
3265
3266        @Override
3267        public void actionPerformed(ActionEvent e) {
3268            AbstractAction routeTableAction = new LayoutBlockRouteTableAction(blockName, layoutBlock);
3269            routeTableAction.actionPerformed(e);
3270        }
3271    }
3272
3273    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutTurnoutView.class);
3274}