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