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