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