001package jmri.jmrit.display.layoutEditor;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.*;
006import java.awt.event.*;
007import java.awt.geom.Point2D;
008import java.awt.geom.Rectangle2D;
009import java.beans.PropertyChangeEvent;
010import java.beans.PropertyVetoException;
011import java.io.File;
012import java.lang.reflect.Field;
013import java.text.MessageFormat;
014import java.util.List;
015import java.util.*;
016import java.util.concurrent.ConcurrentHashMap;
017import java.util.stream.Collectors;
018import java.util.stream.Stream;
019
020import javax.annotation.CheckForNull;
021import javax.annotation.Nonnull;
022import javax.imageio.ImageIO;
023import javax.swing.*;
024import javax.swing.event.PopupMenuEvent;
025import javax.swing.event.PopupMenuListener;
026import javax.swing.filechooser.FileNameExtensionFilter;
027
028import jmri.*;
029import jmri.configurexml.StoreXmlUserAction;
030import jmri.jmrit.catalog.NamedIcon;
031import jmri.jmrit.dispatcher.DispatcherAction;
032import jmri.jmrit.dispatcher.DispatcherFrame;
033import jmri.jmrit.display.*;
034import jmri.jmrit.display.layoutEditor.LayoutEditorDialogs.*;
035import jmri.jmrit.display.layoutEditor.LayoutEditorToolBarPanel.LocationFormat;
036import jmri.jmrit.display.panelEditor.PanelEditor;
037import jmri.jmrit.entryexit.AddEntryExitPairAction;
038import jmri.swing.NamedBeanComboBox;
039import jmri.util.*;
040import jmri.util.swing.JComboBoxUtil;
041import jmri.util.swing.JmriColorChooser;
042
043/**
044 * Provides a scrollable Layout Panel and editor toolbars (that can be hidden)
045 * <p>
046 * This module serves as a manager for the LayoutTurnout, Layout Block,
047 * PositionablePoint, Track Segment, LayoutSlip and LevelXing objects which are
048 * integral subparts of the LayoutEditor class.
049 * <p>
050 * All created objects are put on specific levels depending on their type
051 * (higher levels are in front): Note that higher numbers appear behind lower
052 * numbers.
053 * <p>
054 * The "contents" List keeps track of all text and icon label objects added to
055 * the target frame for later manipulation. Other Lists keep track of drawn
056 * items.
057 * <p>
058 * Based in part on PanelEditor.java (Bob Jacobsen (c) 2002, 2003). In
059 * particular, text and icon label items are copied from Panel editor, as well
060 * as some of the control design.
061 *
062 * @author Dave Duchamp Copyright: (c) 2004-2007
063 * @author George Warner Copyright: (c) 2017-2019
064 */
065final public class LayoutEditor extends PanelEditor implements MouseWheelListener, LayoutModels {
066
067    // Operational instance variables - not saved to disk
068    private JmriJFrame floatingEditToolBoxFrame = null;
069    private JScrollPane floatingEditContentScrollPane = null;
070    private JPanel floatEditHelpPanel = null;
071
072    private JPanel editToolBarContainerPanel = null;
073    private JScrollPane editToolBarScrollPane = null;
074
075    private JPanel helpBarPanel = null;
076    private final JPanel helpBar = new JPanel();
077
078    private final boolean editorUseOldLocSize;
079
080    private LayoutEditorToolBarPanel leToolBarPanel = null;
081
082    @Nonnull
083    public LayoutEditorToolBarPanel getLayoutEditorToolBarPanel() {
084        return leToolBarPanel;
085    }
086
087    // end of main panel controls
088    private boolean delayedPopupTrigger = false;
089    private Point2D currentPoint = new Point2D.Double(100.0, 100.0);
090    private Point2D dLoc = new Point2D.Double(0.0, 0.0);
091
092    private int toolbarHeight = 100;
093    private int toolbarWidth = 100;
094
095    private TrackSegment newTrack = null;
096    private boolean panelChanged = false;
097
098    // size of point boxes
099    public static final double SIZE = 3.0;
100    public static final double SIZE2 = SIZE * 2.; // must be twice SIZE
101
102    public Color turnoutCircleColor = Color.black; // matches earlier versions
103    public Color turnoutCircleThrownColor = Color.black;
104    private boolean turnoutFillControlCircles = false;
105    private int turnoutCircleSize = 4; // matches earlier versions
106
107    // use turnoutCircleSize when you need an int and these when you need a double
108    // note: these only change when setTurnoutCircleSize is called
109    // using these avoids having to call getTurnoutCircleSize() and
110    // the multiply (x2) and the int -> double conversion overhead
111    public double circleRadius = SIZE * getTurnoutCircleSize();
112    public double circleDiameter = 2.0 * circleRadius;
113
114    // selection variables
115    public boolean selectionActive = false;
116    private double selectionX = 0.0;
117    private double selectionY = 0.0;
118    public double selectionWidth = 0.0;
119    public double selectionHeight = 0.0;
120
121    // Option menu items
122    private JCheckBoxMenuItem editModeCheckBoxMenuItem = null;
123
124    private JRadioButtonMenuItem toolBarSideTopButton = null;
125    private JRadioButtonMenuItem toolBarSideLeftButton = null;
126    private JRadioButtonMenuItem toolBarSideBottomButton = null;
127    private JRadioButtonMenuItem toolBarSideRightButton = null;
128    private JRadioButtonMenuItem toolBarSideFloatButton = null;
129
130    private final JCheckBoxMenuItem wideToolBarCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("ToolBarWide"));
131
132    private JCheckBoxMenuItem positionableCheckBoxMenuItem = null;
133    private JCheckBoxMenuItem controlCheckBoxMenuItem = null;
134    private JCheckBoxMenuItem animationCheckBoxMenuItem = null;
135    private JCheckBoxMenuItem showHelpCheckBoxMenuItem = null;
136    private JCheckBoxMenuItem showGridCheckBoxMenuItem = null;
137    private JCheckBoxMenuItem autoAssignBlocksCheckBoxMenuItem = null;
138    private JMenu scrollMenu = null;
139    private JRadioButtonMenuItem scrollBothMenuItem = null;
140    private JRadioButtonMenuItem scrollNoneMenuItem = null;
141    private JRadioButtonMenuItem scrollHorizontalMenuItem = null;
142    private JRadioButtonMenuItem scrollVerticalMenuItem = null;
143    private JMenu tooltipMenu = null;
144    private JRadioButtonMenuItem tooltipAlwaysMenuItem = null;
145    private JRadioButtonMenuItem tooltipNoneMenuItem = null;
146    private JRadioButtonMenuItem tooltipInEditMenuItem = null;
147    private JRadioButtonMenuItem tooltipNotInEditMenuItem = null;
148
149    private JCheckBoxMenuItem pixelsCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("Pixels"));
150    private JCheckBoxMenuItem metricCMCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("MetricCM"));
151    private JCheckBoxMenuItem englishFeetInchesCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("EnglishFeetInches"));
152
153    private JCheckBoxMenuItem snapToGridOnAddCheckBoxMenuItem = null;
154    private JCheckBoxMenuItem snapToGridOnMoveCheckBoxMenuItem = null;
155    private JCheckBoxMenuItem antialiasingOnCheckBoxMenuItem = null;
156    private JCheckBoxMenuItem turnoutCirclesOnCheckBoxMenuItem = null;
157    private JCheckBoxMenuItem turnoutDrawUnselectedLegCheckBoxMenuItem = null;
158    private JCheckBoxMenuItem turnoutFillControlCirclesCheckBoxMenuItem = null;
159    private JCheckBoxMenuItem hideTrackSegmentConstructionLinesCheckBoxMenuItem = null;
160    private JCheckBoxMenuItem useDirectTurnoutControlCheckBoxMenuItem = null;
161    private ButtonGroup turnoutCircleSizeButtonGroup = null;
162
163    private boolean turnoutDrawUnselectedLeg = true;
164    private boolean autoAssignBlocks = false;
165
166    // Tools menu items
167    private final JMenu zoomMenu = new JMenu(Bundle.getMessage("MenuZoom"));
168    private final JRadioButtonMenuItem zoom025Item = new JRadioButtonMenuItem("x 0.25");
169    private final JRadioButtonMenuItem zoom05Item = new JRadioButtonMenuItem("x 0.5");
170    private final JRadioButtonMenuItem zoom075Item = new JRadioButtonMenuItem("x 0.75");
171    private final JRadioButtonMenuItem noZoomItem = new JRadioButtonMenuItem(Bundle.getMessage("NoZoom"));
172    private final JRadioButtonMenuItem zoom15Item = new JRadioButtonMenuItem("x 1.5");
173    private final JRadioButtonMenuItem zoom20Item = new JRadioButtonMenuItem("x 2.0");
174    private final JRadioButtonMenuItem zoom30Item = new JRadioButtonMenuItem("x 3.0");
175    private final JRadioButtonMenuItem zoom40Item = new JRadioButtonMenuItem("x 4.0");
176    private final JRadioButtonMenuItem zoom50Item = new JRadioButtonMenuItem("x 5.0");
177    private final JRadioButtonMenuItem zoom60Item = new JRadioButtonMenuItem("x 6.0");
178    private final JRadioButtonMenuItem zoom70Item = new JRadioButtonMenuItem("x 7.0");
179    private final JRadioButtonMenuItem zoom80Item = new JRadioButtonMenuItem("x 8.0");
180
181    private final JMenuItem undoTranslateSelectionMenuItem = new JMenuItem(Bundle.getMessage("UndoTranslateSelection"));
182    private final JMenuItem assignBlockToSelectionMenuItem = new JMenuItem(Bundle.getMessage("AssignBlockToSelectionTitle") + "...");
183
184    // Selected point information
185    private Point2D startDelta = new Point2D.Double(0.0, 0.0); // starting delta coordinates
186    public Object selectedObject = null;       // selected object, null if nothing selected
187    public Object prevSelectedObject = null;   // previous selected object, for undo
188    private HitPointType selectedHitPointType = HitPointType.NONE;         // hit point type within the selected object
189
190    public LayoutTrack foundTrack = null;      // found object, null if nothing found
191    public LayoutTrackView foundTrackView = null;                 // found view object, null if nothing found
192    private Point2D foundLocation = new Point2D.Double(0.0, 0.0); // location of found object
193    public HitPointType foundHitPointType = HitPointType.NONE;          // connection type within the found object
194
195    public LayoutTrack beginTrack = null;      // begin track segment connection object, null if none
196    public Point2D beginLocation = new Point2D.Double(0.0, 0.0); // location of begin object
197    private HitPointType beginHitPointType = HitPointType.NONE; // connection type within begin connection object
198
199    public Point2D currentLocation = new Point2D.Double(0.0, 0.0); // current location
200
201    // Lists of items that describe the Layout, and allow it to be drawn
202    // Each of the items must be saved to disk over sessions
203    private List<AnalogClock2Display> clocks = new ArrayList<>();           // fast clocks
204    private List<LocoIcon> markerImage = new ArrayList<>();                 // marker images
205    private List<MultiSensorIcon> multiSensors = new ArrayList<>();         // multi-sensor images
206    private List<PositionableLabel> backgroundImage = new ArrayList<>();    // background images
207    private List<PositionableLabel> labelImage = new ArrayList<>();         // positionable label images
208    private List<SensorIcon> sensorImage = new ArrayList<>();               // sensor images
209    private List<SignalHeadIcon> signalHeadImage = new ArrayList<>();       // signal head images
210
211    // PositionableLabel's
212    private List<BlockContentsIcon> blockContentsLabelList = new ArrayList<>(); // BlockContentsIcon Label List
213    private List<MemoryIcon> memoryLabelList = new ArrayList<>();               // Memory Label List
214    private List<SensorIcon> sensorList = new ArrayList<>();                    // Sensor Icons
215    private List<SignalHeadIcon> signalList = new ArrayList<>();                // Signal Head Icons
216    private List<SignalMastIcon> signalMastList = new ArrayList<>();            // Signal Mast Icons
217
218    public final LayoutEditorViewContext gContext = new LayoutEditorViewContext(); // public for now, as things work access changes
219
220    @Nonnull
221    public List<SensorIcon> getSensorList() {
222        return sensorList;
223    }
224
225    @Nonnull
226    public List<PositionableLabel> getLabelImageList()  {
227        return labelImage;
228    }
229
230    @Nonnull
231    public List<BlockContentsIcon> getBlockContentsLabelList() {
232        return blockContentsLabelList;
233    }
234
235    @Nonnull
236    public List<MemoryIcon> getMemoryLabelList() {
237        return memoryLabelList;
238    }
239
240    @Nonnull
241    public List<SignalHeadIcon> getSignalList() {
242        return signalList;
243    }
244
245    @Nonnull
246    public List<SignalMastIcon> getSignalMastList() {
247        return signalMastList;
248    }
249
250    private final List<LayoutShape> layoutShapes = new ArrayList<>();               // LayoutShap list
251
252    // counts used to determine unique internal names
253    private int numAnchors = 0;
254    private int numEndBumpers = 0;
255    private int numEdgeConnectors = 0;
256    private int numTrackSegments = 0;
257    private int numLevelXings = 0;
258    private int numLayoutSlips = 0;
259    private int numLayoutTurnouts = 0;
260    private int numLayoutTurntables = 0;
261    private int numShapes = 0;
262
263    private LayoutEditorFindItems finder = new LayoutEditorFindItems(this);
264
265    @Nonnull
266    public LayoutEditorFindItems getFinder() {
267        return finder;
268    }
269
270    private Color mainlineTrackColor = Color.DARK_GRAY;
271    private Color sidelineTrackColor = Color.DARK_GRAY;
272    public Color defaultTrackColor = Color.DARK_GRAY;
273    private Color defaultOccupiedTrackColor = Color.red;
274    private Color defaultAlternativeTrackColor = Color.white;
275    private Color defaultTextColor = Color.black;
276
277    private String layoutName = "";
278    private boolean animatingLayout = true;
279    private boolean showHelpBar = true;
280    private boolean drawGrid = true;
281
282    private boolean snapToGridOnAdd = false;
283    private boolean snapToGridOnMove = false;
284    private boolean snapToGridInvert = false;
285
286    public boolean antialiasingOn = false;
287    public boolean highlightSelectedBlockFlag = false;
288
289    public boolean turnoutCirclesWithoutEditMode = false;
290    private boolean tooltipsWithoutEditMode = false;
291    private boolean tooltipsInEditMode = true;
292
293    // turnout size parameters - saved with panel
294    private double turnoutBX = LayoutTurnout.turnoutBXDefault; // RH, LH, WYE
295    private double turnoutCX = LayoutTurnout.turnoutCXDefault;
296    private double turnoutWid = LayoutTurnout.turnoutWidDefault;
297    private double xOverLong = LayoutTurnout.xOverLongDefault; // DOUBLE_XOVER, RH_XOVER, LH_XOVER
298    private double xOverHWid = LayoutTurnout.xOverHWidDefault;
299    private double xOverShort = LayoutTurnout.xOverShortDefault;
300    private boolean useDirectTurnoutControl = false; // Uses Left click for closing points, Right click for throwing.
301
302    // saved state of options when panel was loaded or created
303    private boolean savedEditMode = true;
304    private boolean savedPositionable = true;
305    private boolean savedControlLayout = true;
306    private boolean savedAnimatingLayout = true;
307    private boolean savedShowHelpBar = true;
308
309    // zoom
310    private double minZoom = 0.25;
311    private final double maxZoom = 8.0;
312
313    // A hash to store string -> KeyEvent constants, used to set keyboard shortcuts per locale
314    private HashMap<String, Integer> stringsToVTCodes = new HashMap<>();
315
316    /*==============*\
317    |* Toolbar side *|
318    \*==============*/
319    private enum ToolBarSide {
320        eTOP("top"),
321        eLEFT("left"),
322        eBOTTOM("bottom"),
323        eRIGHT("right"),
324        eFLOAT("float");
325
326        private final String name;
327        private static final Map<String, ToolBarSide> ENUM_MAP;
328
329        ToolBarSide(String name) {
330            this.name = name;
331        }
332
333        // Build an immutable map of String name to enum pairs.
334        static {
335            Map<String, ToolBarSide> map = new ConcurrentHashMap<>();
336
337            for (ToolBarSide instance : ToolBarSide.values()) {
338                map.put(instance.getName(), instance);
339            }
340            ENUM_MAP = Collections.unmodifiableMap(map);
341        }
342
343        public static ToolBarSide getName(@CheckForNull String name) {
344            return ENUM_MAP.get(name);
345        }
346
347        public String getName() {
348            return name;
349        }
350    }
351
352    private ToolBarSide toolBarSide = ToolBarSide.eTOP;
353
354    public LayoutEditor() {
355        this("My Layout");
356    }
357
358    public LayoutEditor(@Nonnull String name) {
359        super(name);
360        setSaveSize(true);
361        layoutName = name;
362
363        editorUseOldLocSize = InstanceManager.getDefault(jmri.util.gui.GuiLafPreferencesManager.class).isEditorUseOldLocSize();
364
365        // initialise keycode map
366        initStringsToVTCodes();
367
368        setupToolBar();
369        setupMenuBar();
370
371        super.setDefaultToolTip(new ToolTip(null, 0, 0, new Font("SansSerif", Font.PLAIN, 12),
372                Color.black, new Color(215, 225, 255), Color.black));
373
374        // setup help bar
375        helpBar.setLayout(new BoxLayout(helpBar, BoxLayout.PAGE_AXIS));
376        JTextArea helpTextArea1 = new JTextArea(Bundle.getMessage("Help1"));
377        helpBar.add(helpTextArea1);
378        JTextArea helpTextArea2 = new JTextArea(Bundle.getMessage("Help2"));
379        helpBar.add(helpTextArea2);
380
381        String helpText3 = "";
382
383        switch (SystemType.getType()) {
384            case SystemType.MACOSX: {
385                helpText3 = Bundle.getMessage("Help3Mac");
386                break;
387            }
388
389            case SystemType.WINDOWS:
390            case SystemType.LINUX: {
391                helpText3 = Bundle.getMessage("Help3Win");
392                break;
393            }
394
395            default:
396                helpText3 = Bundle.getMessage("Help3");
397        }
398
399        JTextArea helpTextArea3 = new JTextArea(helpText3);
400        helpBar.add(helpTextArea3);
401
402        // set to full screen
403        Dimension screenDim = Toolkit.getDefaultToolkit().getScreenSize();
404        gContext.setWindowWidth(screenDim.width - 20);
405        gContext.setWindowHeight(screenDim.height - 120);
406
407        // Let Editor make target, and use this frame
408        super.setTargetPanel(null, null);
409        super.setTargetPanelSize(gContext.getWindowWidth(), gContext.getWindowHeight());
410        setSize(screenDim.width, screenDim.height);
411
412        // register the resulting panel for later configuration
413        InstanceManager.getOptionalDefault(ConfigureManager.class)
414                .ifPresent(cm -> cm.registerUser(this));
415
416        // confirm that panel hasn't already been loaded
417        if (!this.equals(InstanceManager.getDefault(EditorManager.class).get(name))) {
418            log.warn("File contains a panel with the same name ({}) as an existing panel", name);
419        }
420        setFocusable(true);
421        addKeyListener(this);
422        resetDirty();
423
424        // establish link to LayoutEditor Tools
425        auxTools = getLEAuxTools();
426
427        SwingUtilities.invokeLater(() -> {
428            // initialize preferences
429            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> {
430                String windowFrameRef = getWindowFrameRef();
431
432                Object prefsProp = prefsMgr.getProperty(windowFrameRef, "toolBarSide");
433                // log.debug("{}.toolBarSide is {}", windowFrameRef, prefsProp);
434                if (prefsProp != null) {
435                    ToolBarSide newToolBarSide = ToolBarSide.getName((String) prefsProp);
436                    setToolBarSide(newToolBarSide);
437                }
438
439                // Note: since prefs default to false and we want wide to be the default
440                // we invert it and save it as thin
441                boolean prefsToolBarIsWide = prefsMgr.getSimplePreferenceState(windowFrameRef + ".toolBarThin");
442
443                log.debug("{}.toolBarThin is {}", windowFrameRef, prefsProp);
444                setToolBarWide(prefsToolBarIsWide);
445
446                boolean prefsShowHelpBar = prefsMgr.getSimplePreferenceState(windowFrameRef + ".showHelpBar");
447                // log.debug("{}.showHelpBar is {}", windowFrameRef, prefsShowHelpBar);
448
449                setShowHelpBar(prefsShowHelpBar);
450
451                boolean prefsAntialiasingOn = prefsMgr.getSimplePreferenceState(windowFrameRef + ".antialiasingOn");
452                // log.debug("{}.antialiasingOn is {}", windowFrameRef, prefsAntialiasingOn);
453
454                setAntialiasingOn(prefsAntialiasingOn);
455
456                boolean prefsHighlightSelectedBlockFlag
457                        = prefsMgr.getSimplePreferenceState(windowFrameRef + ".highlightSelectedBlock");
458                // log.debug("{}.highlightSelectedBlock is {}", windowFrameRef, prefsHighlightSelectedBlockFlag);
459
460                setHighlightSelectedBlock(prefsHighlightSelectedBlockFlag);
461            }); // InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr)
462
463            // make sure that the layoutEditorComponent is in the _targetPanel components
464            List<Component> componentList = Arrays.asList(_targetPanel.getComponents());
465            if (!componentList.contains(layoutEditorComponent)) {
466                try {
467                    _targetPanel.remove(layoutEditorComponent);
468                    _targetPanel.add(layoutEditorComponent, Integer.valueOf(3));
469                    _targetPanel.moveToFront(layoutEditorComponent);
470                } catch (Exception e) {
471                    log.warn("paintTargetPanelBefore: ", e);
472                }
473            }
474        });
475    }
476
477    private void setupMenuBar() {
478        // initialize menu bar
479        JMenuBar menuBar = new JMenuBar();
480
481        // set up File menu
482        JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile"));
483        fileMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("MenuFileMnemonic")));
484        menuBar.add(fileMenu);
485        StoreXmlUserAction store = new StoreXmlUserAction(Bundle.getMessage("MenuItemStore"));
486        int primary_modifier = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
487        store.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(
488                stringsToVTCodes.get(Bundle.getMessage("MenuItemStoreAccelerator")), primary_modifier));
489        fileMenu.add(store);
490        fileMenu.addSeparator();
491
492        JMenuItem deleteItem = new JMenuItem(Bundle.getMessage("DeletePanel"));
493        fileMenu.add(deleteItem);
494        deleteItem.addActionListener((ActionEvent event) -> {
495            if (deletePanel()) {
496                dispose();
497            }
498        });
499        setJMenuBar(menuBar);
500
501        // setup Options menu
502        setupOptionMenu(menuBar);
503
504        // setup Tools menu
505        setupToolsMenu(menuBar);
506
507        // setup Zoom menu
508        setupZoomMenu(menuBar);
509
510        // setup marker menu
511        setupMarkerMenu(menuBar);
512
513        // Setup Dispatcher window
514        setupDispatcherMenu(menuBar);
515
516        // setup Help menu
517        addHelpMenu("package.jmri.jmrit.display.LayoutEditor", true);
518    }
519
520    @Override
521    public void newPanelDefaults() {
522        getLayoutTrackDrawingOptions().setMainRailWidth(2);
523        getLayoutTrackDrawingOptions().setSideRailWidth(1);
524        setBackgroundColor(defaultBackgroundColor);
525        JmriColorChooser.addRecentColor(defaultTrackColor);
526        JmriColorChooser.addRecentColor(defaultOccupiedTrackColor);
527        JmriColorChooser.addRecentColor(defaultAlternativeTrackColor);
528        JmriColorChooser.addRecentColor(defaultBackgroundColor);
529        JmriColorChooser.addRecentColor(defaultTextColor);
530    }
531
532    private final LayoutEditorComponent layoutEditorComponent = new LayoutEditorComponent(this);
533
534    private void setupToolBar() {
535        // Initial setup for both horizontal and vertical
536        Container contentPane = getContentPane();
537
538        // remove these (if present) so we can add them back (without duplicates)
539        if (editToolBarContainerPanel != null) {
540            editToolBarContainerPanel.setVisible(false);
541            contentPane.remove(editToolBarContainerPanel);
542        }
543
544        if (helpBarPanel != null) {
545            contentPane.remove(helpBarPanel);
546        }
547
548        deletefloatingEditToolBoxFrame();
549        if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
550            createfloatingEditToolBoxFrame();
551            createFloatingHelpPanel();
552            return;
553        }
554
555        Dimension screenDim = Toolkit.getDefaultToolkit().getScreenSize();
556        boolean toolBarIsVertical = (toolBarSide.equals(ToolBarSide.eRIGHT) || toolBarSide.equals(ToolBarSide.eLEFT));
557        if (toolBarIsVertical) {
558            leToolBarPanel = new LayoutEditorVerticalToolBarPanel(this);
559            editToolBarScrollPane = new JScrollPane(leToolBarPanel);
560            toolbarWidth = editToolBarScrollPane.getPreferredSize().width;
561            toolbarHeight = screenDim.height;
562        } else {
563            leToolBarPanel = new LayoutEditorHorizontalToolBarPanel(this);
564            editToolBarScrollPane = new JScrollPane(leToolBarPanel);
565            toolbarWidth = screenDim.width;
566            toolbarHeight = editToolBarScrollPane.getPreferredSize().height;
567        }
568
569        editToolBarContainerPanel = new JPanel();
570        editToolBarContainerPanel.setLayout(new BoxLayout(editToolBarContainerPanel, BoxLayout.PAGE_AXIS));
571        editToolBarContainerPanel.add(editToolBarScrollPane);
572
573        // setup notification for when horizontal scrollbar changes visibility
574        // editToolBarScroll.getViewport().addChangeListener(e -> {
575        // log.warn("scrollbars visible: " + editToolBarScroll.getHorizontalScrollBar().isVisible());
576        //});
577        editToolBarContainerPanel.setMinimumSize(new Dimension(toolbarWidth, toolbarHeight));
578        editToolBarContainerPanel.setPreferredSize(new Dimension(toolbarWidth, toolbarHeight));
579
580        helpBarPanel = new JPanel();
581        helpBarPanel.add(helpBar);
582
583        for (Component c : helpBar.getComponents()) {
584            if (c instanceof JTextArea) {
585                JTextArea j = (JTextArea) c;
586                j.setSize(new Dimension(toolbarWidth, j.getSize().height));
587                j.setLineWrap(toolBarIsVertical);
588                j.setWrapStyleWord(toolBarIsVertical);
589            }
590        }
591        contentPane.setLayout(new BoxLayout(contentPane, toolBarIsVertical ? BoxLayout.LINE_AXIS : BoxLayout.PAGE_AXIS));
592
593        switch (toolBarSide) {
594            case eTOP:
595            case eLEFT:
596                contentPane.add(editToolBarContainerPanel, 0);
597                break;
598
599            case eBOTTOM:
600            case eRIGHT:
601                contentPane.add(editToolBarContainerPanel);
602                break;
603
604            default:
605                // fall through
606                break;
607        }
608
609        if (toolBarIsVertical) {
610            editToolBarContainerPanel.add(helpBarPanel);
611        } else {
612            contentPane.add(helpBarPanel);
613        }
614        helpBarPanel.setVisible(isEditable() && getShowHelpBar());
615        editToolBarContainerPanel.setVisible(isEditable());
616    }
617
618    private void createfloatingEditToolBoxFrame() {
619        if (isEditable() && floatingEditToolBoxFrame == null) {
620            // Create a scroll pane to hold the window content.
621            leToolBarPanel = new LayoutEditorFloatingToolBarPanel(this);
622            floatingEditContentScrollPane = new JScrollPane(leToolBarPanel);
623            floatingEditContentScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
624            floatingEditContentScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
625            // Create the window and add the toolbox content
626            floatingEditToolBoxFrame = new JmriJFrame(Bundle.getMessage("ToolBox", getLayoutName()));
627            floatingEditToolBoxFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
628            floatingEditToolBoxFrame.setContentPane(floatingEditContentScrollPane);
629            floatingEditToolBoxFrame.pack();
630            floatingEditToolBoxFrame.setAlwaysOnTop(true);
631            floatingEditToolBoxFrame.setVisible(true);
632        }
633    }
634
635    private void deletefloatingEditToolBoxFrame() {
636        if (floatingEditContentScrollPane != null) {
637            floatingEditContentScrollPane.removeAll();
638            floatingEditContentScrollPane = null;
639        }
640        if (floatingEditToolBoxFrame != null) {
641            floatingEditToolBoxFrame.dispose();
642            floatingEditToolBoxFrame = null;
643        }
644    }
645
646    private void createFloatingHelpPanel() {
647
648        if (leToolBarPanel instanceof LayoutEditorFloatingToolBarPanel) {
649            LayoutEditorFloatingToolBarPanel leftbp = (LayoutEditorFloatingToolBarPanel) leToolBarPanel;
650            floatEditHelpPanel = new JPanel();
651            leToolBarPanel.add(floatEditHelpPanel);
652
653            // Notice: End tree structure indenting
654            // Force the help panel width to the same as the tabs section
655            int tabSectionWidth = (int) leftbp.getPreferredSize().getWidth();
656
657            // Change the textarea settings
658            for (Component c : helpBar.getComponents()) {
659                if (c instanceof JTextArea) {
660                    JTextArea j = (JTextArea) c;
661                    j.setSize(new Dimension(tabSectionWidth, j.getSize().height));
662                    j.setLineWrap(true);
663                    j.setWrapStyleWord(true);
664                }
665            }
666
667            // Change the width of the help panel section
668            floatEditHelpPanel.setMaximumSize(new Dimension(tabSectionWidth, Integer.MAX_VALUE));
669            floatEditHelpPanel.add(helpBar);
670            floatEditHelpPanel.setVisible(isEditable() && getShowHelpBar());
671        }
672    }
673
674    @Override
675    public void init(String name) {
676    }
677
678    @Override
679    public void initView() {
680        editModeCheckBoxMenuItem.setSelected(isEditable());
681
682        positionableCheckBoxMenuItem.setSelected(allPositionable());
683        controlCheckBoxMenuItem.setSelected(allControlling());
684
685        if (isEditable()) {
686            setAllShowToolTip(tooltipsInEditMode);
687        } else {
688            setAllShowToolTip(tooltipsWithoutEditMode);
689        }
690
691        scrollNoneMenuItem.setSelected(_scrollState == Editor.SCROLL_NONE);
692        scrollBothMenuItem.setSelected(_scrollState == Editor.SCROLL_BOTH);
693        scrollHorizontalMenuItem.setSelected(_scrollState == Editor.SCROLL_HORIZONTAL);
694        scrollVerticalMenuItem.setSelected(_scrollState == Editor.SCROLL_VERTICAL);
695    }
696
697    @Override
698    public void setSize(int w, int h) {
699        super.setSize(w, h);
700    }
701
702    @Override
703    public void targetWindowClosingEvent(WindowEvent e) {
704        boolean save = (isDirty() || (savedEditMode != isEditable())
705                || (savedPositionable != allPositionable())
706                || (savedControlLayout != allControlling())
707                || (savedAnimatingLayout != isAnimating())
708                || (savedShowHelpBar != getShowHelpBar()));
709
710        targetWindowClosing(save);
711    }
712
713    /**
714     * Set up NamedBeanComboBox
715     *
716     * @param inComboBox     the NamedBeanComboBox to set up
717     * @param inValidateMode true to validate typed inputs; false otherwise
718     * @param inEnable       boolean to enable / disable the NamedBeanComboBox
719     * @param inEditable     boolean to make the NamedBeanComboBox editable
720     */
721    public static void setupComboBox(@Nonnull NamedBeanComboBox<?> inComboBox, boolean inValidateMode, boolean inEnable, boolean inEditable) {
722        log.debug("LE setupComboBox called");
723        assert inComboBox != null;
724
725        inComboBox.setEnabled(inEnable);
726        inComboBox.setEditable(inEditable);
727        inComboBox.setValidatingInput(inValidateMode);
728        inComboBox.setSelectedIndex(-1);
729
730        // This has to be set before calling setupComboBoxMaxRows
731        // (otherwise if inFirstBlank then the  number of rows will be wrong)
732        inComboBox.setAllowNull(!inValidateMode);
733
734        // set the max number of rows that will fit onscreen
735        JComboBoxUtil.setupComboBoxMaxRows(inComboBox);
736
737        inComboBox.setSelectedIndex(-1);
738    }
739
740    /**
741     * Grabs a subset of the possible KeyEvent constants and puts them into a
742     * hash for fast lookups later. These lookups are used to enable bundles to
743     * specify keyboard shortcuts on a per-locale basis.
744     */
745    private void initStringsToVTCodes() {
746        Field[] fields = KeyEvent.class
747                .getFields();
748
749        for (Field field : fields) {
750            String name = field.getName();
751
752            if (name.startsWith("VK")) {
753                int code = 0;
754                try {
755                    code = field.getInt(null);
756                } catch (IllegalAccessException | IllegalArgumentException e) {
757                    // exceptions make me throw up...
758                }
759
760                String key = name.substring(3);
761
762                // log.debug("VTCode[{}]:'{}'", key, code);
763                stringsToVTCodes.put(key, code);
764            }
765        }
766    }
767
768    /**
769     * Set up the Option menu.
770     *
771     * @param menuBar to add the option menu to
772     * @return option menu that was added
773     */
774    private JMenu setupOptionMenu(@Nonnull JMenuBar menuBar) {
775        assert menuBar != null;
776
777        JMenu optionMenu = new JMenu(Bundle.getMessage("MenuOptions"));
778
779        optionMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("OptionsMnemonic")));
780        menuBar.add(optionMenu);
781
782        //
783        //  edit mode
784        //
785        editModeCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("EditMode"));
786        optionMenu.add(editModeCheckBoxMenuItem);
787        editModeCheckBoxMenuItem.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("EditModeMnemonic")));
788        int primary_modifier = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
789        editModeCheckBoxMenuItem.setAccelerator(KeyStroke.getKeyStroke(
790                stringsToVTCodes.get(Bundle.getMessage("EditModeAccelerator")), primary_modifier));
791        editModeCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
792            setAllEditable(editModeCheckBoxMenuItem.isSelected());
793
794            // show/hide the help bar
795            if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
796                if (floatEditHelpPanel != null) {
797                    floatEditHelpPanel.setVisible(isEditable() && getShowHelpBar());
798                }
799            } else {
800                helpBarPanel.setVisible(isEditable() && getShowHelpBar());
801            }
802
803            if (isEditable()) {
804                setAllShowToolTip(tooltipsInEditMode);
805
806                // redo using the "Extra" color to highlight the selected block
807                if (highlightSelectedBlockFlag) {
808                    if (!highlightBlockInComboBox(leToolBarPanel.blockIDComboBox)) {
809                        highlightBlockInComboBox(leToolBarPanel.blockContentsComboBox);
810                    }
811                }
812            } else {
813                setAllShowToolTip(tooltipsWithoutEditMode);
814
815                // undo using the "Extra" color to highlight the selected block
816                if (highlightSelectedBlockFlag) {
817                    highlightBlock(null);
818                }
819            }
820            awaitingIconChange = false;
821        });
822        editModeCheckBoxMenuItem.setSelected(isEditable());
823
824        //
825        // toolbar
826        //
827        JMenu toolBarMenu = new JMenu(Bundle.getMessage("ToolBar")); // used for ToolBar SubMenu
828        optionMenu.add(toolBarMenu);
829
830        JMenu toolBarSideMenu = new JMenu(Bundle.getMessage("ToolBarSide"));
831        ButtonGroup toolBarSideGroup = new ButtonGroup();
832
833        //
834        // create toolbar side menu items: (top, left, bottom, right)
835        //
836        toolBarSideTopButton = new JRadioButtonMenuItem(Bundle.getMessage("ToolBarSideTop"));
837        toolBarSideTopButton.addActionListener((ActionEvent event) -> setToolBarSide(ToolBarSide.eTOP));
838        toolBarSideTopButton.setSelected(toolBarSide.equals(ToolBarSide.eTOP));
839        toolBarSideMenu.add(toolBarSideTopButton);
840        toolBarSideGroup.add(toolBarSideTopButton);
841
842        toolBarSideLeftButton = new JRadioButtonMenuItem(Bundle.getMessage("ToolBarSideLeft"));
843        toolBarSideLeftButton.addActionListener((ActionEvent event) -> setToolBarSide(ToolBarSide.eLEFT));
844        toolBarSideLeftButton.setSelected(toolBarSide.equals(ToolBarSide.eLEFT));
845        toolBarSideMenu.add(toolBarSideLeftButton);
846        toolBarSideGroup.add(toolBarSideLeftButton);
847
848        toolBarSideBottomButton = new JRadioButtonMenuItem(Bundle.getMessage("ToolBarSideBottom"));
849        toolBarSideBottomButton.addActionListener((ActionEvent event) -> setToolBarSide(ToolBarSide.eBOTTOM));
850        toolBarSideBottomButton.setSelected(toolBarSide.equals(ToolBarSide.eBOTTOM));
851        toolBarSideMenu.add(toolBarSideBottomButton);
852        toolBarSideGroup.add(toolBarSideBottomButton);
853
854        toolBarSideRightButton = new JRadioButtonMenuItem(Bundle.getMessage("ToolBarSideRight"));
855        toolBarSideRightButton.addActionListener((ActionEvent event) -> setToolBarSide(ToolBarSide.eRIGHT));
856        toolBarSideRightButton.setSelected(toolBarSide.equals(ToolBarSide.eRIGHT));
857        toolBarSideMenu.add(toolBarSideRightButton);
858        toolBarSideGroup.add(toolBarSideRightButton);
859
860        toolBarSideFloatButton = new JRadioButtonMenuItem(Bundle.getMessage("ToolBarSideFloat"));
861        toolBarSideFloatButton.addActionListener((ActionEvent event) -> setToolBarSide(ToolBarSide.eFLOAT));
862        toolBarSideFloatButton.setSelected(toolBarSide.equals(ToolBarSide.eFLOAT));
863        toolBarSideMenu.add(toolBarSideFloatButton);
864        toolBarSideGroup.add(toolBarSideFloatButton);
865
866        toolBarMenu.add(toolBarSideMenu);
867
868        //
869        // toolbar wide menu
870        //
871        toolBarMenu.add(wideToolBarCheckBoxMenuItem);
872        wideToolBarCheckBoxMenuItem.addActionListener((ActionEvent event) -> setToolBarWide(wideToolBarCheckBoxMenuItem.isSelected()));
873        wideToolBarCheckBoxMenuItem.setSelected(leToolBarPanel.toolBarIsWide);
874        wideToolBarCheckBoxMenuItem.setEnabled(toolBarSide.equals(ToolBarSide.eTOP) || toolBarSide.equals(ToolBarSide.eBOTTOM));
875
876        //
877        // Scroll Bars
878        //
879        scrollMenu = new JMenu(Bundle.getMessage("ComboBoxScrollable")); // used for ScrollBarsSubMenu
880        optionMenu.add(scrollMenu);
881        ButtonGroup scrollGroup = new ButtonGroup();
882        scrollBothMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("ScrollBoth"));
883        scrollGroup.add(scrollBothMenuItem);
884        scrollMenu.add(scrollBothMenuItem);
885        scrollBothMenuItem.setSelected(_scrollState == Editor.SCROLL_BOTH);
886        scrollBothMenuItem.addActionListener((ActionEvent event) -> {
887            _scrollState = Editor.SCROLL_BOTH;
888            setScroll(_scrollState);
889            redrawPanel();
890        });
891        scrollNoneMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("ScrollNone"));
892        scrollGroup.add(scrollNoneMenuItem);
893        scrollMenu.add(scrollNoneMenuItem);
894        scrollNoneMenuItem.setSelected(_scrollState == Editor.SCROLL_NONE);
895        scrollNoneMenuItem.addActionListener((ActionEvent event) -> {
896            _scrollState = Editor.SCROLL_NONE;
897            setScroll(_scrollState);
898            redrawPanel();
899        });
900        scrollHorizontalMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("ScrollHorizontal"));
901        scrollGroup.add(scrollHorizontalMenuItem);
902        scrollMenu.add(scrollHorizontalMenuItem);
903        scrollHorizontalMenuItem.setSelected(_scrollState == Editor.SCROLL_HORIZONTAL);
904        scrollHorizontalMenuItem.addActionListener((ActionEvent event) -> {
905            _scrollState = Editor.SCROLL_HORIZONTAL;
906            setScroll(_scrollState);
907            redrawPanel();
908        });
909        scrollVerticalMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("ScrollVertical"));
910        scrollGroup.add(scrollVerticalMenuItem);
911        scrollMenu.add(scrollVerticalMenuItem);
912        scrollVerticalMenuItem.setSelected(_scrollState == Editor.SCROLL_VERTICAL);
913        scrollVerticalMenuItem.addActionListener((ActionEvent event) -> {
914            _scrollState = Editor.SCROLL_VERTICAL;
915            setScroll(_scrollState);
916            redrawPanel();
917        });
918
919        //
920        // Tooltips
921        //
922        tooltipMenu = new JMenu(Bundle.getMessage("TooltipSubMenu"));
923        optionMenu.add(tooltipMenu);
924        ButtonGroup tooltipGroup = new ButtonGroup();
925        tooltipNoneMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("TooltipNone"));
926        tooltipGroup.add(tooltipNoneMenuItem);
927        tooltipMenu.add(tooltipNoneMenuItem);
928        tooltipNoneMenuItem.setSelected((!tooltipsInEditMode) && (!tooltipsWithoutEditMode));
929        tooltipNoneMenuItem.addActionListener((ActionEvent event) -> {
930            tooltipsInEditMode = false;
931            tooltipsWithoutEditMode = false;
932            setAllShowToolTip(false);
933        });
934        tooltipAlwaysMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("TooltipAlways"));
935        tooltipGroup.add(tooltipAlwaysMenuItem);
936        tooltipMenu.add(tooltipAlwaysMenuItem);
937        tooltipAlwaysMenuItem.setSelected((tooltipsInEditMode) && (tooltipsWithoutEditMode));
938        tooltipAlwaysMenuItem.addActionListener((ActionEvent event) -> {
939            tooltipsInEditMode = true;
940            tooltipsWithoutEditMode = true;
941            setAllShowToolTip(true);
942        });
943        tooltipInEditMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("TooltipEdit"));
944        tooltipGroup.add(tooltipInEditMenuItem);
945        tooltipMenu.add(tooltipInEditMenuItem);
946        tooltipInEditMenuItem.setSelected((tooltipsInEditMode) && (!tooltipsWithoutEditMode));
947        tooltipInEditMenuItem.addActionListener((ActionEvent event) -> {
948            tooltipsInEditMode = true;
949            tooltipsWithoutEditMode = false;
950            setAllShowToolTip(isEditable());
951        });
952        tooltipNotInEditMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("TooltipNotEdit"));
953        tooltipGroup.add(tooltipNotInEditMenuItem);
954        tooltipMenu.add(tooltipNotInEditMenuItem);
955        tooltipNotInEditMenuItem.setSelected((!tooltipsInEditMode) && (tooltipsWithoutEditMode));
956        tooltipNotInEditMenuItem.addActionListener((ActionEvent event) -> {
957            tooltipsInEditMode = false;
958            tooltipsWithoutEditMode = true;
959            setAllShowToolTip(!isEditable());
960        });
961
962        //
963        // show edit help
964        //
965        showHelpCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("ShowEditHelp"));
966        optionMenu.add(showHelpCheckBoxMenuItem);
967        showHelpCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
968            boolean newShowHelpBar = showHelpCheckBoxMenuItem.isSelected();
969            setShowHelpBar(newShowHelpBar);
970        });
971        showHelpCheckBoxMenuItem.setSelected(getShowHelpBar());
972
973        //
974        // Allow Repositioning
975        //
976        positionableCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("AllowRepositioning"));
977        optionMenu.add(positionableCheckBoxMenuItem);
978        positionableCheckBoxMenuItem.addActionListener((ActionEvent event) -> setAllPositionable(positionableCheckBoxMenuItem.isSelected()));
979        positionableCheckBoxMenuItem.setSelected(allPositionable());
980
981        //
982        // Allow Layout Control
983        //
984        controlCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("AllowLayoutControl"));
985        optionMenu.add(controlCheckBoxMenuItem);
986        controlCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
987            setAllControlling(controlCheckBoxMenuItem.isSelected());
988            redrawPanel();
989        });
990        controlCheckBoxMenuItem.setSelected(allControlling());
991
992        //
993        // use direct turnout control
994        //
995        useDirectTurnoutControlCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("UseDirectTurnoutControl")); // NOI18N
996        optionMenu.add(useDirectTurnoutControlCheckBoxMenuItem);
997        useDirectTurnoutControlCheckBoxMenuItem.addActionListener((ActionEvent event) -> setDirectTurnoutControl(useDirectTurnoutControlCheckBoxMenuItem.isSelected()));
998        useDirectTurnoutControlCheckBoxMenuItem.setSelected(useDirectTurnoutControl);
999
1000        //
1001        // antialiasing
1002        //
1003        antialiasingOnCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("AntialiasingOn"));
1004        optionMenu.add(antialiasingOnCheckBoxMenuItem);
1005        antialiasingOnCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1006            antialiasingOn = antialiasingOnCheckBoxMenuItem.isSelected();
1007            redrawPanel();
1008        });
1009        antialiasingOnCheckBoxMenuItem.setSelected(antialiasingOn);
1010
1011        //
1012        // edit title
1013        //
1014        optionMenu.addSeparator();
1015        JMenuItem titleItem = new JMenuItem(Bundle.getMessage("EditTitle") + "...");
1016        optionMenu.add(titleItem);
1017        titleItem.addActionListener((ActionEvent event) -> {
1018            // prompt for name
1019            String newName = (String) JOptionPane.showInputDialog(getTargetFrame(),
1020                    Bundle.getMessage("MakeLabel", Bundle.getMessage("EnterTitle")),
1021                    Bundle.getMessage("EditTitleMessageTitle"),
1022                    JOptionPane.PLAIN_MESSAGE, null, null, getLayoutName());
1023
1024            if (newName != null) {
1025                if (!newName.equals(getLayoutName())) {
1026                    if (InstanceManager.getDefault(EditorManager.class).contains(newName)) {
1027                        JOptionPane.showMessageDialog(
1028                                null, Bundle.getMessage("CanNotRename"), Bundle.getMessage("PanelExist"),
1029                                JOptionPane.ERROR_MESSAGE);
1030                    } else {
1031                        setTitle(newName);
1032                        setLayoutName(newName);
1033                        getLayoutTrackDrawingOptions().setName(newName);
1034                        setDirty();
1035
1036                        if (toolBarSide.equals(ToolBarSide.eFLOAT) && isEditable()) {
1037                            // Rebuild the toolbox after a name change.
1038                            deletefloatingEditToolBoxFrame();
1039                            createfloatingEditToolBoxFrame();
1040                            createFloatingHelpPanel();
1041                        }
1042                    }
1043                }
1044            }
1045        });
1046
1047        //
1048        // set background color
1049        //
1050        JMenuItem backgroundColorMenuItem = new JMenuItem(Bundle.getMessage("SetBackgroundColor", "..."));
1051        optionMenu.add(backgroundColorMenuItem);
1052        backgroundColorMenuItem.addActionListener((ActionEvent event) -> {
1053            Color desiredColor = JmriColorChooser.showDialog(this,
1054                    Bundle.getMessage("SetBackgroundColor", ""),
1055                    defaultBackgroundColor);
1056            if (desiredColor != null && !defaultBackgroundColor.equals(desiredColor)) {
1057                defaultBackgroundColor = desiredColor;
1058                setBackgroundColor(desiredColor);
1059                setDirty();
1060                redrawPanel();
1061            }
1062        });
1063
1064        //
1065        // set default text color
1066        //
1067        JMenuItem textColorMenuItem = new JMenuItem(Bundle.getMessage("DefaultTextColor", "..."));
1068        optionMenu.add(textColorMenuItem);
1069        textColorMenuItem.addActionListener((ActionEvent event) -> {
1070            Color desiredColor = JmriColorChooser.showDialog(this,
1071                    Bundle.getMessage("DefaultTextColor", ""),
1072                    defaultTextColor);
1073            if (desiredColor != null && !defaultTextColor.equals(desiredColor)) {
1074                setDefaultTextColor(desiredColor);
1075                setDirty();
1076                redrawPanel();
1077            }
1078        });
1079
1080        if (editorUseOldLocSize) {
1081            //
1082            //  save location and size
1083            //
1084            JMenuItem locationItem = new JMenuItem(Bundle.getMessage("SetLocation"));
1085            optionMenu.add(locationItem);
1086            locationItem.addActionListener((ActionEvent event) -> {
1087                setCurrentPositionAndSize();
1088                log.debug("Bounds:{}, {}, {}, {}, {}, {}",
1089                        gContext.getUpperLeftX(), gContext.getUpperLeftY(),
1090                        gContext.getWindowWidth(), gContext.getWindowHeight(),
1091                        gContext.getLayoutWidth(), gContext.getLayoutHeight());
1092            });
1093        }
1094
1095        //
1096        // Add Options
1097        //
1098        JMenu optionsAddMenu = new JMenu(Bundle.getMessage("AddMenuTitle"));
1099        optionMenu.add(optionsAddMenu);
1100
1101        // add background image
1102        JMenuItem backgroundItem = new JMenuItem(Bundle.getMessage("AddBackground") + "...");
1103        optionsAddMenu.add(backgroundItem);
1104        backgroundItem.addActionListener((ActionEvent event) -> {
1105            addBackground();
1106            // note: panel resized in addBackground
1107            setDirty();
1108            redrawPanel();
1109        });
1110
1111        // add fast clock
1112        JMenuItem clockItem = new JMenuItem(Bundle.getMessage("AddItem", Bundle.getMessage("FastClock")));
1113        optionsAddMenu.add(clockItem);
1114        clockItem.addActionListener((ActionEvent event) -> {
1115            AnalogClock2Display c = addClock();
1116            unionToPanelBounds(c.getBounds());
1117            setDirty();
1118            redrawPanel();
1119        });
1120
1121        // add turntable
1122        JMenuItem turntableItem = new JMenuItem(Bundle.getMessage("AddTurntable"));
1123        optionsAddMenu.add(turntableItem);
1124        turntableItem.addActionListener((ActionEvent event) -> {
1125            Point2D pt = windowCenter();
1126            if (selectionActive) {
1127                pt = MathUtil.midPoint(getSelectionRect());
1128            }
1129            addTurntable(pt);
1130            // note: panel resized in addTurntable
1131            setDirty();
1132            redrawPanel();
1133        });
1134
1135        // add reporter
1136        JMenuItem reporterItem = new JMenuItem(Bundle.getMessage("AddReporter") + "...");
1137        optionsAddMenu.add(reporterItem);
1138        reporterItem.addActionListener((ActionEvent event) -> {
1139            Point2D pt = windowCenter();
1140            if (selectionActive) {
1141                pt = MathUtil.midPoint(getSelectionRect());
1142            }
1143            EnterReporterDialog d = new EnterReporterDialog(this);
1144            d.enterReporter((int) pt.getX(), (int) pt.getY());
1145            // note: panel resized in enterReporter
1146            setDirty();
1147            redrawPanel();
1148        });
1149
1150        //
1151        // location coordinates format menu
1152        //
1153        JMenu locationMenu = new JMenu(Bundle.getMessage("LocationMenuTitle")); // used for location format SubMenu
1154        optionMenu.add(locationMenu);
1155
1156        InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> {
1157            String windowFrameRef = getWindowFrameRef();
1158            Object prefsProp = prefsMgr.getProperty(windowFrameRef, "LocationFormat");
1159            // log.debug("{}.LocationFormat is {}", windowFrameRef, prefsProp);
1160            if (prefsProp != null) {
1161                getLayoutEditorToolBarPanel().setLocationFormat(LocationFormat.valueOf((String) prefsProp));
1162            }
1163        });
1164
1165        // pixels (jmri classic)
1166        locationMenu.add(pixelsCheckBoxMenuItem);
1167        pixelsCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1168            getLayoutEditorToolBarPanel().setLocationFormat(LocationFormat.ePIXELS);
1169            selectLocationFormatCheckBoxMenuItem();
1170            redrawPanel();
1171        });
1172
1173        // metric cm's
1174        locationMenu.add(metricCMCheckBoxMenuItem);
1175        metricCMCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1176            getLayoutEditorToolBarPanel().setLocationFormat(LocationFormat.eMETRIC_CM);
1177            selectLocationFormatCheckBoxMenuItem();
1178            redrawPanel();
1179        });
1180
1181        // english feet/inches/16th's
1182        locationMenu.add(englishFeetInchesCheckBoxMenuItem);
1183        englishFeetInchesCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1184            getLayoutEditorToolBarPanel().setLocationFormat(LocationFormat.eENGLISH_FEET_INCHES);
1185            selectLocationFormatCheckBoxMenuItem();
1186            redrawPanel();
1187        });
1188        selectLocationFormatCheckBoxMenuItem();
1189
1190        //
1191        // grid menu
1192        //
1193        JMenu gridMenu = new JMenu(Bundle.getMessage("GridMenuTitle")); // used for Grid SubMenu
1194        optionMenu.add(gridMenu);
1195
1196        // show grid
1197        showGridCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("ShowEditGrid"));
1198        showGridCheckBoxMenuItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(
1199                Bundle.getMessage("ShowEditGridAccelerator")), primary_modifier));
1200        gridMenu.add(showGridCheckBoxMenuItem);
1201        showGridCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1202            drawGrid = showGridCheckBoxMenuItem.isSelected();
1203            redrawPanel();
1204        });
1205        showGridCheckBoxMenuItem.setSelected(getDrawGrid());
1206
1207        // snap to grid on add
1208        snapToGridOnAddCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("SnapToGridOnAdd"));
1209        snapToGridOnAddCheckBoxMenuItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(
1210                Bundle.getMessage("SnapToGridOnAddAccelerator")),
1211                primary_modifier | ActionEvent.SHIFT_MASK));
1212        gridMenu.add(snapToGridOnAddCheckBoxMenuItem);
1213        snapToGridOnAddCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1214            snapToGridOnAdd = snapToGridOnAddCheckBoxMenuItem.isSelected();
1215            redrawPanel();
1216        });
1217        snapToGridOnAddCheckBoxMenuItem.setSelected(snapToGridOnAdd);
1218
1219        // snap to grid on move
1220        snapToGridOnMoveCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("SnapToGridOnMove"));
1221        snapToGridOnMoveCheckBoxMenuItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(
1222                Bundle.getMessage("SnapToGridOnMoveAccelerator")),
1223                primary_modifier | ActionEvent.SHIFT_MASK));
1224        gridMenu.add(snapToGridOnMoveCheckBoxMenuItem);
1225        snapToGridOnMoveCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1226            snapToGridOnMove = snapToGridOnMoveCheckBoxMenuItem.isSelected();
1227            redrawPanel();
1228        });
1229        snapToGridOnMoveCheckBoxMenuItem.setSelected(snapToGridOnMove);
1230
1231        // specify grid square size
1232        JMenuItem gridSizeItem = new JMenuItem(Bundle.getMessage("SetGridSizes") + "...");
1233        gridMenu.add(gridSizeItem);
1234        gridSizeItem.addActionListener((ActionEvent event) -> {
1235            EnterGridSizesDialog d = new EnterGridSizesDialog(this);
1236            d.enterGridSizes();
1237        });
1238
1239        //
1240        // track menu
1241        //
1242        JMenu trackMenu = new JMenu(Bundle.getMessage("TrackMenuTitle"));
1243        optionMenu.add(trackMenu);
1244
1245        // set track drawing options menu item
1246        JMenuItem jmi = new JMenuItem(Bundle.getMessage("SetTrackDrawingOptions"));
1247        trackMenu.add(jmi);
1248        jmi.setToolTipText(Bundle.getMessage("SetTrackDrawingOptionsToolTip"));
1249        jmi.addActionListener((ActionEvent event) -> {
1250            LayoutTrackDrawingOptionsDialog ltdod
1251                    = new LayoutTrackDrawingOptionsDialog(
1252                            this, true, getLayoutTrackDrawingOptions());
1253            ltdod.setVisible(true);
1254        });
1255
1256        // track colors item menu item
1257        JMenu trkColourMenu = new JMenu(Bundle.getMessage("TrackColorSubMenu"));
1258        trackMenu.add(trkColourMenu);
1259
1260        JMenuItem trackColorMenuItem = new JMenuItem(Bundle.getMessage("DefaultTrackColor"));
1261        trkColourMenu.add(trackColorMenuItem);
1262        trackColorMenuItem.addActionListener((ActionEvent event) -> {
1263            Color desiredColor = JmriColorChooser.showDialog(this,
1264                    Bundle.getMessage("DefaultTrackColor"),
1265                    defaultTrackColor);
1266            if (desiredColor != null && !defaultTrackColor.equals(desiredColor)) {
1267                setDefaultTrackColor(desiredColor);
1268                setDirty();
1269                redrawPanel();
1270            }
1271        });
1272
1273        JMenuItem trackOccupiedColorMenuItem = new JMenuItem(Bundle.getMessage("DefaultOccupiedTrackColor"));
1274        trkColourMenu.add(trackOccupiedColorMenuItem);
1275        trackOccupiedColorMenuItem.addActionListener((ActionEvent event) -> {
1276            Color desiredColor = JmriColorChooser.showDialog(this,
1277                    Bundle.getMessage("DefaultOccupiedTrackColor"),
1278                    defaultOccupiedTrackColor);
1279            if (desiredColor != null && !defaultOccupiedTrackColor.equals(desiredColor)) {
1280                setDefaultOccupiedTrackColor(desiredColor);
1281                setDirty();
1282                redrawPanel();
1283            }
1284        });
1285
1286        JMenuItem trackAlternativeColorMenuItem = new JMenuItem(Bundle.getMessage("DefaultAlternativeTrackColor"));
1287        trkColourMenu.add(trackAlternativeColorMenuItem);
1288        trackAlternativeColorMenuItem.addActionListener((ActionEvent event) -> {
1289            Color desiredColor = JmriColorChooser.showDialog(this,
1290                    Bundle.getMessage("DefaultAlternativeTrackColor"),
1291                    defaultAlternativeTrackColor);
1292            if (desiredColor != null && !defaultAlternativeTrackColor.equals(desiredColor)) {
1293                setDefaultAlternativeTrackColor(desiredColor);
1294                setDirty();
1295                redrawPanel();
1296            }
1297        });
1298
1299        // Set All Tracks To Default Colors
1300        JMenuItem setAllTracksToDefaultColorsMenuItem = new JMenuItem(Bundle.getMessage("SetAllTracksToDefaultColors"));
1301        trkColourMenu.add(setAllTracksToDefaultColorsMenuItem);
1302        setAllTracksToDefaultColorsMenuItem.addActionListener((ActionEvent event) -> {
1303            if (setAllTracksToDefaultColors() > 0) {
1304                setDirty();
1305                redrawPanel();
1306            }
1307        });
1308
1309        // Automatically Assign Blocks to Track
1310        autoAssignBlocksCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("AutoAssignBlock"));
1311        trackMenu.add(autoAssignBlocksCheckBoxMenuItem);
1312        autoAssignBlocksCheckBoxMenuItem.addActionListener((ActionEvent event) -> autoAssignBlocks = autoAssignBlocksCheckBoxMenuItem.isSelected());
1313        autoAssignBlocksCheckBoxMenuItem.setSelected(autoAssignBlocks);
1314
1315        // add hideTrackSegmentConstructionLines menu item
1316        hideTrackSegmentConstructionLinesCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("HideTrackConLines"));
1317        trackMenu.add(hideTrackSegmentConstructionLinesCheckBoxMenuItem);
1318        hideTrackSegmentConstructionLinesCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1319            int show = TrackSegment.SHOWCON;
1320
1321            if (hideTrackSegmentConstructionLinesCheckBoxMenuItem.isSelected()) {
1322                show = TrackSegment.HIDECONALL;
1323            }
1324
1325            for (TrackSegment ts : getTrackSegments()) {
1326                ts.hideConstructionLines(show);
1327            }
1328            redrawPanel();
1329        });
1330        hideTrackSegmentConstructionLinesCheckBoxMenuItem.setSelected(autoAssignBlocks);
1331
1332        //
1333        // add turnout options submenu
1334        //
1335        JMenu turnoutOptionsMenu = new JMenu(Bundle.getMessage("TurnoutOptions"));
1336        optionMenu.add(turnoutOptionsMenu);
1337
1338        // animation item
1339        animationCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("AllowTurnoutAnimation"));
1340        turnoutOptionsMenu.add(animationCheckBoxMenuItem);
1341        animationCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1342            boolean mode = animationCheckBoxMenuItem.isSelected();
1343            setTurnoutAnimation(mode);
1344        });
1345        animationCheckBoxMenuItem.setSelected(true);
1346
1347        // circle on Turnouts
1348        turnoutCirclesOnCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("TurnoutCirclesOn"));
1349        turnoutOptionsMenu.add(turnoutCirclesOnCheckBoxMenuItem);
1350        turnoutCirclesOnCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1351            turnoutCirclesWithoutEditMode = turnoutCirclesOnCheckBoxMenuItem.isSelected();
1352            redrawPanel();
1353        });
1354        turnoutCirclesOnCheckBoxMenuItem.setSelected(turnoutCirclesWithoutEditMode);
1355
1356        // select turnout circle color
1357        JMenuItem turnoutCircleColorMenuItem = new JMenuItem(Bundle.getMessage("TurnoutCircleColor"));
1358        turnoutCircleColorMenuItem.addActionListener((ActionEvent event) -> {
1359            Color desiredColor = JmriColorChooser.showDialog(this,
1360                    Bundle.getMessage("TurnoutCircleColor"),
1361                    turnoutCircleColor);
1362            if (desiredColor != null && !turnoutCircleColor.equals(desiredColor)) {
1363                setTurnoutCircleColor(desiredColor);
1364                setDirty();
1365                redrawPanel();
1366            }
1367        });
1368        turnoutOptionsMenu.add(turnoutCircleColorMenuItem);
1369
1370        // select turnout circle thrown color
1371        JMenuItem turnoutCircleThrownColorMenuItem = new JMenuItem(Bundle.getMessage("TurnoutCircleThrownColor"));
1372        turnoutCircleThrownColorMenuItem.addActionListener((ActionEvent event) -> {
1373            Color desiredColor = JmriColorChooser.showDialog(this,
1374                    Bundle.getMessage("TurnoutCircleThrownColor"),
1375                    turnoutCircleThrownColor);
1376            if (desiredColor != null && !turnoutCircleThrownColor.equals(desiredColor)) {
1377                setTurnoutCircleThrownColor(desiredColor);
1378                setDirty();
1379                redrawPanel();
1380            }
1381        });
1382        turnoutOptionsMenu.add(turnoutCircleThrownColorMenuItem);
1383
1384        turnoutFillControlCirclesCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("TurnoutFillControlCircles"));
1385        turnoutOptionsMenu.add(turnoutFillControlCirclesCheckBoxMenuItem);
1386        turnoutFillControlCirclesCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1387            turnoutFillControlCircles = turnoutFillControlCirclesCheckBoxMenuItem.isSelected();
1388            redrawPanel();
1389        });
1390        turnoutFillControlCirclesCheckBoxMenuItem.setSelected(turnoutFillControlCircles);
1391
1392        // select turnout circle size
1393        JMenu turnoutCircleSizeMenu = new JMenu(Bundle.getMessage("TurnoutCircleSize"));
1394        turnoutCircleSizeButtonGroup = new ButtonGroup();
1395        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "1", 1);
1396        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "2", 2);
1397        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "3", 3);
1398        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "4", 4);
1399        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "5", 5);
1400        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "6", 6);
1401        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "7", 7);
1402        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "8", 8);
1403        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "9", 9);
1404        addTurnoutCircleSizeMenuEntry(turnoutCircleSizeMenu, "10", 10);
1405        turnoutOptionsMenu.add(turnoutCircleSizeMenu);
1406
1407        // add "enable drawing of unselected leg " menu item (helps when diverging angle is small)
1408        turnoutDrawUnselectedLegCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("TurnoutDrawUnselectedLeg"));
1409        turnoutOptionsMenu.add(turnoutDrawUnselectedLegCheckBoxMenuItem);
1410        turnoutDrawUnselectedLegCheckBoxMenuItem.addActionListener((ActionEvent event) -> {
1411            turnoutDrawUnselectedLeg = turnoutDrawUnselectedLegCheckBoxMenuItem.isSelected();
1412            redrawPanel();
1413        });
1414        turnoutDrawUnselectedLegCheckBoxMenuItem.setSelected(turnoutDrawUnselectedLeg);
1415
1416        return optionMenu;
1417    }
1418
1419    private void selectLocationFormatCheckBoxMenuItem() {
1420        pixelsCheckBoxMenuItem.setSelected(getLayoutEditorToolBarPanel().getLocationFormat() == LocationFormat.ePIXELS);
1421        metricCMCheckBoxMenuItem.setSelected(getLayoutEditorToolBarPanel().getLocationFormat() == LocationFormat.eMETRIC_CM);
1422        englishFeetInchesCheckBoxMenuItem.setSelected(getLayoutEditorToolBarPanel().getLocationFormat() == LocationFormat.eENGLISH_FEET_INCHES);
1423    }
1424
1425    /*============================================*\
1426    |* LayoutTrackDrawingOptions accessor methods *|
1427    \*============================================*/
1428    private LayoutTrackDrawingOptions layoutTrackDrawingOptions = null;
1429
1430    /**
1431     *
1432     * Getter Layout Track Drawing Options. since 4.15.6 split variable
1433     * defaultTrackColor and mainlineTrackColor/sidelineTrackColor <br>
1434     * blockDefaultColor, blockOccupiedColor and blockAlternativeColor added to
1435     * LayoutTrackDrawingOptions <br>
1436     *
1437     * @return LayoutTrackDrawingOptions object
1438     */
1439    @Nonnull
1440    public LayoutTrackDrawingOptions getLayoutTrackDrawingOptions() {
1441        if (layoutTrackDrawingOptions == null) {
1442            layoutTrackDrawingOptions = new LayoutTrackDrawingOptions(getLayoutName());
1443            // integrate LayoutEditor drawing options with previous drawing options
1444            layoutTrackDrawingOptions.setMainBlockLineWidth(gContext.getMainlineTrackWidth());
1445            layoutTrackDrawingOptions.setSideBlockLineWidth(gContext.getSidelineTrackWidth());
1446            layoutTrackDrawingOptions.setMainRailWidth(gContext.getMainlineTrackWidth());
1447            layoutTrackDrawingOptions.setSideRailWidth(gContext.getSidelineTrackWidth());
1448            layoutTrackDrawingOptions.setMainRailColor(mainlineTrackColor);
1449            layoutTrackDrawingOptions.setSideRailColor(sidelineTrackColor);
1450            layoutTrackDrawingOptions.setBlockDefaultColor(defaultTrackColor);
1451            layoutTrackDrawingOptions.setBlockOccupiedColor(defaultOccupiedTrackColor);
1452            layoutTrackDrawingOptions.setBlockAlternativeColor(defaultAlternativeTrackColor);
1453        }
1454        return layoutTrackDrawingOptions;
1455    }
1456
1457    /**
1458     * since 4.15.6 split variable defaultTrackColor and
1459     * mainlineTrackColor/sidelineTrackColor
1460     *
1461     * @param ltdo LayoutTrackDrawingOptions object
1462     */
1463    public void setLayoutTrackDrawingOptions(LayoutTrackDrawingOptions ltdo) {
1464        layoutTrackDrawingOptions = ltdo;
1465
1466        // copy main/side line block widths
1467        gContext.setMainlineBlockWidth(layoutTrackDrawingOptions.getMainBlockLineWidth());
1468        gContext.setSidelineBlockWidth(layoutTrackDrawingOptions.getSideBlockLineWidth());
1469
1470        // copy main/side line track (rail) widths
1471        gContext.setMainlineTrackWidth(layoutTrackDrawingOptions.getMainRailWidth());
1472        gContext.setSidelineTrackWidth(layoutTrackDrawingOptions.getSideRailWidth());
1473
1474        mainlineTrackColor = layoutTrackDrawingOptions.getMainRailColor();
1475        sidelineTrackColor = layoutTrackDrawingOptions.getSideRailColor();
1476        redrawPanel();
1477    }
1478
1479    private JCheckBoxMenuItem skipTurnoutCheckBoxMenuItem = null;
1480    private AddEntryExitPairAction addEntryExitPairAction = null;
1481
1482    /**
1483     * setup the Layout Editor Tools menu
1484     *
1485     * @param menuBar the menu bar to add the Tools menu to
1486     */
1487    private void setupToolsMenu(@Nonnull JMenuBar menuBar) {
1488        JMenu toolsMenu = new JMenu(Bundle.getMessage("MenuTools"));
1489
1490        toolsMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("MenuToolsMnemonic")));
1491        menuBar.add(toolsMenu);
1492
1493        // setup checks menu
1494        getLEChecks().setupChecksMenu(toolsMenu);
1495
1496        // assign blocks to selection
1497        assignBlockToSelectionMenuItem.setToolTipText(Bundle.getMessage("AssignBlockToSelectionToolTip"));
1498        toolsMenu.add(assignBlockToSelectionMenuItem);
1499        assignBlockToSelectionMenuItem.addActionListener((ActionEvent event) -> {
1500            // bring up scale track diagram dialog
1501            assignBlockToSelection();
1502        });
1503        assignBlockToSelectionMenuItem.setEnabled(_layoutTrackSelection.size() > 0);
1504
1505        // scale track diagram
1506        JMenuItem jmi = new JMenuItem(Bundle.getMessage("ScaleTrackDiagram") + "...");
1507        jmi.setToolTipText(Bundle.getMessage("ScaleTrackDiagramToolTip"));
1508        toolsMenu.add(jmi);
1509        jmi.addActionListener((ActionEvent event) -> {
1510            // bring up scale track diagram dialog
1511            ScaleTrackDiagramDialog d = new ScaleTrackDiagramDialog(this);
1512            d.scaleTrackDiagram();
1513        });
1514
1515        // translate selection
1516        jmi = new JMenuItem(Bundle.getMessage("TranslateSelection") + "...");
1517        jmi.setToolTipText(Bundle.getMessage("TranslateSelectionToolTip"));
1518        toolsMenu.add(jmi);
1519        jmi.addActionListener((ActionEvent event) -> {
1520            // bring up translate selection dialog
1521            if (!selectionActive || (selectionWidth == 0.0) || (selectionHeight == 0.0)) {
1522                // no selection has been made - nothing to move
1523                JOptionPane.showMessageDialog(this, Bundle.getMessage("Error12"),
1524                        Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
1525            } else {
1526                // bring up move selection dialog
1527                MoveSelectionDialog d = new MoveSelectionDialog(this);
1528                d.moveSelection();
1529            }
1530        });
1531
1532        // undo translate selection
1533        undoTranslateSelectionMenuItem.setToolTipText(Bundle.getMessage("UndoTranslateSelectionToolTip"));
1534        toolsMenu.add(undoTranslateSelectionMenuItem);
1535        undoTranslateSelectionMenuItem.addActionListener((ActionEvent event) -> {
1536            // undo previous move selection
1537            undoMoveSelection();
1538        });
1539        undoTranslateSelectionMenuItem.setEnabled(canUndoMoveSelection);
1540
1541        // rotate selection
1542        jmi = new JMenuItem(Bundle.getMessage("RotateSelection90MenuItemTitle"));
1543        jmi.setToolTipText(Bundle.getMessage("RotateSelection90MenuItemToolTip"));
1544        toolsMenu.add(jmi);
1545        jmi.addActionListener((ActionEvent event) -> rotateSelection90());
1546
1547        // rotate entire layout
1548        jmi = new JMenuItem(Bundle.getMessage("RotateLayout90MenuItemTitle"));
1549        jmi.setToolTipText(Bundle.getMessage("RotateLayout90MenuItemToolTip"));
1550        toolsMenu.add(jmi);
1551        jmi.addActionListener((ActionEvent event) -> rotateLayout90());
1552
1553        // align layout to grid
1554        jmi = new JMenuItem(Bundle.getMessage("AlignLayoutToGridMenuItemTitle") + "...");
1555        jmi.setToolTipText(Bundle.getMessage("AlignLayoutToGridMenuItemToolTip"));
1556        toolsMenu.add(jmi);
1557        jmi.addActionListener((ActionEvent event) -> alignLayoutToGrid());
1558
1559        // align selection to grid
1560        jmi = new JMenuItem(Bundle.getMessage("AlignSelectionToGridMenuItemTitle") + "...");
1561        jmi.setToolTipText(Bundle.getMessage("AlignSelectionToGridMenuItemToolTip"));
1562        toolsMenu.add(jmi);
1563        jmi.addActionListener((ActionEvent event) -> alignSelectionToGrid());
1564
1565        // reset turnout size to program defaults
1566        jmi = new JMenuItem(Bundle.getMessage("ResetTurnoutSize"));
1567        jmi.setToolTipText(Bundle.getMessage("ResetTurnoutSizeToolTip"));
1568        toolsMenu.add(jmi);
1569        jmi.addActionListener((ActionEvent event) -> {
1570            // undo previous move selection
1571            resetTurnoutSize();
1572        });
1573        toolsMenu.addSeparator();
1574
1575        // skip turnout
1576        skipTurnoutCheckBoxMenuItem = new JCheckBoxMenuItem(Bundle.getMessage("SkipInternalTurnout"));
1577        skipTurnoutCheckBoxMenuItem.setToolTipText(Bundle.getMessage("SkipInternalTurnoutToolTip"));
1578        toolsMenu.add(skipTurnoutCheckBoxMenuItem);
1579        skipTurnoutCheckBoxMenuItem.addActionListener((ActionEvent event) -> setIncludedTurnoutSkipped(skipTurnoutCheckBoxMenuItem.isSelected()));
1580        skipTurnoutCheckBoxMenuItem.setSelected(isIncludedTurnoutSkipped());
1581
1582        // set signals at turnout
1583        jmi = new JMenuItem(Bundle.getMessage("SignalsAtTurnout") + "...");
1584        jmi.setToolTipText(Bundle.getMessage("SignalsAtTurnoutToolTip"));
1585        toolsMenu.add(jmi);
1586        jmi.addActionListener((ActionEvent event) -> {
1587            // bring up signals at turnout tool dialog
1588            getLETools().setSignalsAtTurnout(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1589        });
1590
1591        // set signals at block boundary
1592        jmi = new JMenuItem(Bundle.getMessage("SignalsAtBoundary") + "...");
1593        jmi.setToolTipText(Bundle.getMessage("SignalsAtBoundaryToolTip"));
1594        toolsMenu.add(jmi);
1595        jmi.addActionListener((ActionEvent event) -> {
1596            // bring up signals at block boundary tool dialog
1597            getLETools().setSignalsAtBlockBoundary(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1598        });
1599
1600        // set signals at crossover turnout
1601        jmi = new JMenuItem(Bundle.getMessage("SignalsAtXoverTurnout") + "...");
1602        jmi.setToolTipText(Bundle.getMessage("SignalsAtXoverTurnoutToolTip"));
1603        toolsMenu.add(jmi);
1604        jmi.addActionListener((ActionEvent event) -> {
1605            // bring up signals at crossover tool dialog
1606            getLETools().setSignalsAtXoverTurnout(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1607        });
1608
1609        // set signals at level crossing
1610        jmi = new JMenuItem(Bundle.getMessage("SignalsAtLevelXing") + "...");
1611        jmi.setToolTipText(Bundle.getMessage("SignalsAtLevelXingToolTip"));
1612        toolsMenu.add(jmi);
1613        jmi.addActionListener((ActionEvent event) -> {
1614            // bring up signals at level crossing tool dialog
1615            getLETools().setSignalsAtLevelXing(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1616        });
1617
1618        // set signals at throat-to-throat turnouts
1619        jmi = new JMenuItem(Bundle.getMessage("SignalsAtTToTTurnout") + "...");
1620        jmi.setToolTipText(Bundle.getMessage("SignalsAtTToTTurnoutToolTip"));
1621        toolsMenu.add(jmi);
1622        jmi.addActionListener((ActionEvent event) -> {
1623            // bring up signals at throat-to-throat turnouts tool dialog
1624            getLETools().setSignalsAtThroatToThroatTurnouts(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1625        });
1626
1627        // set signals at 3-way turnout
1628        jmi = new JMenuItem(Bundle.getMessage("SignalsAt3WayTurnout") + "...");
1629        jmi.setToolTipText(Bundle.getMessage("SignalsAt3WayTurnoutToolTip"));
1630        toolsMenu.add(jmi);
1631        jmi.addActionListener((ActionEvent event) -> {
1632            // bring up signals at 3-way turnout tool dialog
1633            getLETools().setSignalsAt3WayTurnout(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1634        });
1635
1636        jmi = new JMenuItem(Bundle.getMessage("SignalsAtSlip") + "...");
1637        jmi.setToolTipText(Bundle.getMessage("SignalsAtSlipToolTip"));
1638        toolsMenu.add(jmi);
1639        jmi.addActionListener((ActionEvent event) -> {
1640            // bring up signals at throat-to-throat turnouts tool dialog
1641            getLETools().setSignalsAtSlip(leToolBarPanel.signalIconEditor, leToolBarPanel.signalFrame);
1642        });
1643
1644        jmi = new JMenuItem(Bundle.getMessage("EntryExitTitle") + "...");
1645        jmi.setToolTipText(Bundle.getMessage("EntryExitToolTip"));
1646        toolsMenu.add(jmi);
1647        jmi.addActionListener((ActionEvent event) -> {
1648            if (addEntryExitPairAction == null) {
1649                addEntryExitPairAction = new AddEntryExitPairAction("ENTRY EXIT", LayoutEditor.this);
1650            }
1651            addEntryExitPairAction.actionPerformed(event);
1652        });
1653//        if (true) {   // TODO: disable for production
1654//            jmi = new JMenuItem("GEORGE");
1655//            toolsMenu.add(jmi);
1656//            jmi.addActionListener((ActionEvent event) -> {
1657//                // do GEORGE stuff here!
1658//            });
1659//        }
1660    }   // setupToolsMenu
1661
1662    /**
1663     * get the toolbar side
1664     *
1665     * @return the side where to put the tool bar
1666     */
1667    public ToolBarSide getToolBarSide() {
1668        return toolBarSide;
1669    }
1670
1671    /**
1672     * set the tool bar side
1673     *
1674     * @param newToolBarSide on which side to put the toolbar
1675     */
1676    public void setToolBarSide(ToolBarSide newToolBarSide) {
1677        // null if edit toolbar is not setup yet...
1678        if (!newToolBarSide.equals(toolBarSide)) {
1679            toolBarSide = newToolBarSide;
1680            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setProperty(getWindowFrameRef(), "toolBarSide", toolBarSide.getName()));
1681            toolBarSideTopButton.setSelected(toolBarSide.equals(ToolBarSide.eTOP));
1682            toolBarSideLeftButton.setSelected(toolBarSide.equals(ToolBarSide.eLEFT));
1683            toolBarSideBottomButton.setSelected(toolBarSide.equals(ToolBarSide.eBOTTOM));
1684            toolBarSideRightButton.setSelected(toolBarSide.equals(ToolBarSide.eRIGHT));
1685            toolBarSideFloatButton.setSelected(toolBarSide.equals(ToolBarSide.eFLOAT));
1686
1687            setupToolBar(); // re-layout all the toolbar items
1688
1689            if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
1690                if (editToolBarContainerPanel != null) {
1691                    editToolBarContainerPanel.setVisible(false);
1692                }
1693                if (floatEditHelpPanel != null) {
1694                    floatEditHelpPanel.setVisible(isEditable() && getShowHelpBar());
1695                }
1696            } else {
1697                if (floatingEditToolBoxFrame != null) {
1698                    deletefloatingEditToolBoxFrame();
1699                }
1700                editToolBarContainerPanel.setVisible(isEditable());
1701                if (getShowHelpBar()) {
1702                    helpBarPanel.setVisible(isEditable());
1703                    // not sure why... but this is the only way I could
1704                    // get everything to layout correctly
1705                    // when the helpbar is visible...
1706                    boolean editMode = isEditable();
1707                    setAllEditable(!editMode);
1708                    setAllEditable(editMode);
1709                }
1710            }
1711            wideToolBarCheckBoxMenuItem.setEnabled(
1712                    toolBarSide.equals(ToolBarSide.eTOP)
1713                    || toolBarSide.equals(ToolBarSide.eBOTTOM));
1714        }
1715    }   // setToolBarSide
1716
1717    //
1718    //
1719    //
1720    private void setToolBarWide(boolean newToolBarIsWide) {
1721        // null if edit toolbar not setup yet...
1722        if (leToolBarPanel.toolBarIsWide != newToolBarIsWide) {
1723            leToolBarPanel.toolBarIsWide = newToolBarIsWide;
1724
1725            wideToolBarCheckBoxMenuItem.setSelected(leToolBarPanel.toolBarIsWide);
1726
1727            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> {
1728                // Note: since prefs default to false and we want wide to be the default
1729                // we invert it and save it as thin
1730                prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".toolBarThin", !leToolBarPanel.toolBarIsWide);
1731            });
1732
1733            setupToolBar(); // re-layout all the toolbar items
1734
1735            if (getShowHelpBar()) {
1736                // not sure why, but this is the only way I could
1737                // get everything to layout correctly
1738                // when the helpbar is visible...
1739                boolean editMode = isEditable();
1740                setAllEditable(!editMode);
1741                setAllEditable(editMode);
1742            } else {
1743                helpBarPanel.setVisible(isEditable() && getShowHelpBar());
1744            }
1745        }
1746    }   // setToolBarWide
1747
1748    //
1749    //
1750    //
1751    private void setupZoomMenu(@Nonnull JMenuBar menuBar) {
1752        zoomMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("MenuZoomMnemonic")));
1753        menuBar.add(zoomMenu);
1754        ButtonGroup zoomButtonGroup = new ButtonGroup();
1755
1756        int primary_modifier = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
1757
1758        // add zoom choices to menu
1759        JMenuItem zoomInItem = new JMenuItem(Bundle.getMessage("ZoomIn"));
1760        zoomInItem.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("zoomInMnemonic")));
1761        String zoomInAccelerator = Bundle.getMessage("zoomInAccelerator");
1762        // log.debug("zoomInAccelerator: " + zoomInAccelerator);
1763        zoomInItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(zoomInAccelerator), primary_modifier));
1764        zoomMenu.add(zoomInItem);
1765        zoomInItem.addActionListener((ActionEvent event) -> setZoom(getZoom() * 1.1));
1766
1767        JMenuItem zoomOutItem = new JMenuItem(Bundle.getMessage("ZoomOut"));
1768        zoomOutItem.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("zoomOutMnemonic")));
1769        String zoomOutAccelerator = Bundle.getMessage("zoomOutAccelerator");
1770        // log.debug("zoomOutAccelerator: " + zoomOutAccelerator);
1771        zoomOutItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(zoomOutAccelerator), primary_modifier));
1772        zoomMenu.add(zoomOutItem);
1773        zoomOutItem.addActionListener((ActionEvent event) -> setZoom(getZoom() / 1.1));
1774
1775        JMenuItem zoomFitItem = new JMenuItem(Bundle.getMessage("ZoomToFit"));
1776        zoomMenu.add(zoomFitItem);
1777        zoomFitItem.addActionListener((ActionEvent event) -> zoomToFit());
1778        zoomMenu.addSeparator();
1779
1780        // add zoom choices to menu
1781        zoomMenu.add(zoom025Item);
1782        zoom025Item.addActionListener((ActionEvent event) -> setZoom(0.25));
1783        zoomButtonGroup.add(zoom025Item);
1784
1785        zoomMenu.add(zoom05Item);
1786        zoom05Item.addActionListener((ActionEvent event) -> setZoom(0.5));
1787        zoomButtonGroup.add(zoom05Item);
1788
1789        zoomMenu.add(zoom075Item);
1790        zoom075Item.addActionListener((ActionEvent event) -> setZoom(0.75));
1791        zoomButtonGroup.add(zoom075Item);
1792
1793        String zoomNoneAccelerator = Bundle.getMessage("zoomNoneAccelerator");
1794        // log.debug("zoomNoneAccelerator: " + zoomNoneAccelerator);
1795        noZoomItem.setAccelerator(KeyStroke.getKeyStroke(stringsToVTCodes.get(zoomNoneAccelerator), primary_modifier));
1796
1797        zoomMenu.add(noZoomItem);
1798        noZoomItem.addActionListener((ActionEvent event) -> setZoom(1.0));
1799        zoomButtonGroup.add(noZoomItem);
1800
1801        zoomMenu.add(zoom15Item);
1802        zoom15Item.addActionListener((ActionEvent event) -> setZoom(1.5));
1803        zoomButtonGroup.add(zoom15Item);
1804
1805        zoomMenu.add(zoom20Item);
1806        zoom20Item.addActionListener((ActionEvent event) -> setZoom(2.0));
1807        zoomButtonGroup.add(zoom20Item);
1808
1809        zoomMenu.add(zoom30Item);
1810        zoom30Item.addActionListener((ActionEvent event) -> setZoom(3.0));
1811        zoomButtonGroup.add(zoom30Item);
1812
1813        zoomMenu.add(zoom40Item);
1814        zoom40Item.addActionListener((ActionEvent event) -> setZoom(4.0));
1815        zoomButtonGroup.add(zoom40Item);
1816
1817        zoomMenu.add(zoom50Item);
1818        zoom50Item.addActionListener((ActionEvent event) -> setZoom(5.0));
1819        zoomButtonGroup.add(zoom50Item);
1820
1821        zoomMenu.add(zoom60Item);
1822        zoom60Item.addActionListener((ActionEvent event) -> setZoom(6.0));
1823        zoomButtonGroup.add(zoom60Item);
1824
1825        zoomMenu.add(zoom70Item);
1826        zoom70Item.addActionListener((ActionEvent event) -> setZoom(7.0));
1827        zoomButtonGroup.add(zoom70Item);
1828
1829        zoomMenu.add(zoom80Item);
1830        zoom80Item.addActionListener((ActionEvent event) -> setZoom(8.0));
1831        zoomButtonGroup.add(zoom80Item);
1832
1833        // note: because this LayoutEditor object was just instantiated its
1834        // zoom attribute is 1.0; if it's being instantiated from an XML file
1835        // that has a zoom attribute for this object then setZoom will be
1836        // called after this method returns and we'll select the appropriate
1837        // menu item then.
1838        noZoomItem.setSelected(true);
1839
1840        // Note: We have to invoke this stuff later because _targetPanel is not setup yet
1841        SwingUtilities.invokeLater(() -> {
1842            // get the window specific saved zoom user preference
1843            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> {
1844                Object zoomProp = prefsMgr.getProperty(getWindowFrameRef(), "zoom");
1845
1846                log.debug(
1847                        "{} zoom is {}", getWindowFrameRef(), zoomProp);
1848
1849                if (zoomProp
1850                        != null) {
1851                    setZoom((Double) zoomProp);
1852                }
1853            }
1854            );
1855
1856            // get the scroll bars from the scroll pane
1857            JScrollPane scrollPane = getPanelScrollPane();
1858            if (scrollPane != null) {
1859                JScrollBar hsb = scrollPane.getHorizontalScrollBar();
1860                JScrollBar vsb = scrollPane.getVerticalScrollBar();
1861
1862                // Increase scroll bar unit increments!!!
1863                vsb.setUnitIncrement(gContext.getGridSize());
1864                hsb.setUnitIncrement(gContext.getGridSize());
1865
1866                // add scroll bar adjustment listeners
1867                vsb.addAdjustmentListener(this::scrollBarAdjusted);
1868                hsb.addAdjustmentListener(this::scrollBarAdjusted);
1869
1870                // remove all mouse wheel listeners
1871                mouseWheelListeners = scrollPane.getMouseWheelListeners();
1872                for (MouseWheelListener mwl : mouseWheelListeners) {
1873                    scrollPane.removeMouseWheelListener(mwl);
1874                }
1875
1876                // add my mouse wheel listener
1877                // (so mouseWheelMoved (below) will be called)
1878                scrollPane.addMouseWheelListener(this);
1879            }
1880        });
1881    }   // setupZoomMenu
1882
1883    private MouseWheelListener[] mouseWheelListeners;
1884
1885    // scroll bar listener to update x & y coordinates in toolbar on scroll
1886    public void scrollBarAdjusted(AdjustmentEvent event) {
1887        // log.warn("scrollBarAdjusted");
1888        if (isEditable()) {
1889            // get the location of the mouse
1890            PointerInfo mpi = MouseInfo.getPointerInfo();
1891            Point mouseLoc = mpi.getLocation();
1892            // convert to target panel coordinates
1893            SwingUtilities.convertPointFromScreen(mouseLoc, getTargetPanel());
1894            // correct for scaling...
1895            double theZoom = getZoom();
1896            xLoc = (int) (mouseLoc.getX() / theZoom);
1897            yLoc = (int) (mouseLoc.getY() / theZoom);
1898            dLoc = new Point2D.Double(xLoc, yLoc);
1899
1900            leToolBarPanel.setLocationText(dLoc);
1901        }
1902        adjustClip();
1903    }
1904
1905    private void adjustScrollBars() {
1906        // log.info("adjustScrollBars()");
1907
1908        // This is the bounds of what's on the screen
1909        JScrollPane scrollPane = getPanelScrollPane();
1910        Rectangle scrollBounds = scrollPane.getViewportBorderBounds();
1911        // log.info("  getViewportBorderBounds: {}", MathUtil.rectangle2DToString(scrollBounds));
1912
1913        // this is the size of the entire scaled layout panel
1914        Dimension targetPanelSize = getTargetPanelSize();
1915        // log.info("  getTargetPanelSize: {}", MathUtil.dimensionToString(targetPanelSize));
1916
1917        // double scale = getZoom();
1918        // determine the relative position of the current horizontal scrollbar
1919        JScrollBar horScroll = scrollPane.getHorizontalScrollBar();
1920        double oldX = horScroll.getValue();
1921        double oldMaxX = horScroll.getMaximum();
1922        double ratioX = (oldMaxX < 1) ? 0 : oldX / oldMaxX;
1923
1924        // calculate the new X maximum and value
1925        int panelWidth = (int) (targetPanelSize.getWidth());
1926        int scrollWidth = (int) scrollBounds.getWidth();
1927        int newMaxX = Math.max(panelWidth - scrollWidth, 0);
1928        int newX = (int) (newMaxX * ratioX);
1929        horScroll.setMaximum(newMaxX);
1930        horScroll.setValue(newX);
1931
1932        // determine the relative position of the current vertical scrollbar
1933        JScrollBar vertScroll = scrollPane.getVerticalScrollBar();
1934        double oldY = vertScroll.getValue();
1935        double oldMaxY = vertScroll.getMaximum();
1936        double ratioY = (oldMaxY < 1) ? 0 : oldY / oldMaxY;
1937
1938        // calculate the new X maximum and value
1939        int tempPanelHeight = (int) (targetPanelSize.getHeight());
1940        int tempScrollHeight = (int) scrollBounds.getHeight();
1941        int newMaxY = Math.max(tempPanelHeight - tempScrollHeight, 0);
1942        int newY = (int) (newMaxY * ratioY);
1943        vertScroll.setMaximum(newMaxY);
1944        vertScroll.setValue(newY);
1945
1946//        log.info("w: {}, x: {}, h: {}, y: {}", "" + newMaxX, "" + newX, "" + newMaxY, "" + newY);
1947        adjustClip();
1948    }
1949
1950    private void adjustClip() {
1951        // log.info("adjustClip()");
1952
1953        // This is the bounds of what's on the screen
1954        JScrollPane scrollPane = getPanelScrollPane();
1955        Rectangle scrollBounds = scrollPane.getViewportBorderBounds();
1956        // log.info("  ViewportBorderBounds: {}", MathUtil.rectangle2DToString(scrollBounds));
1957
1958        JScrollBar horScroll = scrollPane.getHorizontalScrollBar();
1959        int scrollX = horScroll.getValue();
1960        JScrollBar vertScroll = scrollPane.getVerticalScrollBar();
1961        int scrollY = vertScroll.getValue();
1962
1963        Rectangle2D newClipRect = MathUtil.offset(
1964                scrollBounds,
1965                scrollX - scrollBounds.getMinX(),
1966                scrollY - scrollBounds.getMinY());
1967        newClipRect = MathUtil.scale(newClipRect, 1.0 / getZoom());
1968        newClipRect = MathUtil.granulize(newClipRect, 1.0); // round to nearest pixel
1969        layoutEditorComponent.setClip(newClipRect);
1970
1971        redrawPanel();
1972    }
1973
1974    @Override
1975    public void mouseWheelMoved(@Nonnull MouseWheelEvent event) {
1976        // log.warn("mouseWheelMoved");
1977        if (event.isAltDown()) {
1978            // get the mouse position from the event and convert to target panel coordinates
1979            Component component = (Component) event.getSource();
1980            Point eventPoint = event.getPoint();
1981            JComponent targetPanel = getTargetPanel();
1982            Point2D mousePoint = SwingUtilities.convertPoint(component, eventPoint, targetPanel);
1983
1984            // get the old view port position
1985            JScrollPane scrollPane = getPanelScrollPane();
1986            JViewport viewPort = scrollPane.getViewport();
1987            Point2D viewPosition = viewPort.getViewPosition();
1988
1989            // convert from oldZoom (scaled) coordinates to image coordinates
1990            double zoom = getZoom();
1991            Point2D imageMousePoint = MathUtil.divide(mousePoint, zoom);
1992            Point2D imageViewPosition = MathUtil.divide(viewPosition, zoom);
1993            // compute the delta (in image coordinates)
1994            Point2D imageDelta = MathUtil.subtract(imageMousePoint, imageViewPosition);
1995
1996            // compute how much to change zoom
1997            double amount = Math.pow(1.1, event.getScrollAmount());
1998            if (event.getWheelRotation() < 0.0) {
1999                // reciprocal for zoom out
2000                amount = 1.0 / amount;
2001            }
2002            // set the new zoom
2003            double newZoom = setZoom(zoom * amount);
2004            // recalulate the amount (in case setZoom didn't zoom as much as we wanted)
2005            amount = newZoom / zoom;
2006
2007            // convert the old delta to the new
2008            Point2D newImageDelta = MathUtil.divide(imageDelta, amount);
2009            // calculate the new view position (in image coordinates)
2010            Point2D newImageViewPosition = MathUtil.subtract(imageMousePoint, newImageDelta);
2011            // convert from image coordinates to newZoom (scaled) coordinates
2012            Point2D newViewPosition = MathUtil.multiply(newImageViewPosition, newZoom);
2013
2014            // don't let origin go negative
2015            newViewPosition = MathUtil.max(newViewPosition, MathUtil.zeroPoint2D);
2016            // log.info("mouseWheelMoved: newViewPos2D: {}", newViewPosition);
2017
2018            // set new view position
2019            viewPort.setViewPosition(MathUtil.point2DToPoint(newViewPosition));
2020        } else {
2021            JScrollPane scrollPane = getPanelScrollPane();
2022            if (scrollPane != null) {
2023                if (scrollPane.getVerticalScrollBar().isVisible()) {
2024                    // Redispatch the event to the original MouseWheelListeners
2025                    for (MouseWheelListener mwl : mouseWheelListeners) {
2026                        mwl.mouseWheelMoved(event);
2027                    }
2028                } else {
2029                    // proprogate event to ancestor
2030                    Component ancestor = SwingUtilities.getAncestorOfClass(JScrollPane.class,
2031                            scrollPane);
2032                    if (ancestor != null) {
2033                        MouseWheelEvent mwe = new MouseWheelEvent(
2034                                ancestor,
2035                                event.getID(),
2036                                event.getWhen(),
2037                                event.getModifiersEx(),
2038                                event.getX(),
2039                                event.getY(),
2040                                event.getXOnScreen(),
2041                                event.getYOnScreen(),
2042                                event.getClickCount(),
2043                                event.isPopupTrigger(),
2044                                event.getScrollType(),
2045                                event.getScrollAmount(),
2046                                event.getWheelRotation());
2047
2048                        ancestor.dispatchEvent(mwe);
2049                    }
2050                }
2051            }
2052        }
2053    }
2054
2055    //
2056    // select the apropreate zoom menu item based on the zoomFactor
2057    //
2058    private void selectZoomMenuItem(double zoomFactor) {
2059        // this will put zoomFactor on 100% increments
2060        //(so it will more likely match one of these values)
2061        int newZoomFactor = (int) MathUtil.granulize(zoomFactor, 100);
2062        // int newZoomFactor = ((int) Math.round(zoomFactor)) * 100;
2063        noZoomItem.setSelected(newZoomFactor == 100);
2064        zoom20Item.setSelected(newZoomFactor == 200);
2065        zoom30Item.setSelected(newZoomFactor == 300);
2066        zoom40Item.setSelected(newZoomFactor == 400);
2067        zoom50Item.setSelected(newZoomFactor == 500);
2068        zoom60Item.setSelected(newZoomFactor == 600);
2069        zoom70Item.setSelected(newZoomFactor == 700);
2070        zoom80Item.setSelected(newZoomFactor == 800);
2071
2072        // this will put zoomFactor on 50% increments
2073        //(so it will more likely match one of these values)
2074        // newZoomFactor = ((int) (zoomFactor * 2)) * 50;
2075        newZoomFactor = (int) MathUtil.granulize(zoomFactor, 50);
2076        zoom05Item.setSelected(newZoomFactor == 50);
2077        zoom15Item.setSelected(newZoomFactor == 150);
2078
2079        // this will put zoomFactor on 25% increments
2080        //(so it will more likely match one of these values)
2081        // newZoomFactor = ((int) (zoomFactor * 4)) * 25;
2082        newZoomFactor = (int) MathUtil.granulize(zoomFactor, 25);
2083        zoom025Item.setSelected(newZoomFactor == 25);
2084        zoom075Item.setSelected(newZoomFactor == 75);
2085    }
2086
2087    /**
2088     * setZoom
2089     *
2090     * @param zoomFactor the amount to scale
2091     * @return the new scale amount (not necessarily the same as zoomFactor)
2092     */
2093    public double setZoom(double zoomFactor) {
2094        double newZoom = MathUtil.pin(zoomFactor, minZoom, maxZoom);
2095        selectZoomMenuItem(newZoom);
2096
2097        if (!MathUtil.equals(newZoom, getPaintScale())) {
2098            log.debug("zoom: {}", zoomFactor);
2099            // setPaintScale(newZoom);   //<<== don't call; messes up scrollbars
2100            _paintScale = newZoom;      // just set paint scale directly
2101            resetTargetSize();          // calculate new target panel size
2102            adjustScrollBars();         // and adjust the scrollbars ourselves
2103            // adjustClip();
2104
2105            leToolBarPanel.zoomLabel.setText(String.format("x%1$,.2f", newZoom));
2106
2107            // save the window specific saved zoom user preference
2108            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setProperty(getWindowFrameRef(), "zoom", zoomFactor));
2109        }
2110        return getPaintScale();
2111    }
2112
2113    /**
2114     * getZoom
2115     *
2116     * @return the zooming scale
2117     */
2118    public double getZoom() {
2119        return getPaintScale();
2120    }
2121
2122    /**
2123     * getMinZoom
2124     *
2125     * @return the minimum zoom scale
2126     */
2127    public double getMinZoom() {
2128        return minZoom;
2129    }
2130
2131    /**
2132     * getMaxZoom
2133     *
2134     * @return the maximum zoom scale
2135     */
2136    public double getMaxZoom() {
2137        return maxZoom;
2138    }
2139
2140    //
2141    // TODO: make this public? (might be useful!)
2142    //
2143    private Rectangle2D calculateMinimumLayoutBounds() {
2144        // calculate a union of the bounds of everything on the layout
2145        Rectangle2D result = new Rectangle2D.Double();
2146
2147        // combine all (onscreen) Components into a list of list of Components
2148        List<List<? extends Component>> listOfListsOfComponents = new ArrayList<>();
2149        listOfListsOfComponents.add(backgroundImage);
2150        listOfListsOfComponents.add(sensorImage);
2151        listOfListsOfComponents.add(signalHeadImage);
2152        listOfListsOfComponents.add(markerImage);
2153        listOfListsOfComponents.add(labelImage);
2154        listOfListsOfComponents.add(clocks);
2155        listOfListsOfComponents.add(multiSensors);
2156        listOfListsOfComponents.add(signalList);
2157        listOfListsOfComponents.add(memoryLabelList);
2158        listOfListsOfComponents.add(blockContentsLabelList);
2159        listOfListsOfComponents.add(sensorList);
2160        listOfListsOfComponents.add(signalMastList);
2161        // combine their bounds
2162        for (List<? extends Component> listOfComponents : listOfListsOfComponents) {
2163            for (Component o : listOfComponents) {
2164                if (result.isEmpty()) {
2165                    result = o.getBounds();
2166                } else {
2167                    result = result.createUnion(o.getBounds());
2168                }
2169            }
2170        }
2171
2172        for (LayoutTrackView ov : getLayoutTrackViews()) {
2173            if (result.isEmpty()) {
2174                result = ov.getBounds();
2175            } else {
2176                result = result.createUnion(ov.getBounds());
2177            }
2178        }
2179
2180        for (LayoutShape o : layoutShapes) {
2181            if (result.isEmpty()) {
2182                result = o.getBounds();
2183            } else {
2184                result = result.createUnion(o.getBounds());
2185            }
2186        }
2187
2188        // put a grid size margin around it
2189        result = MathUtil.inset(result, gContext.getGridSize() * gContext.getGridSize2nd() / -2.0);
2190
2191        return result;
2192    }
2193
2194    /**
2195     * resize panel bounds
2196     *
2197     * @param forceFlag if false only grow bigger
2198     * @return the new (?) panel bounds
2199     */
2200    private Rectangle2D resizePanelBounds(boolean forceFlag) {
2201        Rectangle2D panelBounds = getPanelBounds();
2202        Rectangle2D layoutBounds = calculateMinimumLayoutBounds();
2203
2204        // make sure it includes the origin
2205        layoutBounds.add(MathUtil.zeroPoint2D);
2206
2207        if (forceFlag) {
2208            panelBounds = layoutBounds;
2209        } else {
2210            panelBounds.add(layoutBounds);
2211        }
2212
2213        // don't let origin go negative
2214        panelBounds = panelBounds.createIntersection(MathUtil.zeroToInfinityRectangle2D);
2215
2216        // log.info("resizePanelBounds: {}", MathUtil.rectangle2DToString(panelBounds));
2217        setPanelBounds(panelBounds);
2218
2219        return panelBounds;
2220    }
2221
2222    private double zoomToFit() {
2223        Rectangle2D layoutBounds = resizePanelBounds(true);
2224
2225        // calculate the bounds for the scroll pane
2226        JScrollPane scrollPane = getPanelScrollPane();
2227        Rectangle2D scrollBounds = scrollPane.getViewportBorderBounds();
2228
2229        // don't let origin go negative
2230        scrollBounds = scrollBounds.createIntersection(MathUtil.zeroToInfinityRectangle2D);
2231
2232        // calculate the horzontial and vertical scales
2233        double scaleWidth = scrollPane.getWidth() / layoutBounds.getWidth();
2234        double scaleHeight = scrollPane.getHeight() / layoutBounds.getHeight();
2235
2236        // set the new zoom to the smallest of the two
2237        double result = setZoom(Math.min(scaleWidth, scaleHeight));
2238
2239        // set the new zoom (return value may be different)
2240        result = setZoom(result);
2241
2242        // calculate new scroll bounds
2243        scrollBounds = MathUtil.scale(layoutBounds, result);
2244
2245        // don't let origin go negative
2246        scrollBounds = scrollBounds.createIntersection(MathUtil.zeroToInfinityRectangle2D);
2247
2248        // make sure it includes the origin
2249        scrollBounds.add(MathUtil.zeroPoint2D);
2250
2251        // and scroll to it
2252        scrollPane.scrollRectToVisible(MathUtil.rectangle2DToRectangle(scrollBounds));
2253
2254        return result;
2255    }
2256
2257    private Point2D windowCenter() {
2258        // Returns window's center coordinates converted to layout space
2259        // Used for initial setup of turntables and reporters
2260        return MathUtil.divide(MathUtil.center(getBounds()), getZoom());
2261    }
2262
2263    private void setupMarkerMenu(@Nonnull JMenuBar menuBar) {
2264        JMenu markerMenu = new JMenu(Bundle.getMessage("MenuMarker"));
2265
2266        markerMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("MenuMarkerMnemonic")));
2267        menuBar.add(markerMenu);
2268        markerMenu.add(new AbstractAction(Bundle.getMessage("AddLoco") + "...") {
2269            @Override
2270            public void actionPerformed(ActionEvent event) {
2271                locoMarkerFromInput();
2272            }
2273        });
2274        markerMenu.add(new AbstractAction(Bundle.getMessage("AddLocoRoster") + "...") {
2275            @Override
2276            public void actionPerformed(ActionEvent event) {
2277                locoMarkerFromRoster();
2278            }
2279        });
2280        markerMenu.add(new AbstractAction(Bundle.getMessage("RemoveMarkers")) {
2281            @Override
2282            public void actionPerformed(ActionEvent event) {
2283                removeMarkers();
2284            }
2285        });
2286    }
2287
2288    private void setupDispatcherMenu(@Nonnull JMenuBar menuBar) {
2289        JMenu dispMenu = new JMenu(Bundle.getMessage("MenuDispatcher"));
2290
2291        dispMenu.setMnemonic(stringsToVTCodes.get(Bundle.getMessage("MenuDispatcherMnemonic")));
2292        dispMenu.add(new JMenuItem(new DispatcherAction(Bundle.getMessage("MenuItemOpen"))));
2293        menuBar.add(dispMenu);
2294        JMenuItem newTrainItem = new JMenuItem(Bundle.getMessage("MenuItemNewTrain"));
2295        dispMenu.add(newTrainItem);
2296        newTrainItem.addActionListener((ActionEvent event) -> {
2297            if (InstanceManager.getDefault(TransitManager.class).getNamedBeanSet().size() <= 0) {
2298                // Inform the user that there are no Transits available, and don't open the window
2299                JOptionPane.showMessageDialog(
2300                        null,
2301                        ResourceBundle.getBundle("jmri.jmrit.dispatcher.DispatcherBundle").
2302                                getString("NoTransitsMessage"));
2303            } else {
2304                DispatcherFrame df = InstanceManager.getDefault(DispatcherFrame.class
2305                );
2306                if (!df.getNewTrainActive()) {
2307                    df.getActiveTrainFrame().initiateTrain(event, null, null);
2308                    df.setNewTrainActive(true);
2309                } else {
2310                    df.getActiveTrainFrame().showActivateFrame(null);
2311                }
2312            }
2313        });
2314        menuBar.add(dispMenu);
2315    }
2316
2317    private boolean includedTurnoutSkipped = false;
2318
2319    public boolean isIncludedTurnoutSkipped() {
2320        return includedTurnoutSkipped;
2321    }
2322
2323    public void setIncludedTurnoutSkipped(Boolean boo) {
2324        includedTurnoutSkipped = boo;
2325    }
2326
2327    boolean openDispatcherOnLoad = false;
2328
2329    // TODO: @Deprecated // Java standard pattern for boolean getters is "isOpenDispatcherOnLoad()"
2330    public boolean getOpenDispatcherOnLoad() {
2331        return openDispatcherOnLoad;
2332    }
2333
2334    public void setOpenDispatcherOnLoad(Boolean boo) {
2335        openDispatcherOnLoad = boo;
2336    }
2337
2338    /**
2339     * Remove marker icons from panel
2340     */
2341    @Override
2342    public void removeMarkers() {
2343        for (int i = markerImage.size(); i > 0; i--) {
2344            LocoIcon il = markerImage.get(i - 1);
2345
2346            if ((il != null) && (il.isActive())) {
2347                markerImage.remove(i - 1);
2348                il.remove();
2349                il.dispose();
2350                setDirty();
2351            }
2352        }
2353        super.removeMarkers();
2354        redrawPanel();
2355    }
2356
2357    /**
2358     * Assign the block from the toolbar to all selected layout tracks
2359     */
2360    private void assignBlockToSelection() {
2361        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
2362        if (newName == null) {
2363            newName = "";
2364        }
2365        LayoutBlock b = InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(newName);
2366        _layoutTrackSelection.forEach((lt) -> lt.setAllLayoutBlocks(b));
2367    }
2368
2369    public boolean translateTrack(float xDel, float yDel) {
2370        Point2D delta = new Point2D.Double(xDel, yDel);
2371        getLayoutTrackViews().forEach((ltv) -> ltv.setCoordsCenter(MathUtil.add(ltv.getCoordsCenter(), delta)));
2372        resizePanelBounds(true);
2373        return true;
2374    }
2375
2376    /**
2377     * scale all LayoutTracks coordinates by the x and y factors.
2378     *
2379     * @param xFactor the amount to scale X coordinates.
2380     * @param yFactor the amount to scale Y coordinates.
2381     * @return true when complete.
2382     */
2383    public boolean scaleTrack(float xFactor, float yFactor) {
2384        getLayoutTrackViews().forEach((ltv) -> ltv.scaleCoords(xFactor, yFactor));
2385
2386        // update the overall scale factors
2387        gContext.setXScale(gContext.getXScale() * xFactor);
2388        gContext.setYScale(gContext.getYScale() * yFactor);
2389
2390        resizePanelBounds(true);
2391        return true;
2392    }
2393
2394    /**
2395     * loop through all LayoutBlocks and set colors to the default colors from
2396     * this LayoutEditor
2397     *
2398     * @return count of changed blocks
2399     */
2400    public int setAllTracksToDefaultColors() {
2401        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class
2402        );
2403        SortedSet<LayoutBlock> lBList = lbm.getNamedBeanSet();
2404        int changed = 0;
2405        for (LayoutBlock lb : lBList) {
2406            lb.setBlockTrackColor(this.getDefaultTrackColorColor());
2407            lb.setBlockOccupiedColor(this.getDefaultOccupiedTrackColorColor());
2408            lb.setBlockExtraColor(this.getDefaultAlternativeTrackColorColor());
2409            changed++;
2410        }
2411        log.info("Track Colors set to default values for {} layoutBlocks.", changed);
2412        return changed;
2413    }
2414
2415    private Rectangle2D undoRect;
2416    private boolean canUndoMoveSelection = false;
2417    private Point2D undoDelta = MathUtil.zeroPoint2D;
2418
2419    /**
2420     * Translate entire layout by x and y amounts.
2421     *
2422     * @param xTranslation horizontal (X) translation value
2423     * @param yTranslation vertical (Y) translation value
2424     */
2425    public void translate(float xTranslation, float yTranslation) {
2426        // here when all numbers read in - translation if entered
2427        if ((xTranslation != 0.0F) || (yTranslation != 0.0F)) {
2428            Point2D delta = new Point2D.Double(xTranslation, yTranslation);
2429            Rectangle2D selectionRect = getSelectionRect();
2430
2431            // set up undo information
2432            undoRect = MathUtil.offset(selectionRect, delta);
2433            undoDelta = MathUtil.subtract(MathUtil.zeroPoint2D, delta);
2434            canUndoMoveSelection = true;
2435            undoTranslateSelectionMenuItem.setEnabled(canUndoMoveSelection);
2436
2437            // apply translation to icon items within the selection
2438            for (Positionable c : _positionableSelection) {
2439                Point2D newPoint = MathUtil.add(c.getLocation(), delta);
2440                c.setLocation((int) newPoint.getX(), (int) newPoint.getY());
2441            }
2442
2443            for (LayoutTrack lt : _layoutTrackSelection) {
2444                LayoutTrackView ltv = getLayoutTrackView(lt);
2445                ltv.setCoordsCenter(MathUtil.add(ltv.getCoordsCenter(), delta));
2446            }
2447
2448            for (LayoutShape ls : _layoutShapeSelection) {
2449                ls.setCoordsCenter(MathUtil.add(ls.getCoordsCenter(), delta));
2450            }
2451
2452            selectionX = undoRect.getX();
2453            selectionY = undoRect.getY();
2454            selectionWidth = undoRect.getWidth();
2455            selectionHeight = undoRect.getHeight();
2456            resizePanelBounds(false);
2457            setDirty();
2458            redrawPanel();
2459        }
2460    }
2461
2462    /**
2463     * undo the move selection
2464     */
2465    void undoMoveSelection() {
2466        if (canUndoMoveSelection) {
2467            _positionableSelection.forEach((c) -> {
2468                Point2D newPoint = MathUtil.add(c.getLocation(), undoDelta);
2469                c.setLocation((int) newPoint.getX(), (int) newPoint.getY());
2470            });
2471
2472            _layoutTrackSelection.forEach(
2473                    (lt) -> {
2474                        LayoutTrackView ltv = getLayoutTrackView(lt);
2475                        ltv.setCoordsCenter(MathUtil.add(ltv.getCoordsCenter(), undoDelta));
2476                    }
2477            );
2478
2479            _layoutShapeSelection.forEach((ls) -> ls.setCoordsCenter(MathUtil.add(ls.getCoordsCenter(), undoDelta)));
2480
2481            undoRect = MathUtil.offset(undoRect, undoDelta);
2482            selectionX = undoRect.getX();
2483            selectionY = undoRect.getY();
2484            selectionWidth = undoRect.getWidth();
2485            selectionHeight = undoRect.getHeight();
2486
2487            resizePanelBounds(false);
2488            redrawPanel();
2489
2490            canUndoMoveSelection = false;
2491            undoTranslateSelectionMenuItem.setEnabled(canUndoMoveSelection);
2492        }
2493    }
2494
2495    /**
2496     * Rotate selection by 90 degrees clockwise.
2497     */
2498    public void rotateSelection90() {
2499        Rectangle2D bounds = getSelectionRect();
2500        Point2D center = MathUtil.midPoint(bounds);
2501
2502        for (Positionable positionable : _positionableSelection) {
2503            Rectangle2D cBounds = positionable.getBounds(new Rectangle());
2504            Point2D oldBottomLeft = new Point2D.Double(cBounds.getMinX(), cBounds.getMaxY());
2505            Point2D newTopLeft = MathUtil.rotateDEG(oldBottomLeft, center, 90);
2506            boolean rotateFlag = true;
2507            if (positionable instanceof PositionableLabel) {
2508                PositionableLabel positionableLabel = (PositionableLabel) positionable;
2509                if (positionableLabel.isBackground()) {
2510                    rotateFlag = false;
2511                }
2512            }
2513            if (rotateFlag) {
2514                positionable.rotate(positionable.getDegrees() + 90);
2515                positionable.setLocation((int) newTopLeft.getX(), (int) newTopLeft.getY());
2516            }
2517        }
2518
2519        for (LayoutTrack lt : _layoutTrackSelection) {
2520            LayoutTrackView ltv = getLayoutTrackView(lt);
2521            ltv.setCoordsCenter(MathUtil.rotateDEG(ltv.getCoordsCenter(), center, 90));
2522            ltv.rotateCoords(90);
2523        }
2524
2525        for (LayoutShape ls : _layoutShapeSelection) {
2526            ls.setCoordsCenter(MathUtil.rotateDEG(ls.getCoordsCenter(), center, 90));
2527            ls.rotateCoords(90);
2528        }
2529
2530        resizePanelBounds(true);
2531        setDirty();
2532        redrawPanel();
2533    }
2534
2535    /**
2536     * Rotate the entire layout by 90 degrees clockwise.
2537     */
2538    public void rotateLayout90() {
2539        List<Positionable> positionables = new ArrayList<>(_contents);
2540        positionables.addAll(backgroundImage);
2541        positionables.addAll(blockContentsLabelList);
2542        positionables.addAll(labelImage);
2543        positionables.addAll(memoryLabelList);
2544        positionables.addAll(sensorImage);
2545        positionables.addAll(sensorList);
2546        positionables.addAll(signalHeadImage);
2547        positionables.addAll(signalList);
2548        positionables.addAll(signalMastList);
2549
2550        // do this to remove duplicates that may be in more than one list
2551        positionables = positionables.stream().distinct().collect(Collectors.toList());
2552
2553        Rectangle2D bounds = getPanelBounds();
2554        Point2D lowerLeft = new Point2D.Double(bounds.getMinX(), bounds.getMaxY());
2555
2556        for (Positionable positionable : positionables) {
2557            Rectangle2D cBounds = positionable.getBounds(new Rectangle());
2558            Point2D newTopLeft = MathUtil.subtract(MathUtil.rotateDEG(positionable.getLocation(), lowerLeft, 90), lowerLeft);
2559            boolean reLocateFlag = true;
2560            if (positionable instanceof PositionableLabel) {
2561                try {
2562                    PositionableLabel positionableLabel = (PositionableLabel) positionable;
2563                    if (positionableLabel.isBackground()) {
2564                        reLocateFlag = false;
2565                    }
2566                    positionableLabel.rotate(positionableLabel.getDegrees() + 90);
2567                } catch (NullPointerException ex) {
2568                    log.warn("previously-ignored NPE", ex);
2569                }
2570            }
2571            if (reLocateFlag) {
2572                try {
2573                    positionable.setLocation((int) (newTopLeft.getX() - cBounds.getHeight()), (int) newTopLeft.getY());
2574                } catch (NullPointerException ex) {
2575                    log.warn("previously-ignored NPE", ex);
2576                }
2577            }
2578        }
2579
2580        for (LayoutTrackView ltv : getLayoutTrackViews()) {
2581            try {
2582                Point2D newPoint = MathUtil.subtract(MathUtil.rotateDEG(ltv.getCoordsCenter(), lowerLeft, 90), lowerLeft);
2583                ltv.setCoordsCenter(newPoint);
2584                ltv.rotateCoords(90);
2585            } catch (NullPointerException ex) {
2586                log.warn("previously-ignored NPE", ex);
2587            }
2588        }
2589
2590        for (LayoutShape ls : layoutShapes) {
2591            Point2D newPoint = MathUtil.subtract(MathUtil.rotateDEG(ls.getCoordsCenter(), lowerLeft, 90), lowerLeft);
2592            ls.setCoordsCenter(newPoint);
2593            ls.rotateCoords(90);
2594        }
2595
2596        resizePanelBounds(true);
2597        setDirty();
2598        redrawPanel();
2599    }
2600
2601    /**
2602     * align the layout to grid
2603     */
2604    public void alignLayoutToGrid() {
2605        // align to grid
2606        List<Positionable> positionables = new ArrayList<>(_contents);
2607        positionables.addAll(backgroundImage);
2608        positionables.addAll(blockContentsLabelList);
2609        positionables.addAll(labelImage);
2610        positionables.addAll(memoryLabelList);
2611        positionables.addAll(sensorImage);
2612        positionables.addAll(sensorList);
2613        positionables.addAll(signalHeadImage);
2614        positionables.addAll(signalList);
2615        positionables.addAll(signalMastList);
2616
2617        // do this to remove duplicates that may be in more than one list
2618        positionables = positionables.stream().distinct().collect(Collectors.toList());
2619        alignToGrid(positionables, getLayoutTracks(), layoutShapes);
2620    }
2621
2622    /**
2623     * align selection to grid
2624     */
2625    public void alignSelectionToGrid() {
2626        alignToGrid(_positionableSelection, _layoutTrackSelection, _layoutShapeSelection);
2627    }
2628
2629    private void alignToGrid(List<Positionable> positionables, List<LayoutTrack> tracks, List<LayoutShape> shapes) {
2630        for (Positionable positionable : positionables) {
2631            Point2D newLocation = MathUtil.granulize(positionable.getLocation(), gContext.getGridSize());
2632            positionable.setLocation((int) (newLocation.getX()), (int) newLocation.getY());
2633        }
2634        for (LayoutTrack lt : tracks) {
2635            LayoutTrackView ltv = getLayoutTrackView(lt);
2636            ltv.setCoordsCenter(MathUtil.granulize(ltv.getCoordsCenter(), gContext.getGridSize()));
2637            if (lt instanceof LayoutTurntable) {
2638                LayoutTurntable tt = (LayoutTurntable) lt;
2639                LayoutTurntableView ttv = getLayoutTurntableView(tt);
2640                for (LayoutTurntable.RayTrack rt : tt.getRayTrackList()) {
2641                    int rayIndex = rt.getConnectionIndex();
2642                    ttv.setRayCoordsIndexed(MathUtil.granulize(ttv.getRayCoordsIndexed(rayIndex), gContext.getGridSize()), rayIndex);
2643                }
2644            }
2645        }
2646        for (LayoutShape ls : shapes) {
2647            ls.setCoordsCenter(MathUtil.granulize(ls.getCoordsCenter(), gContext.getGridSize()));
2648            for (int idx = 0; idx < ls.getNumberPoints(); idx++) {
2649                ls.setPoint(idx, MathUtil.granulize(ls.getPoint(idx), gContext.getGridSize()));
2650            }
2651        }
2652
2653        resizePanelBounds(true);
2654        setDirty();
2655        redrawPanel();
2656    }
2657
2658    public void setCurrentPositionAndSize() {
2659        // save current panel location and size
2660        Dimension dim = getSize();
2661
2662        // Compute window size based on LayoutEditor size
2663        gContext.setWindowHeight(dim.height);
2664        gContext.setWindowWidth(dim.width);
2665
2666        // Compute layout size based on LayoutPane size
2667        dim = getTargetPanelSize();
2668        gContext.setLayoutWidth((int) (dim.width / getZoom()));
2669        gContext.setLayoutHeight((int) (dim.height / getZoom()));
2670        adjustScrollBars();
2671
2672        Point pt = getLocationOnScreen();
2673        gContext.setUpperLeftY(pt.x);
2674        gContext.setUpperLeftY(pt.y);
2675
2676        log.debug("setCurrentPositionAndSize Position - {},{} WindowSize - {},{} PanelSize - {},{}", gContext.getUpperLeftX(), gContext.getUpperLeftY(), gContext.getWindowWidth(), gContext.getWindowHeight(), gContext.getLayoutWidth(), gContext.getLayoutHeight());
2677        setDirty();
2678    }
2679
2680    private JRadioButtonMenuItem addButtonGroupMenuEntry(
2681            @Nonnull JMenu inMenu,
2682            ButtonGroup inButtonGroup,
2683            final String inName,
2684            boolean inSelected,
2685            ActionListener inActionListener) {
2686        JRadioButtonMenuItem result = new JRadioButtonMenuItem(inName);
2687        if (inActionListener != null) {
2688            result.addActionListener(inActionListener);
2689        }
2690        if (inButtonGroup != null) {
2691            inButtonGroup.add(result);
2692        }
2693        result.setSelected(inSelected);
2694
2695        inMenu.add(result);
2696
2697        return result;
2698    }
2699
2700    private void addTurnoutCircleSizeMenuEntry(
2701            @Nonnull JMenu inMenu,
2702            @Nonnull String inName,
2703            final int inSize) {
2704        ActionListener a = (ActionEvent event) -> {
2705            if (getTurnoutCircleSize() != inSize) {
2706                setTurnoutCircleSize(inSize);
2707                setDirty();
2708                redrawPanel();
2709            }
2710        };
2711        addButtonGroupMenuEntry(inMenu,
2712                turnoutCircleSizeButtonGroup, inName,
2713                getTurnoutCircleSize() == inSize, a);
2714    }
2715
2716    private void setOptionMenuTurnoutCircleSize() {
2717        String tcs = Integer.toString(getTurnoutCircleSize());
2718        Enumeration<AbstractButton> e = turnoutCircleSizeButtonGroup.getElements();
2719        while (e.hasMoreElements()) {
2720            AbstractButton button = e.nextElement();
2721            String buttonName = button.getText();
2722            button.setSelected(buttonName.equals(tcs));
2723        }
2724    }
2725
2726    @Override
2727    public void setScroll(int state) {
2728        if (isEditable()) {
2729            // In edit mode the scroll bars are always displayed, however we will want to set the scroll for when we exit edit mode
2730            super.setScroll(Editor.SCROLL_BOTH);
2731            _scrollState = state;
2732        } else {
2733            super.setScroll(state);
2734        }
2735    }
2736
2737    /**
2738     * Add a layout turntable at location specified
2739     *
2740     * @param pt x,y placement for turntable
2741     */
2742    public void addTurntable(@Nonnull Point2D pt) {
2743        // get unique name
2744        String name = finder.uniqueName("TUR", ++numLayoutTurntables);
2745        LayoutTurntable lt = new LayoutTurntable(name, this);
2746        LayoutTurntableView ltv = new LayoutTurntableView(lt, pt, this);
2747
2748        addLayoutTrack(lt, ltv);
2749
2750        lt.addRay(0.0);
2751        lt.addRay(90.0);
2752        lt.addRay(180.0);
2753        lt.addRay(270.0);
2754        setDirty();
2755
2756    }
2757
2758    /**
2759     * Allow external trigger of re-drawHidden
2760     */
2761    public void redrawPanel() {
2762        repaint();
2763    }
2764
2765    /**
2766     * Allow external set/reset of awaitingIconChange
2767     */
2768    public void setAwaitingIconChange() {
2769        awaitingIconChange = true;
2770    }
2771
2772    public void resetAwaitingIconChange() {
2773        awaitingIconChange = false;
2774    }
2775
2776    /**
2777     * Allow external reset of dirty bit
2778     */
2779    public void resetDirty() {
2780        setDirty(false);
2781        savedEditMode = isEditable();
2782        savedPositionable = allPositionable();
2783        savedControlLayout = allControlling();
2784        savedAnimatingLayout = isAnimating();
2785        savedShowHelpBar = getShowHelpBar();
2786    }
2787
2788    /**
2789     * Allow external set of dirty bit
2790     *
2791     * @param val true/false for panelChanged
2792     */
2793    public void setDirty(boolean val) {
2794        panelChanged = val;
2795    }
2796
2797    public void setDirty() {
2798        setDirty(true);
2799    }
2800
2801    /**
2802     * Check the dirty state.
2803     *
2804     * @return true if panel has changed
2805     */
2806    public boolean isDirty() {
2807        return panelChanged;
2808    }
2809
2810    /*
2811    * Get mouse coordinates and adjust for zoom.
2812    * <p>
2813    * Side effects on xLoc, yLoc and dLoc
2814     */
2815    @Nonnull
2816    private Point2D calcLocation(MouseEvent event, int dX, int dY) {
2817        xLoc = (int) ((event.getX() + dX) / getZoom());
2818        yLoc = (int) ((event.getY() + dY) / getZoom());
2819        dLoc = new Point2D.Double(xLoc, yLoc);
2820        return dLoc;
2821    }
2822
2823    private Point2D calcLocation(MouseEvent event) {
2824        return calcLocation(event, 0, 0);
2825    }
2826
2827    /**
2828     * Handle a mouse pressed event
2829     * <p>
2830     * Side-effects on _anchorX, _anchorY,_lastX, _lastY, xLoc, yLoc, dLoc,
2831     * selectionActive, xLabel, yLabel
2832     *
2833     * @param event the MouseEvent
2834     */
2835    @Override
2836    public void mousePressed(MouseEvent event) {
2837        // initialize cursor position
2838        _anchorX = xLoc;
2839        _anchorY = yLoc;
2840        _lastX = _anchorX;
2841        _lastY = _anchorY;
2842        calcLocation(event);
2843
2844        // TODO: Add command-click on nothing to pan view?
2845        if (isEditable()) {
2846            boolean prevSelectionActive = selectionActive;
2847            selectionActive = false;
2848            leToolBarPanel.setLocationText(dLoc);
2849
2850            if (event.isPopupTrigger()) {
2851                if (isMetaDown(event) || event.isAltDown()) {
2852                    // if requesting a popup and it might conflict with moving, delay the request to mouseReleased
2853                    delayedPopupTrigger = true;
2854                } else {
2855                    // no possible conflict with moving, display the popup now
2856                    showEditPopUps(event);
2857                }
2858            }
2859
2860            if (isMetaDown(event) || event.isAltDown()) {
2861                // if dragging an item, identify the item for mouseDragging
2862                selectedObject = null;
2863                selectedHitPointType = HitPointType.NONE;
2864
2865                if (findLayoutTracksHitPoint(dLoc)) {
2866                    selectedObject = foundTrack;
2867                    selectedHitPointType = foundHitPointType;
2868                    startDelta = MathUtil.subtract(foundLocation, dLoc);
2869                    foundTrack = null;
2870                    foundTrackView = null;
2871                } else {
2872                    selectedObject = checkMarkerPopUps(dLoc);
2873                    if (selectedObject != null) {
2874                        selectedHitPointType = HitPointType.MARKER;
2875                        startDelta = MathUtil.subtract(((LocoIcon) selectedObject).getLocation(), dLoc);
2876                    } else {
2877                        selectedObject = checkClockPopUps(dLoc);
2878                        if (selectedObject != null) {
2879                            selectedHitPointType = HitPointType.LAYOUT_POS_JCOMP;
2880                            startDelta = MathUtil.subtract(((PositionableJComponent) selectedObject).getLocation(), dLoc);
2881                        } else {
2882                            selectedObject = checkMultiSensorPopUps(dLoc);
2883                            if (selectedObject != null) {
2884                                selectedHitPointType = HitPointType.MULTI_SENSOR;
2885                                startDelta = MathUtil.subtract(((MultiSensorIcon) selectedObject).getLocation(), dLoc);
2886                            }
2887                        }
2888                    }
2889
2890                    if (selectedObject == null) {
2891                        selectedObject = checkSensorIconPopUps(dLoc);
2892                        if (selectedObject == null) {
2893                            selectedObject = checkSignalHeadIconPopUps(dLoc);
2894                            if (selectedObject == null) {
2895                                selectedObject = checkLabelImagePopUps(dLoc);
2896                                if (selectedObject == null) {
2897                                    selectedObject = checkSignalMastIconPopUps(dLoc);
2898                                }
2899                            }
2900                        }
2901
2902                        if (selectedObject != null) {
2903                            selectedHitPointType = HitPointType.LAYOUT_POS_LABEL;
2904                            startDelta = MathUtil.subtract(((PositionableLabel) selectedObject).getLocation(), dLoc);
2905                            if (selectedObject instanceof MemoryIcon) {
2906                                MemoryIcon pm = (MemoryIcon) selectedObject;
2907
2908                                if (pm.getPopupUtility().getFixedWidth() == 0) {
2909                                    startDelta = new Point2D.Double((pm.getOriginalX() - dLoc.getX()),
2910                                            (pm.getOriginalY() - dLoc.getY()));
2911                                }
2912                            }
2913                        } else {
2914                            selectedObject = checkBackgroundPopUps(dLoc);
2915
2916                            if (selectedObject != null) {
2917                                selectedHitPointType = HitPointType.LAYOUT_POS_LABEL;
2918                                startDelta = MathUtil.subtract(((PositionableLabel) selectedObject).getLocation(), dLoc);
2919                            } else {
2920                                // dragging a shape?
2921                                ListIterator<LayoutShape> listIterator = layoutShapes.listIterator(layoutShapes.size());
2922                                // hit test in front to back order (reverse order of list)
2923                                while (listIterator.hasPrevious()) {
2924                                    LayoutShape ls = listIterator.previous();
2925                                    selectedHitPointType = ls.findHitPointType(dLoc, true);
2926                                    if (LayoutShape.isShapeHitPointType(selectedHitPointType)) {
2927                                        // log.warn("drag selectedObject: ", lt);
2928                                        selectedObject = ls;    // found one!
2929                                        beginLocation = dLoc;
2930                                        currentLocation = beginLocation;
2931                                        startDelta = MathUtil.zeroPoint2D;
2932                                        break;
2933                                    }
2934                                }
2935                            }
2936                        }
2937                    }
2938                }
2939            } else if (event.isShiftDown() && leToolBarPanel.trackButton.isSelected() && !event.isPopupTrigger()) {
2940                // starting a Track Segment, check for free connection point
2941                selectedObject = null;
2942
2943                if (findLayoutTracksHitPoint(dLoc, true)) {
2944                    // match to a free connection point
2945                    beginTrack = foundTrack;
2946                    beginHitPointType = foundHitPointType;
2947                    beginLocation = foundLocation;
2948                    // BUGFIX: prevents initial drawTrackSegmentInProgress to {0, 0}
2949                    currentLocation = beginLocation;
2950                } else {
2951                    // TODO: auto-add anchor point?
2952                    beginTrack = null;
2953                }
2954            } else if (event.isShiftDown() && leToolBarPanel.shapeButton.isSelected() && !event.isPopupTrigger()) {
2955                // adding or extending a shape
2956                selectedObject = null;  // assume we're adding...
2957                for (LayoutShape ls : layoutShapes) {
2958                    selectedHitPointType = ls.findHitPointType(dLoc, true);
2959                    if (HitPointType.isShapePointOffsetHitPointType(selectedHitPointType)) {
2960                        // log.warn("extend selectedObject: ", lt);
2961                        selectedObject = ls;    // nope, we're extending
2962                        beginLocation = dLoc;
2963                        currentLocation = beginLocation;
2964                        break;
2965                    }
2966                }
2967            } else if (!event.isShiftDown() && !event.isControlDown() && !event.isPopupTrigger()) {
2968                // check if controlling a turnout in edit mode
2969                selectedObject = null;
2970
2971                if (allControlling()) {
2972                    checkControls(false);
2973                }
2974                // initialize starting selection - cancel any previous selection rectangle
2975                selectionActive = true;
2976                selectionX = dLoc.getX();
2977                selectionY = dLoc.getY();
2978                selectionWidth = 0.0;
2979                selectionHeight = 0.0;
2980            }
2981
2982            if (prevSelectionActive) {
2983                redrawPanel();
2984            }
2985        } else if (allControlling()
2986                && !isMetaDown(event) && !event.isPopupTrigger()
2987                && !event.isAltDown() && !event.isShiftDown() && !event.isControlDown()) {
2988            // not in edit mode - check if mouse is on a turnout (using wider search range)
2989            selectedObject = null;
2990            checkControls(true);
2991        } else if ((isMetaDown(event) || event.isAltDown())
2992                && !event.isShiftDown() && !event.isControlDown()) {
2993            // not in edit mode - check if moving a marker if there are any
2994            selectedObject = checkMarkerPopUps(dLoc);
2995            if (selectedObject != null) {
2996                selectedHitPointType = HitPointType.MARKER;
2997                startDelta = MathUtil.subtract(((LocoIcon) selectedObject).getLocation(), dLoc);
2998            }
2999        } else if (event.isPopupTrigger() && !event.isShiftDown()) {
3000            // not in edit mode - check if a marker popup menu is being requested
3001            LocoIcon lo = checkMarkerPopUps(dLoc);
3002            if (lo != null) {
3003                delayedPopupTrigger = true;
3004            }
3005        }
3006
3007        if (!event.isPopupTrigger()) {
3008            List<Positionable> selections = getSelectedItems(event);
3009
3010            if (selections.size() > 0) {
3011                selections.get(0).doMousePressed(event);
3012            }
3013        }
3014
3015        requestFocusInWindow();
3016    }   // mousePressed
3017
3018// this is a method to iterate over a list of lists of items
3019// calling the predicate tester.test on each one
3020// all matching items are then added to the resulting List
3021// note: currently unused; commented out to avoid findbugs warning
3022// private static List testEachItemInListOfLists(
3023//        @Nonnull List<List> listOfListsOfObjects,
3024//        @Nonnull Predicate<Object> tester) {
3025//    List result = new ArrayList<>();
3026//    for (List<Object> listOfObjects : listOfListsOfObjects) {
3027//        List<Object> l = listOfObjects.stream().filter(o -> tester.test(o)).collect(Collectors.toList());
3028//        result.addAll(l);
3029//    }
3030//    return result;
3031//}
3032// this is a method to iterate over a list of lists of items
3033// calling the predicate tester.test on each one
3034// and return the first one that matches
3035// TODO: make this public? (it is useful! ;-)
3036// note: currently unused; commented out to avoid findbugs warning
3037// private static Object findFirstMatchingItemInListOfLists(
3038//        @Nonnull List<List> listOfListsOfObjects,
3039//        @Nonnull Predicate<Object> tester) {
3040//    Object result = null;
3041//    for (List listOfObjects : listOfListsOfObjects) {
3042//        Optional<Object> opt = listOfObjects.stream().filter(o -> tester.test(o)).findFirst();
3043//        if (opt.isPresent()) {
3044//            result = opt.get();
3045//            break;
3046//        }
3047//    }
3048//    return result;
3049//}
3050    /**
3051     * Called by {@link #mousePressed} to determine if the mouse click was in a
3052     * turnout control location. If so, update selectedHitPointType and
3053     * selectedObject for use by {@link #mouseReleased}.
3054     * <p>
3055     * If there's no match, selectedObject is set to null and
3056     * selectedHitPointType is left referring to the results of the checking the
3057     * last track on the list.
3058     * <p>
3059     * Refers to the current value of {@link #getLayoutTracks()} and
3060     * {@link #dLoc}.
3061     *
3062     * @param useRectangles set true to use rectangle; false for circles.
3063     */
3064    private void checkControls(boolean useRectangles) {
3065        selectedObject = null;  // deliberate side-effect
3066        for (LayoutTrackView theTrackView : getLayoutTrackViews()) {
3067            selectedHitPointType = theTrackView.findHitPointType(dLoc, useRectangles); // deliberate side-effect
3068            if (HitPointType.isControlHitType(selectedHitPointType)) {
3069                selectedObject = theTrackView.getLayoutTrack(); // deliberate side-effect
3070                return;
3071            }
3072        }
3073    }
3074
3075    // This is a geometric search, and should be done with views.
3076    // Hence this form is inevitably temporary.
3077    //
3078    private boolean findLayoutTracksHitPoint(
3079            @Nonnull Point2D loc, boolean requireUnconnected) {
3080        return findLayoutTracksHitPoint(loc, requireUnconnected, null);
3081    }
3082
3083    // This is a geometric search, and should be done with views.
3084    // Hence this form is inevitably temporary.
3085    //
3086    // optional parameter requireUnconnected
3087    private boolean findLayoutTracksHitPoint(@Nonnull Point2D loc) {
3088        return findLayoutTracksHitPoint(loc, false, null);
3089    }
3090
3091    /**
3092     * Internal (private) method to find the track closest to a point, with some
3093     * modifiers to the search. The {@link #foundTrack} and
3094     * {@link #foundHitPointType} members are set from the search.
3095     * <p>
3096     * This is a geometric search, and should be done with views. Hence this
3097     * form is inevitably temporary.
3098     *
3099     * @param loc                Point to search from
3100     * @param requireUnconnected forwarded to {@link #getLayoutTrackView}; if
3101     *                           true, return only free connections
3102     * @param avoid              Don't return this track, keep searching. Note
3103     *                           that {@Link #selectedObject} is also always
3104     *                           avoided automatically
3105     * @returns true if values of {@link #foundTrack} and
3106     * {@link #foundHitPointType} correct; note they may have changed even if
3107     * false is returned.
3108     */
3109    private boolean findLayoutTracksHitPoint(@Nonnull Point2D loc,
3110            boolean requireUnconnected, @CheckForNull LayoutTrack avoid) {
3111        boolean result = false; // assume failure (pessimist!)
3112
3113        foundTrack = null;
3114        foundTrackView = null;
3115        foundHitPointType = HitPointType.NONE;
3116
3117        Optional<LayoutTrack> opt = getLayoutTracks().stream().filter(layoutTrack -> {  // != means can't (yet) loop over Views
3118            if ((layoutTrack != avoid) && (layoutTrack != selectedObject)) {
3119                foundHitPointType = getLayoutTrackView(layoutTrack).findHitPointType(loc, false, requireUnconnected);
3120            }
3121            return (HitPointType.NONE != foundHitPointType);
3122        }).findFirst();
3123
3124        LayoutTrack layoutTrack = null;
3125        if (opt.isPresent()) {
3126            layoutTrack = opt.get();
3127        }
3128
3129        if (layoutTrack != null) {
3130            foundTrack = layoutTrack;
3131            foundTrackView = this.getLayoutTrackView(layoutTrack);
3132
3133            // get screen coordinates
3134            foundLocation = foundTrackView.getCoordsForConnectionType(foundHitPointType);
3135            /// foundNeedsConnect = isDisconnected(foundHitPointType);
3136            result = true;
3137        }
3138        return result;
3139    }
3140
3141    private TrackSegment checkTrackSegmentPopUps(@Nonnull Point2D loc) {
3142        assert loc != null;
3143
3144        TrackSegment result = null;
3145
3146        // NOTE: Rather than calculate all the hit rectangles for all
3147        // the points below and test if this location is in any of those
3148        // rectangles just create a hit rectangle for the location and
3149        // see if any of the points below are in it instead...
3150        Rectangle2D r = layoutEditorControlCircleRectAt(loc);
3151
3152        // check Track Segments, if any
3153        for (TrackSegmentView tsv : getTrackSegmentViews()) {
3154            if (r.contains(tsv.getCentreSeg())) {
3155                result = tsv.getTrackSegment();
3156                break;
3157            }
3158        }
3159        return result;
3160    }
3161
3162    private PositionableLabel checkBackgroundPopUps(@Nonnull Point2D loc) {
3163        assert loc != null;
3164
3165        PositionableLabel result = null;
3166        // check background images, if any
3167        for (int i = backgroundImage.size() - 1; i >= 0; i--) {
3168            PositionableLabel b = backgroundImage.get(i);
3169            Rectangle2D r = b.getBounds();
3170            if (r.contains(loc)) {
3171                result = b;
3172                break;
3173            }
3174        }
3175        return result;
3176    }
3177
3178    private SensorIcon checkSensorIconPopUps(@Nonnull Point2D loc) {
3179        assert loc != null;
3180
3181        SensorIcon result = null;
3182        // check sensor images, if any
3183        for (int i = sensorImage.size() - 1; i >= 0; i--) {
3184            SensorIcon s = sensorImage.get(i);
3185            Rectangle2D r = s.getBounds();
3186            if (r.contains(loc)) {
3187                result = s;
3188            }
3189        }
3190        return result;
3191    }
3192
3193    private SignalHeadIcon checkSignalHeadIconPopUps(@Nonnull Point2D loc) {
3194        assert loc != null;
3195
3196        SignalHeadIcon result = null;
3197        // check signal head images, if any
3198        for (int i = signalHeadImage.size() - 1; i >= 0; i--) {
3199            SignalHeadIcon s = signalHeadImage.get(i);
3200            Rectangle2D r = s.getBounds();
3201            if (r.contains(loc)) {
3202                result = s;
3203                break;
3204            }
3205        }
3206        return result;
3207    }
3208
3209    private SignalMastIcon checkSignalMastIconPopUps(@Nonnull Point2D loc) {
3210        assert loc != null;
3211
3212        SignalMastIcon result = null;
3213        // check signal head images, if any
3214        for (int i = signalMastList.size() - 1; i >= 0; i--) {
3215            SignalMastIcon s = signalMastList.get(i);
3216            Rectangle2D r = s.getBounds();
3217            if (r.contains(loc)) {
3218                result = s;
3219                break;
3220            }
3221        }
3222        return result;
3223    }
3224
3225    private PositionableLabel checkLabelImagePopUps(@Nonnull Point2D loc) {
3226        assert loc != null;
3227
3228        PositionableLabel result = null;
3229        int level = 0;
3230
3231        for (int i = labelImage.size() - 1; i >= 0; i--) {
3232            PositionableLabel s = labelImage.get(i);
3233            double x = s.getX();
3234            double y = s.getY();
3235            double w = 10.0;
3236            double h = 5.0;
3237
3238            if (s.isIcon() || s.isRotated() || s.getPopupUtility().getOrientation() != PositionablePopupUtil.HORIZONTAL) {
3239                w = s.maxWidth();
3240                h = s.maxHeight();
3241            } else if (s.isText()) {
3242                h = s.getFont().getSize();
3243                w = (h * 2 * (s.getText().length())) / 3;
3244            }
3245
3246            Rectangle2D r = new Rectangle2D.Double(x, y, w, h);
3247            if (r.contains(loc)) {
3248                if (s.getDisplayLevel() >= level) {
3249                    // Check to make sure that we are returning the highest level label.
3250                    result = s;
3251                    level = s.getDisplayLevel();
3252                }
3253            }
3254        }
3255        return result;
3256    }
3257
3258    private AnalogClock2Display checkClockPopUps(@Nonnull Point2D loc) {
3259        assert loc != null;
3260
3261        AnalogClock2Display result = null;
3262        // check clocks, if any
3263        for (int i = clocks.size() - 1; i >= 0; i--) {
3264            AnalogClock2Display s = clocks.get(i);
3265            Rectangle2D r = s.getBounds();
3266            if (r.contains(loc)) {
3267                result = s;
3268                break;
3269            }
3270        }
3271        return result;
3272    }
3273
3274    private MultiSensorIcon checkMultiSensorPopUps(@Nonnull Point2D loc) {
3275        assert loc != null;
3276
3277        MultiSensorIcon result = null;
3278        // check multi sensor icons, if any
3279        for (int i = multiSensors.size() - 1; i >= 0; i--) {
3280            MultiSensorIcon s = multiSensors.get(i);
3281            Rectangle2D r = s.getBounds();
3282            if (r.contains(loc)) {
3283                result = s;
3284                break;
3285            }
3286        }
3287        return result;
3288    }
3289
3290    private LocoIcon checkMarkerPopUps(@Nonnull Point2D loc) {
3291        assert loc != null;
3292
3293        LocoIcon result = null;
3294        // check marker icons, if any
3295        for (int i = markerImage.size() - 1; i >= 0; i--) {
3296            LocoIcon l = markerImage.get(i);
3297            Rectangle2D r = l.getBounds();
3298            if (r.contains(loc)) {
3299                // mouse was pressed in marker icon
3300                result = l;
3301                break;
3302            }
3303        }
3304        return result;
3305    }
3306
3307    private LayoutShape checkLayoutShapePopUps(@Nonnull Point2D loc) {
3308        assert loc != null;
3309
3310        LayoutShape result = null;
3311        for (LayoutShape ls : layoutShapes) {
3312            selectedHitPointType = ls.findHitPointType(loc, true);
3313            if (LayoutShape.isShapeHitPointType(selectedHitPointType)) {
3314                result = ls;
3315                break;
3316            }
3317        }
3318        return result;
3319    }
3320
3321    /**
3322     * Get the coordinates for the connection type of the specified LayoutTrack
3323     * or subtype.
3324     * <p>
3325     * This uses the current LayoutEditor object to map a LayoutTrack (no
3326     * coordinates) object to _a_ specific LayoutTrackView object in the current
3327     * LayoutEditor i.e. window. This allows the same model object in two
3328     * windows, but not twice in a single window.
3329     * <p>
3330     * This is temporary, and needs to go away as the LayoutTrack doesn't
3331     * logically have position; just the LayoutTrackView does, and multiple
3332     * LayoutTrackViews can refer to one specific LayoutTrack.
3333     *
3334     * @param trk            the object (LayoutTrack subclass)
3335     * @param connectionType the type of connection
3336     * @return the coordinates for the connection type of the specified object
3337     */
3338    @Nonnull
3339    public Point2D getCoords(@Nonnull LayoutTrack trk, HitPointType connectionType) {
3340        assert trk != null;
3341
3342        return getCoords(getLayoutTrackView(trk), connectionType);
3343    }
3344
3345    /**
3346     * Get the coordinates for the connection type of the specified
3347     * LayoutTrackView or subtype.
3348     *
3349     * @param trkv           the object (LayoutTrackView subclass)
3350     * @param connectionType the type of connection
3351     * @return the coordinates for the connection type of the specified object
3352     */
3353    @Nonnull
3354    public Point2D getCoords(@Nonnull LayoutTrackView trkv, HitPointType connectionType) {
3355        assert trkv != null;
3356
3357        return trkv.getCoordsForConnectionType(connectionType);
3358    }
3359
3360    @Override
3361    public void mouseReleased(MouseEvent event) {
3362        super.setToolTip(null);
3363
3364        // initialize mouse position
3365        calcLocation(event);
3366
3367        // if alt modifier is down invert the snap to grid behaviour
3368        snapToGridInvert = event.isAltDown();
3369
3370        if (isEditable()) {
3371            leToolBarPanel.setLocationText(dLoc);
3372
3373            // released the mouse with shift down... see what we're adding
3374            if (!event.isPopupTrigger() && !isMetaDown(event) && event.isShiftDown()) {
3375
3376                currentPoint = new Point2D.Double(xLoc, yLoc);
3377
3378                if (snapToGridOnAdd != snapToGridInvert) {
3379                    // this snaps the current point to the grid
3380                    currentPoint = MathUtil.granulize(currentPoint, gContext.getGridSize());
3381                    xLoc = (int) currentPoint.getX();
3382                    yLoc = (int) currentPoint.getY();
3383                    leToolBarPanel.setLocationText(currentPoint);
3384                }
3385
3386                if (leToolBarPanel.turnoutRHButton.isSelected()) {
3387                    addLayoutTurnout(LayoutTurnout.TurnoutType.RH_TURNOUT);
3388                } else if (leToolBarPanel.turnoutLHButton.isSelected()) {
3389                    addLayoutTurnout(LayoutTurnout.TurnoutType.LH_TURNOUT);
3390                } else if (leToolBarPanel.turnoutWYEButton.isSelected()) {
3391                    addLayoutTurnout(LayoutTurnout.TurnoutType.WYE_TURNOUT);
3392                } else if (leToolBarPanel.doubleXoverButton.isSelected()) {
3393                    addLayoutTurnout(LayoutTurnout.TurnoutType.DOUBLE_XOVER);
3394                } else if (leToolBarPanel.rhXoverButton.isSelected()) {
3395                    addLayoutTurnout(LayoutTurnout.TurnoutType.RH_XOVER);
3396                } else if (leToolBarPanel.lhXoverButton.isSelected()) {
3397                    addLayoutTurnout(LayoutTurnout.TurnoutType.LH_XOVER);
3398                } else if (leToolBarPanel.levelXingButton.isSelected()) {
3399                    addLevelXing();
3400                } else if (leToolBarPanel.layoutSingleSlipButton.isSelected()) {
3401                    addLayoutSlip(LayoutSlip.TurnoutType.SINGLE_SLIP);
3402                } else if (leToolBarPanel.layoutDoubleSlipButton.isSelected()) {
3403                    addLayoutSlip(LayoutSlip.TurnoutType.DOUBLE_SLIP);
3404                } else if (leToolBarPanel.endBumperButton.isSelected()) {
3405                    addEndBumper();
3406                } else if (leToolBarPanel.anchorButton.isSelected()) {
3407                    addAnchor();
3408                } else if (leToolBarPanel.edgeButton.isSelected()) {
3409                    addEdgeConnector();
3410                } else if (leToolBarPanel.trackButton.isSelected()) {
3411                    if ((beginTrack != null) && (foundTrack != null)
3412                            && (beginTrack != foundTrack)) {
3413                        addTrackSegment();
3414                        _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
3415                    }
3416                    beginTrack = null;
3417                    foundTrack = null;
3418                    foundTrackView = null;
3419                } else if (leToolBarPanel.multiSensorButton.isSelected()) {
3420                    startMultiSensor();
3421                } else if (leToolBarPanel.sensorButton.isSelected()) {
3422                    addSensor();
3423                } else if (leToolBarPanel.signalButton.isSelected()) {
3424                    addSignalHead();
3425                } else if (leToolBarPanel.textLabelButton.isSelected()) {
3426                    addLabel();
3427                } else if (leToolBarPanel.memoryButton.isSelected()) {
3428                    addMemory();
3429                } else if (leToolBarPanel.blockContentsButton.isSelected()) {
3430                    addBlockContents();
3431                } else if (leToolBarPanel.iconLabelButton.isSelected()) {
3432                    addIcon();
3433                } else if (leToolBarPanel.shapeButton.isSelected()) {
3434                    if (selectedObject == null) {
3435                        addLayoutShape(currentPoint);
3436                        _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
3437                    } else {
3438                        LayoutShape ls = (LayoutShape) selectedObject;
3439                        ls.addPoint(currentPoint, selectedHitPointType.shapePointIndex());
3440                    }
3441                } else if (leToolBarPanel.signalMastButton.isSelected()) {
3442                    addSignalMast();
3443                } else {
3444                    log.warn("No item selected in panel edit mode");
3445                }
3446                // resizePanelBounds(false);
3447                selectedObject = null;
3448                redrawPanel();
3449            } else if ((event.isPopupTrigger() || delayedPopupTrigger) && !isDragging) {
3450                selectedObject = null;
3451                selectedHitPointType = HitPointType.NONE;
3452                whenReleased = event.getWhen();
3453                showEditPopUps(event);
3454            } else if ((selectedObject != null) && (selectedHitPointType == HitPointType.TURNOUT_CENTER)
3455                    && allControlling() && (!isMetaDown(event) && !event.isAltDown()) && !event.isPopupTrigger()
3456                    && !event.isShiftDown() && !event.isControlDown()) {
3457                // controlling turnouts, in edit mode
3458                LayoutTurnout t = (LayoutTurnout) selectedObject;
3459                t.toggleTurnout();
3460            } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.SLIP_LEFT)
3461                    || (selectedHitPointType == HitPointType.SLIP_RIGHT))
3462                    && allControlling() && (!isMetaDown(event) && !event.isAltDown()) && !event.isPopupTrigger()
3463                    && !event.isShiftDown() && !event.isControlDown()) {
3464                // controlling slips, in edit mode
3465                LayoutSlip sl = (LayoutSlip) selectedObject;
3466                sl.toggleState(selectedHitPointType);
3467            } else if ((selectedObject != null) && (HitPointType.isTurntableRayHitType(selectedHitPointType))
3468                    && allControlling() && (!isMetaDown(event) && !event.isAltDown()) && !event.isPopupTrigger()
3469                    && !event.isShiftDown() && !event.isControlDown()) {
3470                // controlling turntable, in edit mode
3471                LayoutTurntable t = (LayoutTurntable) selectedObject;
3472                t.setPosition(selectedHitPointType.turntableTrackIndex());
3473            } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.TURNOUT_CENTER)
3474                    || (selectedHitPointType == HitPointType.SLIP_CENTER)
3475                    || (selectedHitPointType == HitPointType.SLIP_LEFT)
3476                    || (selectedHitPointType == HitPointType.SLIP_RIGHT))
3477                    && allControlling() && (isMetaDown(event) && !event.isAltDown())
3478                    && !event.isShiftDown() && !event.isControlDown() && isDragging) {
3479                // We just dropped a turnout (or slip)... see if it will connect to anything
3480                hitPointCheckLayoutTurnouts((LayoutTurnout) selectedObject);
3481            } else if ((selectedObject != null) && (selectedHitPointType == HitPointType.POS_POINT)
3482                    && allControlling() && (isMetaDown(event))
3483                    && !event.isShiftDown() && !event.isControlDown() && isDragging) {
3484                // We just dropped a PositionablePoint... see if it will connect to anything
3485                PositionablePoint p = (PositionablePoint) selectedObject;
3486                if ((p.getConnect1() == null) || (p.getConnect2() == null)) {
3487                    checkPointOfPositionable(p);
3488                }
3489            }
3490
3491            if ((leToolBarPanel.trackButton.isSelected()) && (beginTrack != null) && (foundTrack != null)) {
3492                // user let up shift key before releasing the mouse when creating a track segment
3493                _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
3494                beginTrack = null;
3495                foundTrack = null;
3496                foundTrackView = null;
3497                redrawPanel();
3498            }
3499            createSelectionGroups();
3500        } else if ((selectedObject != null) && (selectedHitPointType == HitPointType.TURNOUT_CENTER)
3501                && allControlling() && !isMetaDown(event) && !event.isAltDown() && !event.isPopupTrigger()
3502                && !event.isShiftDown() && (!delayedPopupTrigger)) {
3503            // controlling turnout out of edit mode
3504            LayoutTurnout t = (LayoutTurnout) selectedObject;
3505            if (useDirectTurnoutControl) {
3506                t.setState(Turnout.CLOSED);
3507            } else {
3508                t.toggleTurnout();
3509            }
3510        } else if ((selectedObject != null) && ((selectedHitPointType == HitPointType.SLIP_LEFT)
3511                || (selectedHitPointType == HitPointType.SLIP_RIGHT))
3512                && allControlling() && !isMetaDown(event) && !event.isAltDown() && !event.isPopupTrigger()
3513                && !event.isShiftDown() && (!delayedPopupTrigger)) {
3514            // controlling slip out of edit mode
3515            LayoutSlip sl = (LayoutSlip) selectedObject;
3516            sl.toggleState(selectedHitPointType);
3517        } else if ((selectedObject != null) && (HitPointType.isTurntableRayHitType(selectedHitPointType))
3518                && allControlling() && !isMetaDown(event) && !event.isAltDown() && !event.isPopupTrigger()
3519                && !event.isShiftDown() && (!delayedPopupTrigger)) {
3520            // controlling turntable out of edit mode
3521            LayoutTurntable t = (LayoutTurntable) selectedObject;
3522            t.setPosition(selectedHitPointType.turntableTrackIndex());
3523        } else if ((event.isPopupTrigger() || delayedPopupTrigger) && (!isDragging)) {
3524            // requesting marker popup out of edit mode
3525            LocoIcon lo = checkMarkerPopUps(dLoc);
3526            if (lo != null) {
3527                showPopUp(lo, event);
3528            } else {
3529                if (findLayoutTracksHitPoint(dLoc)) {
3530                    // show popup menu
3531                    switch (foundHitPointType) {
3532                        case TURNOUT_CENTER: {
3533                            if (useDirectTurnoutControl) {
3534                                LayoutTurnout t = (LayoutTurnout) foundTrack;
3535                                t.setState(Turnout.THROWN);
3536                            } else {
3537                                foundTrackView.showPopup(event);
3538                            }
3539                            break;
3540                        }
3541
3542                        case LEVEL_XING_CENTER:
3543                        case SLIP_RIGHT:
3544                        case SLIP_LEFT: {
3545                            foundTrackView.showPopup(event);
3546                            break;
3547                        }
3548
3549                        default: {
3550                            break;
3551                        }
3552                    }
3553                }
3554                AnalogClock2Display c = checkClockPopUps(dLoc);
3555                if (c != null) {
3556                    showPopUp(c, event);
3557                } else {
3558                    SignalMastIcon sm = checkSignalMastIconPopUps(dLoc);
3559                    if (sm != null) {
3560                        showPopUp(sm, event);
3561                    } else {
3562                        PositionableLabel im = checkLabelImagePopUps(dLoc);
3563                        if (im != null) {
3564                            showPopUp(im, event);
3565                        }
3566                    }
3567                }
3568            }
3569        }
3570
3571        if (!event.isPopupTrigger() && !isDragging) {
3572            List<Positionable> selections = getSelectedItems(event);
3573            if (selections.size() > 0) {
3574                selections.get(0).doMouseReleased(event);
3575                whenReleased = event.getWhen();
3576            }
3577        }
3578
3579        // train icon needs to know when moved
3580        if (event.isPopupTrigger() && isDragging) {
3581            List<Positionable> selections = getSelectedItems(event);
3582            if (selections.size() > 0) {
3583                selections.get(0).doMouseDragged(event);
3584            }
3585        }
3586
3587        if (selectedObject != null) {
3588            // An object was selected, deselect it
3589            prevSelectedObject = selectedObject;
3590            selectedObject = null;
3591        }
3592
3593        // clear these
3594        beginTrack = null;
3595        foundTrack = null;
3596        foundTrackView = null;
3597
3598        delayedPopupTrigger = false;
3599
3600        if (isDragging) {
3601            resizePanelBounds(true);
3602            isDragging = false;
3603        }
3604
3605        requestFocusInWindow();
3606    }   // mouseReleased
3607
3608    private void showEditPopUps(@Nonnull MouseEvent event) {
3609        if (findLayoutTracksHitPoint(dLoc)) {
3610            if (HitPointType.isBezierHitType(foundHitPointType)) {
3611                getTrackSegmentView((TrackSegment) foundTrack).showBezierPopUp(event, foundHitPointType);
3612            } else if (HitPointType.isTurntableRayHitType(foundHitPointType)) {
3613                LayoutTurntable t = (LayoutTurntable) foundTrack;
3614                if (t.isTurnoutControlled()) {
3615                    LayoutTurntableView ltview = getLayoutTurntableView((LayoutTurntable) foundTrack);
3616                    ltview.showRayPopUp(event, foundHitPointType.turntableTrackIndex());
3617                }
3618            } else if (HitPointType.isPopupHitType(foundHitPointType)) {
3619                foundTrackView.showPopup(event);
3620            } else if (HitPointType.isTurnoutHitType(foundHitPointType)) {
3621                // don't curently have edit popup for these
3622            } else {
3623                log.warn("Unknown foundPointType:{}", foundHitPointType);
3624            }
3625        } else {
3626            do {
3627                TrackSegment ts = checkTrackSegmentPopUps(dLoc);
3628                if (ts != null) {
3629                    TrackSegmentView tsv = getTrackSegmentView(ts);
3630                    tsv.showPopup(event);
3631                    break;
3632                }
3633
3634                SensorIcon s = checkSensorIconPopUps(dLoc);
3635                if (s != null) {
3636                    showPopUp(s, event);
3637                    break;
3638                }
3639
3640                LocoIcon lo = checkMarkerPopUps(dLoc);
3641                if (lo != null) {
3642                    showPopUp(lo, event);
3643                    break;
3644                }
3645
3646                SignalHeadIcon sh = checkSignalHeadIconPopUps(dLoc);
3647                if (sh != null) {
3648                    showPopUp(sh, event);
3649                    break;
3650                }
3651
3652                AnalogClock2Display c = checkClockPopUps(dLoc);
3653                if (c != null) {
3654                    showPopUp(c, event);
3655                    break;
3656                }
3657
3658                MultiSensorIcon ms = checkMultiSensorPopUps(dLoc);
3659                if (ms != null) {
3660                    showPopUp(ms, event);
3661                    break;
3662                }
3663
3664                PositionableLabel lb = checkLabelImagePopUps(dLoc);
3665                if (lb != null) {
3666                    showPopUp(lb, event);
3667                    break;
3668                }
3669
3670                PositionableLabel b = checkBackgroundPopUps(dLoc);
3671                if (b != null) {
3672                    showPopUp(b, event);
3673                    break;
3674                }
3675
3676                SignalMastIcon sm = checkSignalMastIconPopUps(dLoc);
3677                if (sm != null) {
3678                    showPopUp(sm, event);
3679                    break;
3680                }
3681                LayoutShape ls = checkLayoutShapePopUps(dLoc);
3682                if (ls != null) {
3683                    ls.showShapePopUp(event, selectedHitPointType);
3684                    break;
3685                }
3686            } while (false);
3687        }
3688    }
3689
3690    /**
3691     * Select the menu items to display for the Positionable's popup.
3692     */
3693    @Override
3694    public void showPopUp(@Nonnull Positionable p, @Nonnull MouseEvent event) {
3695        assert p != null;
3696
3697        if (!((Component) p).isVisible()) {
3698            return; // component must be showing on the screen to determine its location
3699        }
3700        JPopupMenu popup = new JPopupMenu();
3701
3702        if (p.isEditable()) {
3703            JMenuItem jmi;
3704
3705            if (showAlignPopup()) {
3706                setShowAlignmentMenu(popup);
3707                popup.add(new AbstractAction(Bundle.getMessage("ButtonDelete")) {
3708                    @Override
3709                    public void actionPerformed(ActionEvent event) {
3710                        deleteSelectedItems();
3711                    }
3712                });
3713            } else {
3714                if (p.doViemMenu()) {
3715                    String objectType = p.getClass().getName();
3716                    objectType = objectType.substring(objectType.lastIndexOf('.') + 1);
3717                    jmi = popup.add(objectType);
3718                    jmi.setEnabled(false);
3719
3720                    jmi = popup.add(p.getNameString());
3721                    jmi.setEnabled(false);
3722
3723                    if (p.isPositionable()) {
3724                        setShowCoordinatesMenu(p, popup);
3725                    }
3726                    setDisplayLevelMenu(p, popup);
3727                    setPositionableMenu(p, popup);
3728                }
3729
3730                boolean popupSet = false;
3731                popupSet |= p.setRotateOrthogonalMenu(popup);
3732                popupSet |= p.setRotateMenu(popup);
3733                if (popupSet) {
3734                    popup.addSeparator();
3735                    popupSet = false;
3736                }
3737                popupSet |= p.setEditIconMenu(popup);
3738                popupSet |= p.setTextEditMenu(popup);
3739
3740                PositionablePopupUtil util = p.getPopupUtility();
3741
3742                if (util != null) {
3743                    util.setFixedTextMenu(popup);
3744                    util.setTextMarginMenu(popup);
3745                    util.setTextBorderMenu(popup);
3746                    util.setTextFontMenu(popup);
3747                    util.setBackgroundMenu(popup);
3748                    util.setTextJustificationMenu(popup);
3749                    util.setTextOrientationMenu(popup);
3750                    popup.addSeparator();
3751                    util.propertyUtil(popup);
3752                    util.setAdditionalEditPopUpMenu(popup);
3753                    popupSet = true;
3754                }
3755
3756                if (popupSet) {
3757                    popup.addSeparator();
3758                    // popupSet = false;
3759                }
3760                p.setDisableControlMenu(popup);
3761                setShowAlignmentMenu(popup);
3762
3763                // for Positionables with unique settings
3764                p.showPopUp(popup);
3765                setShowToolTipMenu(p, popup);
3766
3767                setRemoveMenu(p, popup);
3768
3769                if (p.doViemMenu()) {
3770                    setHiddenMenu(p, popup);
3771                }
3772            }
3773        } else {
3774            p.showPopUp(popup);
3775            PositionablePopupUtil util = p.getPopupUtility();
3776
3777            if (util != null) {
3778                util.setAdditionalViewPopUpMenu(popup);
3779            }
3780        }
3781        popup.show((Component) p, p.getWidth() / 2 + (int) ((getZoom() - 1.0) * p.getX()),
3782                p.getHeight() / 2 + (int) ((getZoom() - 1.0) * p.getY()));
3783
3784        /*popup.show((Component)pt, event.getX(), event.getY());*/
3785    }
3786
3787    private long whenReleased = 0; // used to identify event that was popup trigger
3788    private boolean awaitingIconChange = false;
3789
3790    @Override
3791    public void mouseClicked(@Nonnull MouseEvent event) {
3792        // initialize mouse position
3793        calcLocation(event);
3794
3795        // if alt modifier is down invert the snap to grid behaviour
3796        snapToGridInvert = event.isAltDown();
3797
3798        if (!isMetaDown(event) && !event.isPopupTrigger() && !event.isAltDown()
3799                && !awaitingIconChange && !event.isShiftDown() && !event.isControlDown()) {
3800            List<Positionable> selections = getSelectedItems(event);
3801
3802            if (selections.size() > 0) {
3803                selections.get(0).doMouseClicked(event);
3804            }
3805        } else if (event.isPopupTrigger() && (whenReleased != event.getWhen())) {
3806
3807            if (isEditable()) {
3808                selectedObject = null;
3809                selectedHitPointType = HitPointType.NONE;
3810                showEditPopUps(event);
3811            } else {
3812                LocoIcon lo = checkMarkerPopUps(dLoc);
3813
3814                if (lo != null) {
3815                    showPopUp(lo, event);
3816                }
3817            }
3818        }
3819
3820        if (event.isControlDown() && !event.isPopupTrigger()) {
3821            if (findLayoutTracksHitPoint(dLoc)) {
3822                switch (foundHitPointType) {
3823                    case POS_POINT:
3824                    case TURNOUT_CENTER:
3825                    case LEVEL_XING_CENTER:
3826                    case SLIP_LEFT:
3827                    case SLIP_RIGHT:
3828                    case TURNTABLE_CENTER: {
3829                        amendSelectionGroup(foundTrack);
3830                        break;
3831                    }
3832
3833                    default: {
3834                        break;
3835                    }
3836                }
3837            } else {
3838                PositionableLabel s = checkSensorIconPopUps(dLoc);
3839                if (s != null) {
3840                    amendSelectionGroup(s);
3841                } else {
3842                    PositionableLabel sh = checkSignalHeadIconPopUps(dLoc);
3843                    if (sh != null) {
3844                        amendSelectionGroup(sh);
3845                    } else {
3846                        PositionableLabel ms = checkMultiSensorPopUps(dLoc);
3847                        if (ms != null) {
3848                            amendSelectionGroup(ms);
3849                        } else {
3850                            PositionableLabel lb = checkLabelImagePopUps(dLoc);
3851                            if (lb != null) {
3852                                amendSelectionGroup(lb);
3853                            } else {
3854                                PositionableLabel b = checkBackgroundPopUps(dLoc);
3855                                if (b != null) {
3856                                    amendSelectionGroup(b);
3857                                } else {
3858                                    PositionableLabel sm = checkSignalMastIconPopUps(dLoc);
3859                                    if (sm != null) {
3860                                        amendSelectionGroup(sm);
3861                                    } else {
3862                                        LayoutShape ls = checkLayoutShapePopUps(dLoc);
3863                                        if (ls != null) {
3864                                            amendSelectionGroup(ls);
3865                                        }
3866                                    }
3867                                }
3868                            }
3869                        }
3870                    }
3871                }
3872            }
3873        } else if ((selectionWidth == 0) || (selectionHeight == 0)) {
3874            clearSelectionGroups();
3875        }
3876        requestFocusInWindow();
3877    }
3878
3879    private void checkPointOfPositionable(@Nonnull PositionablePoint p) {
3880        assert p != null;
3881
3882        TrackSegment t = p.getConnect1();
3883
3884        if (t == null) {
3885            t = p.getConnect2();
3886        }
3887
3888        // Nothing connected to this bit of track so ignore
3889        if (t == null) {
3890            return;
3891        }
3892        beginTrack = p;
3893        beginHitPointType = HitPointType.POS_POINT;
3894        PositionablePointView pv = getPositionablePointView(p);
3895        Point2D loc = pv.getCoordsCenter();
3896
3897        if (findLayoutTracksHitPoint(loc, true, p)) {
3898            switch (foundHitPointType) {
3899                case POS_POINT: {
3900                    PositionablePoint p2 = (PositionablePoint) foundTrack;
3901
3902                    if ((p2.getType() == PositionablePoint.PointType.ANCHOR) && p2.setTrackConnection(t)) {
3903                        if (t.getConnect1() == p) {
3904                            t.setNewConnect1(p2, foundHitPointType);
3905                        } else {
3906                            t.setNewConnect2(p2, foundHitPointType);
3907                        }
3908                        p.removeTrackConnection(t);
3909
3910                        if ((p.getConnect1() == null) && (p.getConnect2() == null)) {
3911                            removePositionablePoint(p);
3912                        }
3913                    }
3914                    break;
3915                }
3916                case TURNOUT_A:
3917                case TURNOUT_B:
3918                case TURNOUT_C:
3919                case TURNOUT_D:
3920                case SLIP_A:
3921                case SLIP_B:
3922                case SLIP_C:
3923                case SLIP_D:
3924                case LEVEL_XING_A:
3925                case LEVEL_XING_B:
3926                case LEVEL_XING_C:
3927                case LEVEL_XING_D: {
3928                    try {
3929                        if (foundTrack.getConnection(foundHitPointType) == null) {
3930                            foundTrack.setConnection(foundHitPointType, t, HitPointType.TRACK);
3931
3932                            if (t.getConnect1() == p) {
3933                                t.setNewConnect1(foundTrack, foundHitPointType);
3934                            } else {
3935                                t.setNewConnect2(foundTrack, foundHitPointType);
3936                            }
3937                            p.removeTrackConnection(t);
3938
3939                            if ((p.getConnect1() == null) && (p.getConnect2() == null)) {
3940                                removePositionablePoint(p);
3941                            }
3942                        }
3943                    } catch (JmriException e) {
3944                        log.debug("Unable to set location");
3945                    }
3946                    break;
3947                }
3948
3949                default: {
3950                    if (HitPointType.isTurntableRayHitType(foundHitPointType)) {
3951                        LayoutTurntable tt = (LayoutTurntable) foundTrack;
3952                        int ray = foundHitPointType.turntableTrackIndex();
3953
3954                        if (tt.getRayConnectIndexed(ray) == null) {
3955                            tt.setRayConnect(t, ray);
3956
3957                            if (t.getConnect1() == p) {
3958                                t.setNewConnect1(tt, foundHitPointType);
3959                            } else {
3960                                t.setNewConnect2(tt, foundHitPointType);
3961                            }
3962                            p.removeTrackConnection(t);
3963
3964                            if ((p.getConnect1() == null) && (p.getConnect2() == null)) {
3965                                removePositionablePoint(p);
3966                            }
3967                        }
3968                    } else {
3969                        log.debug("No valid point, so will quit");
3970                        return;
3971                    }
3972                    break;
3973                }
3974            }
3975            redrawPanel();
3976
3977            if (t.getLayoutBlock() != null) {
3978                getLEAuxTools().setBlockConnectivityChanged();
3979            }
3980        }
3981        beginTrack = null;
3982    }
3983
3984    // We just dropped a turnout... see if it will connect to anything
3985    private void hitPointCheckLayoutTurnouts(@Nonnull LayoutTurnout lt) {
3986        beginTrack = lt;
3987
3988        LayoutTurnoutView ltv = getLayoutTurnoutView(lt);
3989
3990        if (lt.getConnectA() == null) {
3991            if (lt instanceof LayoutSlip) {
3992                beginHitPointType = HitPointType.SLIP_A;
3993            } else {
3994                beginHitPointType = HitPointType.TURNOUT_A;
3995            }
3996            dLoc = ltv.getCoordsA();
3997            hitPointCheckLayoutTurnoutSubs(dLoc);
3998        }
3999
4000        if (lt.getConnectB() == null) {
4001            if (lt instanceof LayoutSlip) {
4002                beginHitPointType = HitPointType.SLIP_B;
4003            } else {
4004                beginHitPointType = HitPointType.TURNOUT_B;
4005            }
4006            dLoc = ltv.getCoordsB();
4007            hitPointCheckLayoutTurnoutSubs(dLoc);
4008        }
4009
4010        if (lt.getConnectC() == null) {
4011            if (lt instanceof LayoutSlip) {
4012                beginHitPointType = HitPointType.SLIP_C;
4013            } else {
4014                beginHitPointType = HitPointType.TURNOUT_C;
4015            }
4016            dLoc = ltv.getCoordsC();
4017            hitPointCheckLayoutTurnoutSubs(dLoc);
4018        }
4019
4020        if ((lt.getConnectD() == null) && (lt.isTurnoutTypeXover() || lt.isTurnoutTypeSlip())) {
4021            if (lt instanceof LayoutSlip) {
4022                beginHitPointType = HitPointType.SLIP_D;
4023            } else {
4024                beginHitPointType = HitPointType.TURNOUT_D;
4025            }
4026            dLoc = ltv.getCoordsD();
4027            hitPointCheckLayoutTurnoutSubs(dLoc);
4028        }
4029        beginTrack = null;
4030        foundTrack = null;
4031        foundTrackView = null;
4032    }
4033
4034    private void hitPointCheckLayoutTurnoutSubs(@Nonnull Point2D dLoc) {
4035        assert dLoc != null;
4036
4037        if (findLayoutTracksHitPoint(dLoc, true)) {
4038            switch (foundHitPointType) {
4039                case POS_POINT: {
4040                    PositionablePoint p2 = (PositionablePoint) foundTrack;
4041
4042                    if (((p2.getConnect1() == null) && (p2.getConnect2() != null))
4043                            || ((p2.getConnect1() != null) && (p2.getConnect2() == null))) {
4044                        TrackSegment t = p2.getConnect1();
4045
4046                        if (t == null) {
4047                            t = p2.getConnect2();
4048                        }
4049
4050                        if (t == null) {
4051                            return;
4052                        }
4053                        LayoutTurnout lt = (LayoutTurnout) beginTrack;
4054                        try {
4055                            if (lt.getConnection(beginHitPointType) == null) {
4056                                lt.setConnection(beginHitPointType, t, HitPointType.TRACK);
4057                                p2.removeTrackConnection(t);
4058
4059                                if (t.getConnect1() == p2) {
4060                                    t.setNewConnect1(lt, beginHitPointType);
4061                                } else {
4062                                    t.setNewConnect2(lt, beginHitPointType);
4063                                }
4064                                removePositionablePoint(p2);
4065                            }
4066
4067                            if (t.getLayoutBlock() != null) {
4068                                getLEAuxTools().setBlockConnectivityChanged();
4069                            }
4070                        } catch (JmriException e) {
4071                            log.debug("Unable to set location");
4072                        }
4073                    }
4074                    break;
4075                }
4076
4077                case TURNOUT_A:
4078                case TURNOUT_B:
4079                case TURNOUT_C:
4080                case TURNOUT_D:
4081                case SLIP_A:
4082                case SLIP_B:
4083                case SLIP_C:
4084                case SLIP_D: {
4085                    LayoutTurnout ft = (LayoutTurnout) foundTrack;
4086                    addTrackSegment();
4087
4088                    if ((ft.getTurnoutType() == LayoutTurnout.TurnoutType.RH_TURNOUT) || (ft.getTurnoutType() == LayoutTurnout.TurnoutType.LH_TURNOUT)) {
4089                        rotateTurnout(ft);
4090                    }
4091
4092                    // Assign a block to the new zero length track segment.
4093                    ((LayoutTurnoutView) foundTrackView).setTrackSegmentBlock(foundHitPointType, true);
4094                    break;
4095                }
4096
4097                default: {
4098                    log.warn("Unexpected foundPointType {} in hitPointCheckLayoutTurnoutSubs", foundHitPointType);
4099                    break;
4100                }
4101            }
4102        }
4103    }
4104
4105    private void rotateTurnout(@Nonnull LayoutTurnout t) {
4106        assert t != null;
4107
4108        LayoutTurnoutView tv = getLayoutTurnoutView(t);
4109
4110        LayoutTurnout be = (LayoutTurnout) beginTrack;
4111        LayoutTurnoutView bev = getLayoutTurnoutView(be);
4112
4113        if (((beginHitPointType == HitPointType.TURNOUT_A) && ((be.getConnectB() != null) || (be.getConnectC() != null)))
4114                || ((beginHitPointType == HitPointType.TURNOUT_B) && ((be.getConnectA() != null) || (be.getConnectC() != null)))
4115                || ((beginHitPointType == HitPointType.TURNOUT_C) && ((be.getConnectB() != null) || (be.getConnectA() != null)))) {
4116            return;
4117        }
4118
4119        if ((be.getTurnoutType() != LayoutTurnout.TurnoutType.RH_TURNOUT) && (be.getTurnoutType() != LayoutTurnout.TurnoutType.LH_TURNOUT)) {
4120            return;
4121        }
4122
4123        Point2D c, diverg, xy2;
4124
4125        if ((foundHitPointType == HitPointType.TURNOUT_C) && (beginHitPointType == HitPointType.TURNOUT_C)) {
4126            c = tv.getCoordsA();
4127            diverg = tv.getCoordsB();
4128            xy2 = MathUtil.subtract(c, diverg);
4129        } else if ((foundHitPointType == HitPointType.TURNOUT_C)
4130                && ((beginHitPointType == HitPointType.TURNOUT_A) || (beginHitPointType == HitPointType.TURNOUT_B))) {
4131
4132            c = tv.getCoordsCenter();
4133            diverg = tv.getCoordsC();
4134
4135            if (beginHitPointType == HitPointType.TURNOUT_A) {
4136                xy2 = MathUtil.subtract(bev.getCoordsB(), bev.getCoordsA());
4137            } else {
4138                xy2 = MathUtil.subtract(bev.getCoordsA(), bev.getCoordsB());
4139            }
4140        } else if (foundHitPointType == HitPointType.TURNOUT_B) {
4141            c = tv.getCoordsA();
4142            diverg = tv.getCoordsB();
4143
4144            switch (beginHitPointType) {
4145                case TURNOUT_B:
4146                    xy2 = MathUtil.subtract(bev.getCoordsA(), bev.getCoordsB());
4147                    break;
4148                case TURNOUT_A:
4149                    xy2 = MathUtil.subtract(bev.getCoordsB(), bev.getCoordsA());
4150                    break;
4151                case TURNOUT_C:
4152                default:
4153                    xy2 = MathUtil.subtract(bev.getCoordsCenter(), bev.getCoordsC());
4154                    break;
4155            }
4156        } else if (foundHitPointType == HitPointType.TURNOUT_A) {
4157            c = tv.getCoordsA();
4158            diverg = tv.getCoordsB();
4159
4160            switch (beginHitPointType) {
4161                case TURNOUT_A:
4162                    xy2 = MathUtil.subtract(bev.getCoordsA(), bev.getCoordsB());
4163                    break;
4164                case TURNOUT_B:
4165                    xy2 = MathUtil.subtract(bev.getCoordsB(), bev.getCoordsA());
4166                    break;
4167                case TURNOUT_C:
4168                default:
4169                    xy2 = MathUtil.subtract(bev.getCoordsC(), bev.getCoordsCenter());
4170                    break;
4171            }
4172        } else {
4173            return;
4174        }
4175        Point2D xy = MathUtil.subtract(diverg, c);
4176        double radius = Math.toDegrees(Math.atan2(xy.getY(), xy.getX()));
4177        double eRadius = Math.toDegrees(Math.atan2(xy2.getY(), xy2.getX()));
4178        bev.rotateCoords(radius - eRadius);
4179
4180        Point2D conCord = bev.getCoordsA();
4181        Point2D tCord = tv.getCoordsC();
4182
4183        if (foundHitPointType == HitPointType.TURNOUT_B) {
4184            tCord = tv.getCoordsB();
4185        }
4186
4187        if (foundHitPointType == HitPointType.TURNOUT_A) {
4188            tCord = tv.getCoordsA();
4189        }
4190
4191        switch (beginHitPointType) {
4192            case TURNOUT_A:
4193                conCord = bev.getCoordsA();
4194                break;
4195            case TURNOUT_B:
4196                conCord = bev.getCoordsB();
4197                break;
4198            case TURNOUT_C:
4199                conCord = bev.getCoordsC();
4200                break;
4201            default:
4202                break;
4203        }
4204        xy = MathUtil.subtract(conCord, tCord);
4205        Point2D offset = MathUtil.subtract(bev.getCoordsCenter(), xy);
4206        bev.setCoordsCenter(offset);
4207    }
4208
4209    public List<Positionable> _positionableSelection = new ArrayList<>();
4210    public List<LayoutTrack> _layoutTrackSelection = new ArrayList<>();
4211    public List<LayoutShape> _layoutShapeSelection = new ArrayList<>();
4212
4213    @Nonnull
4214    public List<Positionable> getPositionalSelection() {
4215        return _positionableSelection;
4216    }
4217
4218    @Nonnull
4219    public List<LayoutTrack> getLayoutTrackSelection() {
4220        return _layoutTrackSelection;
4221    }
4222
4223    @Nonnull
4224    public List<LayoutShape> getLayoutShapeSelection() {
4225        return _layoutShapeSelection;
4226    }
4227
4228    private void createSelectionGroups() {
4229        Rectangle2D selectionRect = getSelectionRect();
4230
4231        getContents().forEach((o) -> {
4232            if (selectionRect.contains(o.getLocation())) {
4233
4234                log.trace("found item o of class {}", o.getClass());
4235                if (!_positionableSelection.contains(o)) {
4236                    _positionableSelection.add(o);
4237                }
4238            }
4239        });
4240
4241        getLayoutTracks().forEach((lt) -> {
4242            LayoutTrackView ltv = getLayoutTrackView(lt);
4243            Point2D center = ltv.getCoordsCenter();
4244            if (selectionRect.contains(center)) {
4245                if (!_layoutTrackSelection.contains(lt)) {
4246                    _layoutTrackSelection.add(lt);
4247                }
4248            }
4249        });
4250        assignBlockToSelectionMenuItem.setEnabled(_layoutTrackSelection.size() > 0);
4251
4252        layoutShapes.forEach((ls) -> {
4253            if (selectionRect.intersects(ls.getBounds())) {
4254                if (!_layoutShapeSelection.contains(ls)) {
4255                    _layoutShapeSelection.add(ls);
4256                }
4257            }
4258        });
4259        redrawPanel();
4260    }
4261
4262    public void clearSelectionGroups() {
4263        selectionActive = false;
4264        _positionableSelection.clear();
4265        _layoutTrackSelection.clear();
4266        assignBlockToSelectionMenuItem.setEnabled(false);
4267        _layoutShapeSelection.clear();
4268    }
4269
4270    private boolean noWarnGlobalDelete = false;
4271
4272    private void deleteSelectedItems() {
4273        if (!noWarnGlobalDelete) {
4274            int selectedValue = JOptionPane.showOptionDialog(this,
4275                    Bundle.getMessage("Question6"), Bundle.getMessage("WarningTitle"),
4276                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
4277                    new Object[]{Bundle.getMessage("ButtonYes"),
4278                        Bundle.getMessage("ButtonNo"),
4279                        Bundle.getMessage("ButtonYesPlus")},
4280                    Bundle.getMessage("ButtonNo"));
4281
4282            if (selectedValue == JOptionPane.NO_OPTION) {
4283                return; // return without creating if "No" response
4284            }
4285
4286            if (selectedValue == JOptionPane.CANCEL_OPTION) {
4287                // Suppress future warnings, and continue
4288                noWarnGlobalDelete = true;
4289            }
4290        }
4291
4292        _positionableSelection.forEach(this::remove);
4293
4294        _layoutTrackSelection.forEach((lt) -> {
4295            if (lt instanceof PositionablePoint) {
4296                boolean oldWarning = noWarnPositionablePoint;
4297                noWarnPositionablePoint = true;
4298                removePositionablePoint((PositionablePoint) lt);
4299                noWarnPositionablePoint = oldWarning;
4300            } else if (lt instanceof LevelXing) {
4301                boolean oldWarning = noWarnLevelXing;
4302                noWarnLevelXing = true;
4303                removeLevelXing((LevelXing) lt);
4304                noWarnLevelXing = oldWarning;
4305            } else if (lt instanceof LayoutSlip) {
4306                boolean oldWarning = noWarnSlip;
4307                noWarnSlip = true;
4308                removeLayoutSlip((LayoutSlip) lt);
4309                noWarnSlip = oldWarning;
4310            } else if (lt instanceof LayoutTurntable) {
4311                boolean oldWarning = noWarnTurntable;
4312                noWarnTurntable = true;
4313                removeTurntable((LayoutTurntable) lt);
4314                noWarnTurntable = oldWarning;
4315            } else if (lt instanceof LayoutTurnout) {  //<== this includes LayoutSlips
4316                boolean oldWarning = noWarnLayoutTurnout;
4317                noWarnLayoutTurnout = true;
4318                removeLayoutTurnout((LayoutTurnout) lt);
4319                noWarnLayoutTurnout = oldWarning;
4320            }
4321        });
4322
4323        layoutShapes.removeAll(_layoutShapeSelection);
4324
4325        clearSelectionGroups();
4326        redrawPanel();
4327    }
4328
4329    private void amendSelectionGroup(@Nonnull Positionable p) {
4330        assert p != null;
4331
4332        if (_positionableSelection.contains(p)) {
4333            _positionableSelection.remove(p);
4334        } else {
4335            _positionableSelection.add(p);
4336        }
4337        redrawPanel();
4338    }
4339
4340    public void amendSelectionGroup(@Nonnull LayoutTrack p) {
4341        assert p != null;
4342
4343        if (_layoutTrackSelection.contains(p)) {
4344            _layoutTrackSelection.remove(p);
4345        } else {
4346            _layoutTrackSelection.add(p);
4347        }
4348        assignBlockToSelectionMenuItem.setEnabled(_layoutTrackSelection.size() > 0);
4349        redrawPanel();
4350    }
4351
4352    public void amendSelectionGroup(@Nonnull LayoutShape ls) {
4353        assert ls != null;
4354
4355        if (_layoutShapeSelection.contains(ls)) {
4356            _layoutShapeSelection.remove(ls);
4357        } else {
4358            _layoutShapeSelection.add(ls);
4359        }
4360        redrawPanel();
4361    }
4362
4363    public void alignSelection(boolean alignX) {
4364        Point2D minPoint = MathUtil.infinityPoint2D;
4365        Point2D maxPoint = MathUtil.zeroPoint2D;
4366        Point2D sumPoint = MathUtil.zeroPoint2D;
4367        int cnt = 0;
4368
4369        for (Positionable comp : _positionableSelection) {
4370            if (!getFlag(Editor.OPTION_POSITION, comp.isPositionable())) {
4371                continue;   // skip non-positionables
4372            }
4373            Point2D p = MathUtil.pointToPoint2D(comp.getLocation());
4374            minPoint = MathUtil.min(minPoint, p);
4375            maxPoint = MathUtil.max(maxPoint, p);
4376            sumPoint = MathUtil.add(sumPoint, p);
4377            cnt++;
4378        }
4379
4380        for (LayoutTrack lt : _layoutTrackSelection) {
4381            LayoutTrackView ltv = getLayoutTrackView(lt);
4382            Point2D p = ltv.getCoordsCenter();
4383            minPoint = MathUtil.min(minPoint, p);
4384            maxPoint = MathUtil.max(maxPoint, p);
4385            sumPoint = MathUtil.add(sumPoint, p);
4386            cnt++;
4387        }
4388
4389        for (LayoutShape ls : _layoutShapeSelection) {
4390            Point2D p = ls.getCoordsCenter();
4391            minPoint = MathUtil.min(minPoint, p);
4392            maxPoint = MathUtil.max(maxPoint, p);
4393            sumPoint = MathUtil.add(sumPoint, p);
4394            cnt++;
4395        }
4396
4397        Point2D avePoint = MathUtil.divide(sumPoint, cnt);
4398        int aveX = (int) avePoint.getX();
4399        int aveY = (int) avePoint.getY();
4400
4401        for (Positionable comp : _positionableSelection) {
4402            if (!getFlag(Editor.OPTION_POSITION, comp.isPositionable())) {
4403                continue;   // skip non-positionables
4404            }
4405
4406            if (alignX) {
4407                comp.setLocation(aveX, comp.getY());
4408            } else {
4409                comp.setLocation(comp.getX(), aveY);
4410            }
4411        }
4412
4413        _layoutTrackSelection.forEach((lt) -> {
4414            LayoutTrackView ltv = getLayoutTrackView(lt);
4415            if (alignX) {
4416                ltv.setCoordsCenter(new Point2D.Double(aveX, ltv.getCoordsCenter().getY()));
4417            } else {
4418                ltv.setCoordsCenter(new Point2D.Double(ltv.getCoordsCenter().getX(), aveY));
4419            }
4420        });
4421
4422        _layoutShapeSelection.forEach((ls) -> {
4423            if (alignX) {
4424                ls.setCoordsCenter(new Point2D.Double(aveX, ls.getCoordsCenter().getY()));
4425            } else {
4426                ls.setCoordsCenter(new Point2D.Double(ls.getCoordsCenter().getX(), aveY));
4427            }
4428        });
4429
4430        redrawPanel();
4431    }
4432
4433    private boolean showAlignPopup() {
4434        return ((_positionableSelection.size() > 0)
4435                || (_layoutTrackSelection.size() > 0)
4436                || (_layoutShapeSelection.size() > 0));
4437    }
4438
4439    /**
4440     * Offer actions to align the selected Positionable items either
4441     * Horizontally (at average y coord) or Vertically (at average x coord).
4442     *
4443     * @param popup the JPopupMenu to add alignment menu to
4444     * @return true if alignment menu added
4445     */
4446    public boolean setShowAlignmentMenu(@Nonnull JPopupMenu popup) {
4447        if (showAlignPopup()) {
4448            JMenu edit = new JMenu(Bundle.getMessage("EditAlignment"));
4449            edit.add(new AbstractAction(Bundle.getMessage("AlignX")) {
4450                @Override
4451                public void actionPerformed(ActionEvent event) {
4452                    alignSelection(true);
4453                }
4454            });
4455            edit.add(new AbstractAction(Bundle.getMessage("AlignY")) {
4456                @Override
4457                public void actionPerformed(ActionEvent event) {
4458                    alignSelection(false);
4459                }
4460            });
4461            popup.add(edit);
4462
4463            return true;
4464        }
4465        return false;
4466    }
4467
4468    @Override
4469    public void keyPressed(@Nonnull KeyEvent event) {
4470        if (event.getKeyCode() == KeyEvent.VK_DELETE) {
4471            deleteSelectedItems();
4472            return;
4473        }
4474
4475        double deltaX = returnDeltaPositionX(event);
4476        double deltaY = returnDeltaPositionY(event);
4477
4478        if ((deltaX != 0) || (deltaY != 0)) {
4479            selectionX += deltaX;
4480            selectionY += deltaY;
4481
4482            Point2D delta = new Point2D.Double(deltaX, deltaY);
4483            _positionableSelection.forEach((c) -> {
4484                Point2D newPoint = c.getLocation();
4485                if ((c instanceof MemoryIcon) && (c.getPopupUtility().getFixedWidth() == 0)) {
4486                    MemoryIcon pm = (MemoryIcon) c;
4487                    newPoint = new Point2D.Double(pm.getOriginalX(), pm.getOriginalY());
4488                }
4489                newPoint = MathUtil.add(newPoint, delta);
4490                newPoint = MathUtil.max(MathUtil.zeroPoint2D, newPoint);
4491                c.setLocation(MathUtil.point2DToPoint(newPoint));
4492            });
4493
4494            _layoutTrackSelection.forEach((lt) -> {
4495                LayoutTrackView ltv = getLayoutTrackView(lt);
4496                Point2D newPoint = MathUtil.add(ltv.getCoordsCenter(), delta);
4497                newPoint = MathUtil.max(MathUtil.zeroPoint2D, newPoint);
4498                getLayoutTrackView(lt).setCoordsCenter(newPoint);
4499            });
4500
4501            _layoutShapeSelection.forEach((ls) -> {
4502                Point2D newPoint = MathUtil.add(ls.getCoordsCenter(), delta);
4503                newPoint = MathUtil.max(MathUtil.zeroPoint2D, newPoint);
4504                ls.setCoordsCenter(newPoint);
4505            });
4506            redrawPanel();
4507            return;
4508        }
4509        getLayoutEditorToolBarPanel().keyPressed(event);
4510    }
4511
4512    private double returnDeltaPositionX(@Nonnull KeyEvent event) {
4513        double result = 0.0;
4514        double amount = event.isShiftDown() ? 5.0 : 1.0;
4515
4516        switch (event.getKeyCode()) {
4517            case KeyEvent.VK_LEFT: {
4518                result = -amount;
4519                break;
4520            }
4521
4522            case KeyEvent.VK_RIGHT: {
4523                result = +amount;
4524                break;
4525            }
4526
4527            default: {
4528                break;
4529            }
4530        }
4531        return result;
4532    }
4533
4534    private double returnDeltaPositionY(@Nonnull KeyEvent event) {
4535        double result = 0.0;
4536        double amount = event.isShiftDown() ? 5.0 : 1.0;
4537
4538        switch (event.getKeyCode()) {
4539            case KeyEvent.VK_UP: {
4540                result = -amount;
4541                break;
4542            }
4543
4544            case KeyEvent.VK_DOWN: {
4545                result = +amount;
4546                break;
4547            }
4548
4549            default: {
4550                break;
4551            }
4552        }
4553        return result;
4554    }
4555
4556    int _prevNumSel = 0;
4557
4558    @Override
4559    public void mouseMoved(@Nonnull MouseEvent event) {
4560        // initialize mouse position
4561        calcLocation(event);
4562
4563        // if alt modifier is down invert the snap to grid behaviour
4564        snapToGridInvert = event.isAltDown();
4565
4566        if (isEditable()) {
4567            leToolBarPanel.setLocationText(dLoc);
4568        }
4569        List<Positionable> selections = getSelectedItems(event);
4570        Positionable selection = null;
4571        int numSel = selections.size();
4572
4573        if (numSel > 0) {
4574            selection = selections.get(0);
4575        }
4576
4577        if ((selection != null) && (selection.getDisplayLevel() > Editor.BKG) && selection.showToolTip()) {
4578            showToolTip(selection, event);
4579        } else {
4580            super.setToolTip(null);
4581        }
4582
4583        if (numSel != _prevNumSel) {
4584            redrawPanel();
4585            _prevNumSel = numSel;
4586        }
4587
4588        if (findLayoutTracksHitPoint(dLoc)) {
4589            // log.debug("foundTrack: {}", foundTrack);
4590            if (HitPointType.isControlHitType(foundHitPointType)) {
4591                _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
4592            } else {
4593                _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
4594            }
4595            foundTrack = null;
4596            foundHitPointType = HitPointType.NONE;
4597        } else {
4598            _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
4599        }
4600    }   // mouseMoved
4601
4602    private boolean isDragging = false;
4603
4604    @Override
4605    public void mouseDragged(@Nonnull MouseEvent event) {
4606        // initialize mouse position
4607        calcLocation(event);
4608
4609        // ignore this event if still at the original point
4610        if ((!isDragging) && (xLoc == getAnchorX()) && (yLoc == getAnchorY())) {
4611            return;
4612        }
4613
4614        // if alt modifier is down invert the snap to grid behaviour
4615        snapToGridInvert = event.isAltDown();
4616
4617        // process this mouse dragged event
4618        if (isEditable()) {
4619            leToolBarPanel.setLocationText(dLoc);
4620        }
4621        currentPoint = MathUtil.add(dLoc, startDelta);
4622        // don't allow negative placement, objects could become unreachable
4623        currentPoint = MathUtil.max(currentPoint, MathUtil.zeroPoint2D);
4624
4625        if ((selectedObject != null) && (isMetaDown(event) || event.isAltDown())
4626                && (selectedHitPointType == HitPointType.MARKER)) {
4627            // marker moves regardless of editMode or positionable
4628            PositionableLabel pl = (PositionableLabel) selectedObject;
4629            pl.setLocation((int) currentPoint.getX(), (int) currentPoint.getY());
4630            isDragging = true;
4631            redrawPanel();
4632            return;
4633        }
4634
4635        if (isEditable()) {
4636            if ((selectedObject != null) && isMetaDown(event) && allPositionable()) {
4637                if (snapToGridOnMove != snapToGridInvert) {
4638                    // this snaps currentPoint to the grid
4639                    currentPoint = MathUtil.granulize(currentPoint, gContext.getGridSize());
4640                    xLoc = (int) currentPoint.getX();
4641                    yLoc = (int) currentPoint.getY();
4642                    leToolBarPanel.setLocationText(currentPoint);
4643                }
4644
4645                if ((_positionableSelection.size() > 0)
4646                        || (_layoutTrackSelection.size() > 0)
4647                        || (_layoutShapeSelection.size() > 0)) {
4648                    Point2D lastPoint = new Point2D.Double(_lastX, _lastY);
4649                    Point2D offset = MathUtil.subtract(currentPoint, lastPoint);
4650                    Point2D newPoint;
4651
4652                    for (Positionable c : _positionableSelection) {
4653                        if ((c instanceof MemoryIcon) && (c.getPopupUtility().getFixedWidth() == 0)) {
4654                            MemoryIcon pm = (MemoryIcon) c;
4655                            newPoint = new Point2D.Double(pm.getOriginalX(), pm.getOriginalY());
4656                        } else {
4657                            newPoint = c.getLocation();
4658                        }
4659                        newPoint = MathUtil.add(newPoint, offset);
4660                        // don't allow negative placement, objects could become unreachable
4661                        newPoint = MathUtil.max(newPoint, MathUtil.zeroPoint2D);
4662                        c.setLocation(MathUtil.point2DToPoint(newPoint));
4663                    }
4664
4665                    for (LayoutTrack lt : _layoutTrackSelection) {
4666                        LayoutTrackView ltv = getLayoutTrackView(lt);
4667                        Point2D center = ltv.getCoordsCenter();
4668                        newPoint = MathUtil.add(center, offset);
4669                        // don't allow negative placement, objects could become unreachable
4670                        newPoint = MathUtil.max(newPoint, MathUtil.zeroPoint2D);
4671                        getLayoutTrackView(lt).setCoordsCenter(newPoint);
4672                    }
4673
4674                    for (LayoutShape ls : _layoutShapeSelection) {
4675                        Point2D center = ls.getCoordsCenter();
4676                        newPoint = MathUtil.add(center, offset);
4677                        // don't allow negative placement, objects could become unreachable
4678                        newPoint = MathUtil.max(newPoint, MathUtil.zeroPoint2D);
4679                        ls.setCoordsCenter(newPoint);
4680                    }
4681
4682                    _lastX = xLoc;
4683                    _lastY = yLoc;
4684                } else {
4685                    switch (selectedHitPointType) {
4686                        case POS_POINT:
4687                        case TURNOUT_CENTER:
4688                        case LEVEL_XING_CENTER:
4689                        case SLIP_LEFT:
4690                        case SLIP_RIGHT:
4691                        case TURNTABLE_CENTER: {
4692                            getLayoutTrackView((LayoutTrack) selectedObject).setCoordsCenter(currentPoint);
4693                            isDragging = true;
4694                            break;
4695                        }
4696
4697                        case TURNOUT_A: {
4698                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsA(currentPoint);
4699                            break;
4700                        }
4701
4702                        case TURNOUT_B: {
4703                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsB(currentPoint);
4704                            break;
4705                        }
4706
4707                        case TURNOUT_C: {
4708                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsC(currentPoint);
4709                            break;
4710                        }
4711
4712                        case TURNOUT_D: {
4713                            getLayoutTurnoutView((LayoutTurnout) selectedObject).setCoordsD(currentPoint);
4714                            break;
4715                        }
4716
4717                        case LEVEL_XING_A: {
4718                            getLevelXingView((LevelXing) selectedObject).setCoordsA(currentPoint);
4719                            break;
4720                        }
4721
4722                        case LEVEL_XING_B: {
4723                            getLevelXingView((LevelXing) selectedObject).setCoordsB(currentPoint);
4724                            break;
4725                        }
4726
4727                        case LEVEL_XING_C: {
4728                            getLevelXingView((LevelXing) selectedObject).setCoordsC(currentPoint);
4729                            break;
4730                        }
4731
4732                        case LEVEL_XING_D: {
4733                            getLevelXingView((LevelXing) selectedObject).setCoordsD(currentPoint);
4734                            break;
4735                        }
4736
4737                        case SLIP_A: {
4738                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsA(currentPoint);
4739                            break;
4740                        }
4741
4742                        case SLIP_B: {
4743                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsB(currentPoint);
4744                            break;
4745                        }
4746
4747                        case SLIP_C: {
4748                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsC(currentPoint);
4749                            break;
4750                        }
4751
4752                        case SLIP_D: {
4753                            getLayoutSlipView((LayoutSlip) selectedObject).setCoordsD(currentPoint);
4754                            break;
4755                        }
4756
4757                        case LAYOUT_POS_LABEL:
4758                        case MULTI_SENSOR: {
4759                            PositionableLabel pl = (PositionableLabel) selectedObject;
4760
4761                            if (pl.isPositionable()) {
4762                                pl.setLocation((int) currentPoint.getX(), (int) currentPoint.getY());
4763                                isDragging = true;
4764                            }
4765                            break;
4766                        }
4767
4768                        case LAYOUT_POS_JCOMP: {
4769                            PositionableJComponent c = (PositionableJComponent) selectedObject;
4770
4771                            if (c.isPositionable()) {
4772                                c.setLocation((int) currentPoint.getX(), (int) currentPoint.getY());
4773                                isDragging = true;
4774                            }
4775                            break;
4776                        }
4777
4778                        case TRACK_CIRCLE_CENTRE: {
4779                            TrackSegmentView tv = getTrackSegmentView((TrackSegment) selectedObject);
4780                            tv.reCalculateTrackSegmentAngle(currentPoint.getX(), currentPoint.getY());
4781                            break;
4782                        }
4783
4784                        default: {
4785                            if (HitPointType.isBezierHitType(foundHitPointType)) {
4786                                int index = selectedHitPointType.bezierPointIndex();
4787                                getTrackSegmentView((TrackSegment) selectedObject).setBezierControlPoint(currentPoint, index);
4788                            } else if ((selectedHitPointType == HitPointType.SHAPE_CENTER)) {
4789                                ((LayoutShape) selectedObject).setCoordsCenter(currentPoint);
4790                            } else if (HitPointType.isShapePointOffsetHitPointType(selectedHitPointType)) {
4791                                int index = selectedHitPointType.shapePointIndex();
4792                                ((LayoutShape) selectedObject).setPoint(index, currentPoint);
4793                            } else if (HitPointType.isTurntableRayHitType(selectedHitPointType)) {
4794                                LayoutTurntable turn = (LayoutTurntable) selectedObject;
4795                                LayoutTurntableView turnView = getLayoutTurntableView(turn);
4796                                turnView.setRayCoordsIndexed(currentPoint.getX(), currentPoint.getY(),
4797                                        selectedHitPointType.turntableTrackIndex());
4798                            }
4799                            break;
4800                        }
4801                    }
4802                }
4803            } else if ((beginTrack != null)
4804                    && event.isShiftDown()
4805                    && leToolBarPanel.trackButton.isSelected()) {
4806                // dragging from first end of Track Segment
4807                currentLocation = new Point2D.Double(xLoc, yLoc);
4808                boolean needResetCursor = (foundTrack != null);
4809
4810                if (findLayoutTracksHitPoint(currentLocation, true)) {
4811                    // have match to free connection point, change cursor
4812                    _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
4813                } else if (needResetCursor) {
4814                    _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
4815                }
4816            } else if (event.isShiftDown()
4817                    && leToolBarPanel.shapeButton.isSelected() && (selectedObject != null)) {
4818                // dragging from end of shape
4819                currentLocation = new Point2D.Double(xLoc, yLoc);
4820            } else if (selectionActive && !event.isShiftDown() && !isMetaDown(event)) {
4821                selectionWidth = xLoc - selectionX;
4822                selectionHeight = yLoc - selectionY;
4823            }
4824            redrawPanel();
4825        } else {
4826            Rectangle r = new Rectangle(event.getX(), event.getY(), 1, 1);
4827            ((JComponent) event.getSource()).scrollRectToVisible(r);
4828        }   // if (isEditable())
4829    }   // mouseDragged
4830
4831    @Override
4832    public void mouseEntered(@Nonnull MouseEvent event) {
4833        _targetPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
4834    }
4835
4836    /**
4837     * Add an Anchor point.
4838     */
4839    public void addAnchor() {
4840        addAnchor(currentPoint);
4841    }
4842
4843    @Nonnull
4844    public PositionablePoint addAnchor(@Nonnull Point2D p) {
4845        assert p != null;
4846
4847        // get unique name
4848        String name = finder.uniqueName("A", ++numAnchors);
4849
4850        // create object
4851        PositionablePoint o = new PositionablePoint(name,
4852                PositionablePoint.PointType.ANCHOR, this);
4853        PositionablePointView pv = new PositionablePointView(o, currentPoint, this);
4854        addLayoutTrack(o, pv);
4855
4856        setDirty();
4857
4858        return o;
4859    }
4860
4861    /**
4862     * Add an End Bumper point.
4863     */
4864    public void addEndBumper() {
4865        // get unique name
4866        String name = finder.uniqueName("EB", ++numEndBumpers);
4867
4868        // create object
4869        PositionablePoint o = new PositionablePoint(name,
4870                PositionablePoint.PointType.END_BUMPER, this);
4871        PositionablePointView pv = new PositionablePointView(o, currentPoint, this);
4872        addLayoutTrack(o, pv);
4873
4874        setDirty();
4875    }
4876
4877    /**
4878     * Add an Edge Connector point.
4879     */
4880    public void addEdgeConnector() {
4881        // get unique name
4882        String name = finder.uniqueName("EC", ++numEdgeConnectors);
4883
4884        // create object
4885        PositionablePoint o = new PositionablePoint(name,
4886                PositionablePoint.PointType.EDGE_CONNECTOR, this);
4887        PositionablePointView pv = new PositionablePointView(o, currentPoint, this);
4888        addLayoutTrack(o, pv);
4889
4890        setDirty();
4891    }
4892
4893    /**
4894     * Add a Track Segment
4895     */
4896    public void addTrackSegment() {
4897        // get unique name
4898        String name = finder.uniqueName("T", ++numTrackSegments);
4899
4900        // create object
4901        newTrack = new TrackSegment(name, beginTrack, beginHitPointType,
4902                foundTrack, foundHitPointType,
4903                leToolBarPanel.mainlineTrack.isSelected(), this);
4904
4905        TrackSegmentView tsv = new TrackSegmentView(
4906                newTrack,
4907                this
4908        );
4909        addLayoutTrack(newTrack, tsv);
4910
4911        setDirty();
4912
4913        // link to connected objects
4914        setLink(beginTrack, beginHitPointType, newTrack, HitPointType.TRACK);
4915        setLink(foundTrack, foundHitPointType, newTrack, HitPointType.TRACK);
4916
4917        // check on layout block
4918        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
4919        if (newName == null) {
4920            newName = "";
4921        }
4922        LayoutBlock b = provideLayoutBlock(newName);
4923
4924        if (b != null) {
4925            newTrack.setLayoutBlock(b);
4926            getLEAuxTools().setBlockConnectivityChanged();
4927
4928            // check on occupancy sensor
4929            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
4930            if (sensorName == null) {
4931                sensorName = "";
4932            }
4933
4934            if (!sensorName.isEmpty()) {
4935                if (!validateSensor(sensorName, b, this)) {
4936                    b.setOccupancySensorName("");
4937                } else {
4938                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
4939                }
4940            }
4941            newTrack.updateBlockInfo();
4942        }
4943    }
4944
4945    /**
4946     * Add a Level Crossing
4947     */
4948    public void addLevelXing() {
4949        // get unique name
4950        String name = finder.uniqueName("X", ++numLevelXings);
4951
4952        // create object
4953        LevelXing o = new LevelXing(name, this);
4954        LevelXingView ov = new LevelXingView(o, currentPoint, this);
4955        addLayoutTrack(o, ov);
4956
4957        setDirty();
4958
4959        // check on layout block
4960        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
4961        if (newName == null) {
4962            newName = "";
4963        }
4964        LayoutBlock b = provideLayoutBlock(newName);
4965
4966        if (b != null) {
4967            o.setLayoutBlockAC(b);
4968            o.setLayoutBlockBD(b);
4969
4970            // check on occupancy sensor
4971            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
4972            if (sensorName == null) {
4973                sensorName = "";
4974            }
4975
4976            if (!sensorName.isEmpty()) {
4977                if (!validateSensor(sensorName, b, this)) {
4978                    b.setOccupancySensorName("");
4979                } else {
4980                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
4981                }
4982            }
4983        }
4984    }
4985
4986    /**
4987     * Add a LayoutSlip
4988     *
4989     * @param type the slip type
4990     */
4991    public void addLayoutSlip(LayoutTurnout.TurnoutType type) {
4992        // get the rotation entry
4993        double rot = 0.0;
4994        String s = leToolBarPanel.rotationComboBox.getEditor().getItem().toString().trim();
4995
4996        if (s.isEmpty()) {
4997            rot = 0.0;
4998        } else {
4999            try {
5000                rot = Double.parseDouble(s);
5001            } catch (NumberFormatException e) {
5002                JOptionPane.showMessageDialog(this, Bundle.getMessage("Error3") + " "
5003                        + e, Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
5004
5005                return;
5006            }
5007        }
5008
5009        // get unique name
5010        String name = finder.uniqueName("SL", ++numLayoutSlips);
5011
5012        // create object
5013        LayoutSlip o;
5014        LayoutSlipView ov;
5015
5016        switch (type) {
5017            case DOUBLE_SLIP:
5018                LayoutDoubleSlip lds = new LayoutDoubleSlip(name, this);
5019                o = lds;
5020                ov = new LayoutDoubleSlipView(lds, currentPoint, rot, this);
5021                break;
5022            case SINGLE_SLIP:
5023                LayoutSingleSlip lss = new LayoutSingleSlip(name, this);
5024                o = lss;
5025                ov = new LayoutSingleSlipView(lss, currentPoint, rot, this);
5026                break;
5027            default:
5028                log.error("can't create slip {} with type {}", name, type);
5029                return; // without creating
5030        }
5031
5032        addLayoutTrack(o, ov);
5033
5034        setDirty();
5035
5036        // check on layout block
5037        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
5038        if (newName == null) {
5039            newName = "";
5040        }
5041        LayoutBlock b = provideLayoutBlock(newName);
5042
5043        if (b != null) {
5044            ov.setLayoutBlock(b);
5045
5046            // check on occupancy sensor
5047            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
5048            if (sensorName == null) {
5049                sensorName = "";
5050            }
5051
5052            if (!sensorName.isEmpty()) {
5053                if (!validateSensor(sensorName, b, this)) {
5054                    b.setOccupancySensorName("");
5055                } else {
5056                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
5057                }
5058            }
5059        }
5060
5061        String turnoutName = leToolBarPanel.turnoutNameComboBox.getSelectedItemDisplayName();
5062        if (turnoutName == null) {
5063            turnoutName = "";
5064        }
5065
5066        if (validatePhysicalTurnout(turnoutName, this)) {
5067            // turnout is valid and unique.
5068            o.setTurnout(turnoutName);
5069
5070            if (o.getTurnout().getSystemName().equals(turnoutName)) {
5071                leToolBarPanel.turnoutNameComboBox.setSelectedItem(o.getTurnout());
5072            }
5073        } else {
5074            o.setTurnout("");
5075            leToolBarPanel.turnoutNameComboBox.setSelectedItem(null);
5076            leToolBarPanel.turnoutNameComboBox.setSelectedIndex(-1);
5077        }
5078        turnoutName = leToolBarPanel.extraTurnoutNameComboBox.getSelectedItemDisplayName();
5079        if (turnoutName == null) {
5080            turnoutName = "";
5081        }
5082
5083        if (validatePhysicalTurnout(turnoutName, this)) {
5084            // turnout is valid and unique.
5085            o.setTurnoutB(turnoutName);
5086
5087            if (o.getTurnoutB().getSystemName().equals(turnoutName)) {
5088                leToolBarPanel.extraTurnoutNameComboBox.setSelectedItem(o.getTurnoutB());
5089            }
5090        } else {
5091            o.setTurnoutB("");
5092            leToolBarPanel.extraTurnoutNameComboBox.setSelectedItem(null);
5093            leToolBarPanel.extraTurnoutNameComboBox.setSelectedIndex(-1);
5094        }
5095    }
5096
5097    /**
5098     * Add a Layout Turnout
5099     *
5100     * @param type the turnout type
5101     */
5102    public void addLayoutTurnout(LayoutTurnout.TurnoutType type) {
5103        // get the rotation entry
5104        double rot = 0.0;
5105        String s = leToolBarPanel.rotationComboBox.getEditor().getItem().toString().trim();
5106
5107        if (s.isEmpty()) {
5108            rot = 0.0;
5109        } else {
5110            try {
5111                rot = Double.parseDouble(s);
5112            } catch (NumberFormatException e) {
5113                JOptionPane.showMessageDialog(this, Bundle.getMessage("Error3") + " "
5114                        + e, Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
5115
5116                return;
5117            }
5118        }
5119
5120        // get unique name
5121        String name = finder.uniqueName("TO", ++numLayoutTurnouts);
5122
5123        // create object - check all types, although not clear all actually reach here
5124        LayoutTurnout o;
5125        LayoutTurnoutView ov;
5126
5127        switch (type) {
5128
5129            case RH_TURNOUT:
5130                LayoutRHTurnout lrht = new LayoutRHTurnout(name, this);
5131                o = lrht;
5132                ov = new LayoutRHTurnoutView(lrht, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5133                break;
5134            case LH_TURNOUT:
5135                LayoutLHTurnout llht = new LayoutLHTurnout(name, this);
5136                o = llht;
5137                ov = new LayoutLHTurnoutView(llht, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5138                break;
5139            case WYE_TURNOUT:
5140                LayoutWye lw = new LayoutWye(name, this);
5141                o = lw;
5142                ov = new LayoutWyeView(lw, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5143                break;
5144            case DOUBLE_XOVER:
5145                LayoutDoubleXOver ldx = new LayoutDoubleXOver(name, this);
5146                o = ldx;
5147                ov = new LayoutDoubleXOverView(ldx, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5148                break;
5149            case RH_XOVER:
5150                LayoutRHXOver lrx = new LayoutRHXOver(name, this);
5151                o = lrx;
5152                ov = new LayoutRHXOverView(lrx, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5153                break;
5154            case LH_XOVER:
5155                LayoutLHXOver llx = new LayoutLHXOver(name, this);
5156                o = llx;
5157                ov = new LayoutLHXOverView(llx, currentPoint, rot, gContext.getXScale(), gContext.getYScale(), this);
5158                break;
5159
5160            case DOUBLE_SLIP:
5161                LayoutDoubleSlip lds = new LayoutDoubleSlip(name, this);
5162                o = lds;
5163                ov = new LayoutDoubleSlipView(lds, currentPoint, rot, this);
5164                log.error("Found SINGLE_SLIP in addLayoutTurnout for element {}", name);
5165                break;
5166            case SINGLE_SLIP:
5167                LayoutSingleSlip lss = new LayoutSingleSlip(name, this);
5168                o = lss;
5169                ov = new LayoutSingleSlipView(lss, currentPoint, rot, this);
5170                log.error("Found SINGLE_SLIP in addLayoutTurnout for element {}", name);
5171                break;
5172
5173            default:
5174                log.error("can't create LayoutTrack {} with type {}", name, type);
5175                return; // without creating
5176        }
5177
5178        addLayoutTrack(o, ov);
5179
5180        setDirty();
5181
5182        // check on layout block
5183        String newName = leToolBarPanel.blockIDComboBox.getSelectedItemDisplayName();
5184        if (newName == null) {
5185            newName = "";
5186        }
5187        LayoutBlock b = provideLayoutBlock(newName);
5188
5189        if (b != null) {
5190            ov.setLayoutBlock(b);
5191
5192            // check on occupancy sensor
5193            String sensorName = leToolBarPanel.blockSensorComboBox.getSelectedItemDisplayName();
5194            if (sensorName == null) {
5195                sensorName = "";
5196            }
5197
5198            if (!sensorName.isEmpty()) {
5199                if (!validateSensor(sensorName, b, this)) {
5200                    b.setOccupancySensorName("");
5201                } else {
5202                    leToolBarPanel.blockSensorComboBox.setSelectedItem(b.getOccupancySensor());
5203                }
5204            }
5205        }
5206
5207        // set default continuing route Turnout State
5208        o.setContinuingSense(Turnout.CLOSED);
5209
5210        // check on a physical turnout
5211        String turnoutName = leToolBarPanel.turnoutNameComboBox.getSelectedItemDisplayName();
5212        if (turnoutName == null) {
5213            turnoutName = "";
5214        }
5215
5216        if (validatePhysicalTurnout(turnoutName, this)) {
5217            // turnout is valid and unique.
5218            o.setTurnout(turnoutName);
5219
5220            if (o.getTurnout().getSystemName().equals(turnoutName)) {
5221                leToolBarPanel.turnoutNameComboBox.setSelectedItem(o.getTurnout());
5222            }
5223        } else {
5224            o.setTurnout("");
5225            leToolBarPanel.turnoutNameComboBox.setSelectedItem(null);
5226            leToolBarPanel.turnoutNameComboBox.setSelectedIndex(-1);
5227        }
5228    }
5229
5230    /**
5231     * Validates that a physical turnout exists and is unique among Layout
5232     * Turnouts Returns true if valid turnout was entered, false otherwise
5233     *
5234     * @param inTurnoutName the (system or user) name of the turnout
5235     * @param inOpenPane    the pane over which to show dialogs (null to
5236     *                      suppress dialogs)
5237     * @return true if valid
5238     */
5239    public boolean validatePhysicalTurnout(
5240            @Nonnull String inTurnoutName,
5241            @CheckForNull Component inOpenPane) {
5242        // check if turnout name was entered
5243        if (inTurnoutName.isEmpty()) {
5244            // no turnout entered
5245            return false;
5246        }
5247
5248        // check that the unique turnout name corresponds to a defined physical turnout
5249        Turnout t = InstanceManager.turnoutManagerInstance().getTurnout(inTurnoutName);
5250        if (t == null) {
5251            // There is no turnout corresponding to this name
5252            if (inOpenPane != null) {
5253                JOptionPane.showMessageDialog(inOpenPane,
5254                        MessageFormat.format(Bundle.getMessage("Error8"), inTurnoutName),
5255                        Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
5256            }
5257            return false;
5258        }
5259
5260        log.debug("validatePhysicalTurnout('{}')", inTurnoutName);
5261        boolean result = true;  // assume success (optimist!)
5262
5263        // ensure that this turnout is unique among Layout Turnouts in this Layout
5264        for (LayoutTurnout lt : getLayoutTurnouts()) {
5265            t = lt.getTurnout();
5266            if (t != null) {
5267                String sname = t.getSystemName();
5268                String uname = t.getUserName();
5269                log.debug("{}: Turnout tested '{}' and '{}'.", lt.getName(), sname, uname);
5270                if ((sname.equals(inTurnoutName))
5271                        || ((uname != null) && (uname.equals(inTurnoutName)))) {
5272                    result = false;
5273                    break;
5274                }
5275            }
5276
5277            // Only check for the second turnout if the type is a double cross over
5278            // otherwise the second turnout is used to throw an additional turnout at
5279            // the same time.
5280            if (lt.isTurnoutTypeXover()) {
5281                t = lt.getSecondTurnout();
5282                if (t != null) {
5283                    String sname = t.getSystemName();
5284                    String uname = t.getUserName();
5285                    log.debug("{}: 2nd Turnout tested '{}' and '{}'.", lt.getName(), sname, uname);
5286                    if ((sname.equals(inTurnoutName))
5287                            || ((uname != null) && (uname.equals(inTurnoutName)))) {
5288                        result = false;
5289                        break;
5290                    }
5291                }
5292            }
5293        }
5294
5295        if (result) {   // only need to test slips if we haven't failed yet...
5296            // ensure that this turnout is unique among Layout slips in this Layout
5297            for (LayoutSlip sl : getLayoutSlips()) {
5298                t = sl.getTurnout();
5299                if (t != null) {
5300                    String sname = t.getSystemName();
5301                    String uname = t.getUserName();
5302                    log.debug("{}: slip Turnout tested '{}' and '{}'.", sl.getName(), sname, uname);
5303                    if ((sname.equals(inTurnoutName))
5304                            || ((uname != null) && (uname.equals(inTurnoutName)))) {
5305                        result = false;
5306                        break;
5307                    }
5308                }
5309
5310                t = sl.getTurnoutB();
5311                if (t != null) {
5312                    String sname = t.getSystemName();
5313                    String uname = t.getUserName();
5314                    log.debug("{}: slip Turnout B tested '{}' and '{}'.", sl.getName(), sname, uname);
5315                    if ((sname.equals(inTurnoutName))
5316                            || ((uname != null) && (uname.equals(inTurnoutName)))) {
5317                        result = false;
5318                        break;
5319                    }
5320                }
5321            }
5322        }
5323
5324        if (result) {   // only need to test Turntable turnouts if we haven't failed yet...
5325            // ensure that this turntable turnout is unique among turnouts in this Layout
5326            for (LayoutTurntable tt : getLayoutTurntables()) {
5327                for (LayoutTurntable.RayTrack ray : tt.getRayTrackList()) {
5328                    t = ray.getTurnout();
5329                    if (t != null) {
5330                        String sname = t.getSystemName();
5331                        String uname = t.getUserName();
5332                        log.debug("{}: Turntable turnout tested '{}' and '{}'.", ray.getTurnoutName(), sname, uname);
5333                        if ((sname.equals(inTurnoutName))
5334                                || ((uname != null) && (uname.equals(inTurnoutName)))) {
5335                            result = false;
5336                            break;
5337                        }
5338                    }
5339                }
5340            }
5341        }
5342
5343        if (!result && (inOpenPane != null)) {
5344            JOptionPane.showMessageDialog(inOpenPane,
5345                    MessageFormat.format(Bundle.getMessage("Error4"), inTurnoutName),
5346                    Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
5347        }
5348        return result;
5349    }
5350
5351    /**
5352     * link the 'from' object and type to the 'to' object and type
5353     *
5354     * @param fromObject    the object to link from
5355     * @param fromPointType the object type to link from
5356     * @param toObject      the object to link to
5357     * @param toPointType   the object type to link to
5358     */
5359    public void setLink(@Nonnull LayoutTrack fromObject, HitPointType fromPointType,
5360            @Nonnull LayoutTrack toObject, HitPointType toPointType) {
5361        switch (fromPointType) {
5362            case POS_POINT: {
5363                if ((toPointType == HitPointType.TRACK) && (fromObject instanceof PositionablePoint)) {
5364                    ((PositionablePoint) fromObject).setTrackConnection((TrackSegment) toObject);
5365                } else {
5366                    log.error("Attempt to link a non-TRACK connection ('{}')to a Positionable Point ('{}')",
5367                            toObject.getName(), fromObject.getName());
5368                }
5369                break;
5370            }
5371
5372            case TURNOUT_A:
5373            case TURNOUT_B:
5374            case TURNOUT_C:
5375            case TURNOUT_D:
5376            case SLIP_A:
5377            case SLIP_B:
5378            case SLIP_C:
5379            case SLIP_D:
5380            case LEVEL_XING_A:
5381            case LEVEL_XING_B:
5382            case LEVEL_XING_C:
5383            case LEVEL_XING_D: {
5384                try {
5385                    fromObject.setConnection(fromPointType, toObject, toPointType);
5386                } catch (jmri.JmriException e) {
5387                    // ignore (log.error in setConnection method)
5388                }
5389                break;
5390            }
5391
5392            case TRACK: {
5393                // should never happen, Track Segment links are set in ctor
5394                log.error("Illegal request to set a Track Segment link");
5395                break;
5396            }
5397
5398            default: {
5399                if (HitPointType.isTurntableRayHitType(fromPointType) && (fromObject instanceof LayoutTurntable)) {
5400                    if (toObject instanceof TrackSegment) {
5401                        ((LayoutTurntable) fromObject).setRayConnect((TrackSegment) toObject,
5402                                fromPointType.turntableTrackIndex());
5403                    } else {
5404                        log.warn("setLink found expected toObject type {} with fromPointType {} fromObject type {}",
5405                                toObject.getClass(), fromPointType, fromObject.getClass(), new Exception("traceback"));
5406                    }
5407                } else {
5408                    log.warn("setLink found expected fromObject type {} with fromPointType {} toObject type {}",
5409                            fromObject.getClass(), fromPointType, toObject.getClass(), new Exception("traceback"));
5410                }
5411                break;
5412            }
5413        }
5414    }
5415
5416    /**
5417     * Return a layout block with the entered name, creating a new one if
5418     * needed. Note that the entered name becomes the user name of the
5419     * LayoutBlock, and a system name is automatically created by
5420     * LayoutBlockManager if needed.
5421     * <p>
5422     * If the block name is a system name, then the user will have to supply a
5423     * user name for the block.
5424     * <p>
5425     * Some, but not all, errors pop a Swing error dialog in addition to
5426     * logging.
5427     *
5428     * @param inBlockName the entered name
5429     * @return the provided LayoutBlock
5430     */
5431    public LayoutBlock provideLayoutBlock(@Nonnull String inBlockName) {
5432        LayoutBlock result = null; // assume failure (pessimist!)
5433        LayoutBlock newBlk = null; // assume failure (pessimist!)
5434
5435        if (inBlockName.isEmpty()) {
5436            // nothing entered, try autoAssign
5437            if (autoAssignBlocks) {
5438                newBlk = InstanceManager.getDefault(LayoutBlockManager.class).createNewLayoutBlock();
5439                if (null == newBlk) {
5440                    log.error("provideLayoutBlock: Failure to auto-assign for empty LayoutBlock name");
5441                }
5442            } else {
5443                log.error("provideLayoutBlock: no name given and not assigning auto block names");
5444            }
5445        } else {
5446            // check if this Layout Block already exists
5447            result = InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(inBlockName);
5448            if (result == null) { //(no)
5449                // The combo box name can be either a block system name or a block user name
5450                Block checkBlock = InstanceManager.getDefault(BlockManager.class).getBlock(inBlockName);
5451                if (checkBlock == null) {
5452                    log.error("provideLayoutBlock: The block name '{}' does not return a block.", inBlockName);
5453                } else {
5454                    String checkUserName = checkBlock.getUserName();
5455                    if (checkUserName != null && checkUserName.equals(inBlockName)) {
5456                        // Go ahead and use the name for the layout block
5457                        newBlk = InstanceManager.getDefault(LayoutBlockManager.class).createNewLayoutBlock(null, inBlockName);
5458                        if (newBlk == null) {
5459                            log.error("provideLayoutBlock: Failure to create new LayoutBlock '{}'.", inBlockName);
5460                        }
5461                    } else {
5462                        // Appears to be a system name, request a user name
5463                        String blkUserName = JOptionPane.showInputDialog(getTargetFrame(),
5464                                Bundle.getMessage("BlkUserNameMsg"),
5465                                Bundle.getMessage("BlkUserNameTitle"),
5466                                JOptionPane.PLAIN_MESSAGE);
5467                        if (blkUserName != null && !blkUserName.isEmpty()) {
5468                            // Verify the user name
5469                            Block checkDuplicate = InstanceManager.getDefault(BlockManager.class).getByUserName(blkUserName);
5470                            if (checkDuplicate != null) {
5471                                JOptionPane.showMessageDialog(getTargetFrame(),
5472                                        Bundle.getMessage("BlkUserNameInUse", blkUserName),
5473                                        Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
5474                            } else {
5475                                // OK to use as a block user name
5476                                checkBlock.setUserName(blkUserName);
5477                                newBlk = InstanceManager.getDefault(LayoutBlockManager.class).createNewLayoutBlock(null, blkUserName);
5478                                if (newBlk == null) {
5479                                    log.error("provideLayoutBlock: Failure to create new LayoutBlock '{}' with a new user name.", blkUserName);
5480                                }
5481                            }
5482                        }
5483                    }
5484                }
5485            }
5486        }
5487
5488        // if we created a new block
5489        if (newBlk != null) {
5490            // initialize the new block
5491            // log.debug("provideLayoutBlock :: Init new block {}", inBlockName);
5492            newBlk.initializeLayoutBlock();
5493            newBlk.initializeLayoutBlockRouting();
5494            newBlk.setBlockTrackColor(defaultTrackColor);
5495            newBlk.setBlockOccupiedColor(defaultOccupiedTrackColor);
5496            newBlk.setBlockExtraColor(defaultAlternativeTrackColor);
5497            result = newBlk;
5498        }
5499
5500        if (result != null) {
5501            // set both new and previously existing block
5502            result.addLayoutEditor(this);
5503            result.incrementUse();
5504            setDirty();
5505        }
5506        return result;
5507    }
5508
5509    /**
5510     * Validates that the supplied occupancy sensor name corresponds to an
5511     * existing sensor and is unique among all blocks. If valid, returns true
5512     * and sets the block sensor name in the block. Else returns false, and does
5513     * nothing to the block.
5514     *
5515     * @param sensorName the sensor name to validate
5516     * @param blk        the LayoutBlock in which to set it
5517     * @param openFrame  the frame (Component) it is in
5518     * @return true if sensor is valid
5519     */
5520    public boolean validateSensor(
5521            @Nonnull String sensorName,
5522            @Nonnull LayoutBlock blk,
5523            @Nonnull Component openFrame) {
5524        boolean result = false; // assume failure (pessimist!)
5525
5526        // check if anything entered
5527        if (!sensorName.isEmpty()) {
5528            // get a validated sensor corresponding to this name and assigned to block
5529            if (blk.getOccupancySensorName().equals(sensorName)) {
5530                result = true;
5531            } else {
5532                Sensor s = blk.validateSensor(sensorName, openFrame);
5533                result = (s != null); // if sensor returned result is true.
5534            }
5535        }
5536        return result;
5537    }
5538
5539    /**
5540     * Return a layout block with the given name if one exists. Registers this
5541     * LayoutEditor with the layout block. This method is designed to be used
5542     * when a panel is loaded. The calling method must handle whether the use
5543     * count should be incremented.
5544     *
5545     * @param blockID the given name
5546     * @return null if blockID does not already exist
5547     */
5548    public LayoutBlock getLayoutBlock(@Nonnull String blockID) {
5549        // check if this Layout Block already exists
5550        LayoutBlock blk = InstanceManager.getDefault(LayoutBlockManager.class).getByUserName(blockID);
5551        if (blk == null) {
5552            log.error("LayoutBlock '{}' not found when panel loaded", blockID);
5553            return null;
5554        }
5555        blk.addLayoutEditor(this);
5556        return blk;
5557    }
5558
5559    /**
5560     * Remove object from all Layout Editor temporary lists of items not part of
5561     * track schematic
5562     *
5563     * @param s the object to remove
5564     * @return true if found
5565     */
5566    private boolean remove(@Nonnull Object s) {
5567        boolean found = false;
5568
5569        if (backgroundImage.contains(s)) {
5570            backgroundImage.remove(s);
5571            found = true;
5572        }
5573        if (memoryLabelList.contains(s)) {
5574            memoryLabelList.remove(s);
5575            found = true;
5576        }
5577        if (blockContentsLabelList.contains(s)) {
5578            blockContentsLabelList.remove(s);
5579            found = true;
5580        }
5581        if (multiSensors.contains(s)) {
5582            multiSensors.remove(s);
5583            found = true;
5584        }
5585        if (clocks.contains(s)) {
5586            clocks.remove(s);
5587            found = true;
5588        }
5589        if (labelImage.contains(s)) {
5590            labelImage.remove(s);
5591            found = true;
5592        }
5593
5594        if (sensorImage.contains(s) || sensorList.contains(s)) {
5595            Sensor sensor = ((SensorIcon) s).getSensor();
5596            if (sensor != null) {
5597                if (removeAttachedBean((sensor))) {
5598                    sensorImage.remove(s);
5599                    sensorList.remove(s);
5600                    found = true;
5601                } else {
5602                    return false;
5603                }
5604            }
5605        }
5606
5607        if (signalHeadImage.contains(s) || signalList.contains(s)) {
5608            SignalHead head = ((SignalHeadIcon) s).getSignalHead();
5609            if (head != null) {
5610                if (removeAttachedBean((head))) {
5611                    signalHeadImage.remove(s);
5612                    signalList.remove(s);
5613                    found = true;
5614                } else {
5615                    return false;
5616                }
5617            }
5618        }
5619
5620        if (signalMastList.contains(s)) {
5621            SignalMast mast = ((SignalMastIcon) s).getSignalMast();
5622            if (mast != null) {
5623                if (removeAttachedBean((mast))) {
5624                    signalMastList.remove(s);
5625                    found = true;
5626                } else {
5627                    return false;
5628                }
5629            }
5630        }
5631
5632        super.removeFromContents((Positionable) s);
5633
5634        if (found) {
5635            setDirty();
5636            redrawPanel();
5637        }
5638        return found;
5639    }
5640
5641    @Override
5642    public boolean removeFromContents(@Nonnull Positionable l) {
5643        return remove(l);
5644    }
5645
5646    private String findBeanUsage(@Nonnull NamedBean bean) {
5647        PositionablePoint pe;
5648        PositionablePoint pw;
5649        LayoutTurnout lt;
5650        LevelXing lx;
5651        LayoutSlip ls;
5652        boolean found = false;
5653        StringBuilder sb = new StringBuilder();
5654        String msgKey = "DeleteReference";  // NOI18N
5655        String beanKey = "None";  // NOI18N
5656        String beanValue = bean.getDisplayName();
5657
5658        if (bean instanceof SignalMast) {
5659            beanKey = "BeanNameSignalMast";  // NOI18N
5660
5661            if (InstanceManager.getDefault(SignalMastLogicManager.class).isSignalMastUsed((SignalMast) bean)) {
5662                SignalMastLogic sml = InstanceManager.getDefault(
5663                        SignalMastLogicManager.class).getSignalMastLogic((SignalMast) bean);
5664                if ((sml != null) && sml.useLayoutEditor(sml.getDestinationList().get(0))) {
5665                    msgKey = "DeleteSmlReference";  // NOI18N
5666                }
5667            }
5668        } else if (bean instanceof Sensor) {
5669            beanKey = "BeanNameSensor";  // NOI18N
5670        } else if (bean instanceof SignalHead) {
5671            beanKey = "BeanNameSignalHead";  // NOI18N
5672        }
5673        if (!beanKey.equals("None")) {  // NOI18N
5674            sb.append(Bundle.getMessage(msgKey, Bundle.getMessage(beanKey), beanValue));
5675        }
5676
5677        if ((pw = finder.findPositionablePointByWestBoundBean(bean)) != null) {
5678            TrackSegment t1 = pw.getConnect1();
5679            TrackSegment t2 = pw.getConnect2();
5680            if (t1 != null) {
5681                if (t2 != null) {
5682                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
5683                    sb.append(Bundle.getMessage("DeleteAtPoint2", t2.getBlockName()));  // NOI18N
5684                } else {
5685                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
5686                }
5687            }
5688            found = true;
5689        }
5690
5691        if ((pe = finder.findPositionablePointByEastBoundBean(bean)) != null) {
5692            TrackSegment t1 = pe.getConnect1();
5693            TrackSegment t2 = pe.getConnect2();
5694
5695            if (t1 != null) {
5696                if (t2 != null) {
5697                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
5698                    sb.append(Bundle.getMessage("DeleteAtPoint2", t2.getBlockName()));  // NOI18N
5699                } else {
5700                    sb.append(Bundle.getMessage("DeleteAtPoint1", t1.getBlockName()));  // NOI18N
5701                }
5702            }
5703            found = true;
5704        }
5705
5706        if ((lt = finder.findLayoutTurnoutByBean(bean)) != null) {
5707            sb.append(Bundle.getMessage("DeleteAtOther", Bundle.getMessage("BeanNameTurnout"), lt.getTurnoutName()));   // NOI18N
5708            found = true;
5709        }
5710
5711        if ((lx = finder.findLevelXingByBean(bean)) != null) {
5712            sb.append(Bundle.getMessage("DeleteAtOther", Bundle.getMessage("LevelCrossing"), lx.getId()));   // NOI18N
5713            found = true;
5714        }
5715
5716        if ((ls = finder.findLayoutSlipByBean(bean)) != null) {
5717            sb.append(Bundle.getMessage("DeleteAtOther", Bundle.getMessage("Slip"), ls.getTurnoutName()));   // NOI18N
5718            found = true;
5719        }
5720
5721        if (!found) {
5722            return null;
5723        }
5724        return sb.toString();
5725    }
5726
5727    /**
5728     * NX Sensors, Signal Heads and Signal Masts can be attached to positional
5729     * points, turnouts and level crossings. If an attachment exists, present an
5730     * option to cancel the remove action, remove the attachement or retain the
5731     * attachment.
5732     *
5733     * @param bean The named bean to be removed.
5734     * @return true if OK to remove the related icon.
5735     */
5736    private boolean removeAttachedBean(@Nonnull NamedBean bean) {
5737        String usage = findBeanUsage(bean);
5738
5739        if (usage != null) {
5740            usage = String.format("<html>%s</html>", usage);
5741            int selectedValue = JOptionPane.showOptionDialog(this,
5742                    usage, Bundle.getMessage("WarningTitle"),
5743                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
5744                    new Object[]{Bundle.getMessage("ButtonYes"),
5745                        Bundle.getMessage("ButtonNo"),
5746                        Bundle.getMessage("ButtonCancel")},
5747                    Bundle.getMessage("ButtonYes"));
5748
5749            if (selectedValue == JOptionPane.NO_OPTION) {
5750                return true; // return leaving the references in place but allow the icon to be deleted.
5751            }
5752
5753            if (selectedValue == JOptionPane.CANCEL_OPTION) {
5754                return false; // do not delete the item
5755            }
5756            if (bean instanceof Sensor) {
5757                // Additional actions for NX sensor pairs
5758                return getLETools().removeSensorAssignment((Sensor) bean);
5759            } else {
5760                removeBeanRefs(bean);
5761            }
5762        }
5763        return true;
5764    }
5765
5766    private void removeBeanRefs(@Nonnull NamedBean bean) {
5767        PositionablePoint pe;
5768        PositionablePoint pw;
5769        LayoutTurnout lt;
5770        LevelXing lx;
5771        LayoutSlip ls;
5772
5773        if ((pw = finder.findPositionablePointByWestBoundBean(bean)) != null) {
5774            pw.removeBeanReference(bean);
5775        }
5776
5777        if ((pe = finder.findPositionablePointByEastBoundBean(bean)) != null) {
5778            pe.removeBeanReference(bean);
5779        }
5780
5781        if ((lt = finder.findLayoutTurnoutByBean(bean)) != null) {
5782            lt.removeBeanReference(bean);
5783        }
5784
5785        if ((lx = finder.findLevelXingByBean(bean)) != null) {
5786            lx.removeBeanReference(bean);
5787        }
5788
5789        if ((ls = finder.findLayoutSlipByBean(bean)) != null) {
5790            ls.removeBeanReference(bean);
5791        }
5792    }
5793
5794    private boolean noWarnPositionablePoint = false;
5795
5796    /**
5797     * Remove a PositionablePoint -- an Anchor or an End Bumper.
5798     *
5799     * @param o the PositionablePoint to remove
5800     * @return true if removed
5801     */
5802    public boolean removePositionablePoint(@Nonnull PositionablePoint o) {
5803        // First verify with the user that this is really wanted, only show message if there is a bit of track connected
5804        if ((o.getConnect1() != null) || (o.getConnect2() != null)) {
5805            if (!noWarnPositionablePoint) {
5806                int selectedValue = JOptionPane.showOptionDialog(this,
5807                        Bundle.getMessage("Question2"), Bundle.getMessage("WarningTitle"),
5808                        JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
5809                        new Object[]{Bundle.getMessage("ButtonYes"),
5810                            Bundle.getMessage("ButtonNo"),
5811                            Bundle.getMessage("ButtonYesPlus")},
5812                        Bundle.getMessage("ButtonNo"));
5813
5814                if (selectedValue == JOptionPane.NO_OPTION) {
5815                    return false; // return without creating if "No" response
5816                }
5817
5818                if (selectedValue == JOptionPane.CANCEL_OPTION) {
5819                    // Suppress future warnings, and continue
5820                    noWarnPositionablePoint = true;
5821                }
5822            }
5823
5824            // remove from selection information
5825            if (selectedObject == o) {
5826                selectedObject = null;
5827            }
5828
5829            if (prevSelectedObject == o) {
5830                prevSelectedObject = null;
5831            }
5832
5833            // remove connections if any
5834            TrackSegment t1 = o.getConnect1();
5835            TrackSegment t2 = o.getConnect2();
5836
5837            if (t1 != null) {
5838                removeTrackSegment(t1);
5839            }
5840
5841            if (t2 != null) {
5842                removeTrackSegment(t2);
5843            }
5844
5845            // delete from array
5846        }
5847
5848        return removeLayoutTrackAndRedraw(o);
5849    }
5850
5851    private boolean noWarnLayoutTurnout = false;
5852
5853    /**
5854     * Remove a LayoutTurnout
5855     *
5856     * @param o the LayoutTurnout to remove
5857     * @return true if removed
5858     */
5859    public boolean removeLayoutTurnout(@Nonnull LayoutTurnout o) {
5860        // First verify with the user that this is really wanted
5861        if (!noWarnLayoutTurnout) {
5862            int selectedValue = JOptionPane.showOptionDialog(this,
5863                    Bundle.getMessage("Question1r"), Bundle.getMessage("WarningTitle"),
5864                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
5865                    new Object[]{Bundle.getMessage("ButtonYes"),
5866                        Bundle.getMessage("ButtonNo"),
5867                        Bundle.getMessage("ButtonYesPlus")},
5868                    Bundle.getMessage("ButtonNo"));
5869
5870            if (selectedValue == JOptionPane.NO_OPTION) {
5871                return false; // return without removing if "No" response
5872            }
5873
5874            if (selectedValue == JOptionPane.CANCEL_OPTION) {
5875                // Suppress future warnings, and continue
5876                noWarnLayoutTurnout = true;
5877            }
5878        }
5879
5880        // remove from selection information
5881        if (selectedObject == o) {
5882            selectedObject = null;
5883        }
5884
5885        if (prevSelectedObject == o) {
5886            prevSelectedObject = null;
5887        }
5888
5889        // remove connections if any
5890        TrackSegment t = (TrackSegment) o.getConnectA();
5891
5892        if (t != null) {
5893            substituteAnchor(getLayoutTurnoutView(o).getCoordsA(), o, t);
5894        }
5895        t = (TrackSegment) o.getConnectB();
5896
5897        if (t != null) {
5898            substituteAnchor(getLayoutTurnoutView(o).getCoordsB(), o, t);
5899        }
5900        t = (TrackSegment) o.getConnectC();
5901
5902        if (t != null) {
5903            substituteAnchor(getLayoutTurnoutView(o).getCoordsC(), o, t);
5904        }
5905        t = (TrackSegment) o.getConnectD();
5906
5907        if (t != null) {
5908            substituteAnchor(getLayoutTurnoutView(o).getCoordsD(), o, t);
5909        }
5910
5911        // decrement Block use count(s)
5912        LayoutBlock b = o.getLayoutBlock();
5913
5914        if (b != null) {
5915            b.decrementUse();
5916        }
5917
5918        if (o.isTurnoutTypeXover() || o.isTurnoutTypeSlip()) {
5919            LayoutBlock b2 = o.getLayoutBlockB();
5920
5921            if ((b2 != null) && (b2 != b)) {
5922                b2.decrementUse();
5923            }
5924            LayoutBlock b3 = o.getLayoutBlockC();
5925
5926            if ((b3 != null) && (b3 != b) && (b3 != b2)) {
5927                b3.decrementUse();
5928            }
5929            LayoutBlock b4 = o.getLayoutBlockD();
5930
5931            if ((b4 != null) && (b4 != b)
5932                    && (b4 != b2) && (b4 != b3)) {
5933                b4.decrementUse();
5934            }
5935        }
5936
5937        return removeLayoutTrackAndRedraw(o);
5938    }
5939
5940    private void substituteAnchor(@Nonnull Point2D loc,
5941            @Nonnull LayoutTrack o, @Nonnull TrackSegment t) {
5942        PositionablePoint p = addAnchor(loc);
5943
5944        if (t.getConnect1() == o) {
5945            t.setNewConnect1(p, HitPointType.POS_POINT);
5946        }
5947
5948        if (t.getConnect2() == o) {
5949            t.setNewConnect2(p, HitPointType.POS_POINT);
5950        }
5951        p.setTrackConnection(t);
5952    }
5953
5954    private boolean noWarnLevelXing = false;
5955
5956    /**
5957     * Remove a Level Crossing
5958     *
5959     * @param o the LevelXing to remove
5960     * @return true if removed
5961     */
5962    public boolean removeLevelXing(@Nonnull LevelXing o) {
5963        // First verify with the user that this is really wanted
5964        if (!noWarnLevelXing) {
5965            int selectedValue = JOptionPane.showOptionDialog(this,
5966                    Bundle.getMessage("Question3r"), Bundle.getMessage("WarningTitle"),
5967                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
5968                    new Object[]{Bundle.getMessage("ButtonYes"),
5969                        Bundle.getMessage("ButtonNo"),
5970                        Bundle.getMessage("ButtonYesPlus")},
5971                    Bundle.getMessage("ButtonNo"));
5972
5973            if (selectedValue == JOptionPane.NO_OPTION) {
5974                return false; // return without creating if "No" response
5975            }
5976
5977            if (selectedValue == JOptionPane.CANCEL_OPTION) {
5978                // Suppress future warnings, and continue
5979                noWarnLevelXing = true;
5980            }
5981        }
5982
5983        // remove from selection information
5984        if (selectedObject == o) {
5985            selectedObject = null;
5986        }
5987
5988        if (prevSelectedObject == o) {
5989            prevSelectedObject = null;
5990        }
5991
5992        // remove connections if any
5993        LevelXingView ov = getLevelXingView(o);
5994
5995        TrackSegment t = (TrackSegment) o.getConnectA();
5996        if (t != null) {
5997            substituteAnchor(ov.getCoordsA(), o, t);
5998        }
5999        t = (TrackSegment) o.getConnectB();
6000
6001        if (t != null) {
6002            substituteAnchor(ov.getCoordsB(), o, t);
6003        }
6004        t = (TrackSegment) o.getConnectC();
6005
6006        if (t != null) {
6007            substituteAnchor(ov.getCoordsC(), o, t);
6008        }
6009        t = (TrackSegment) o.getConnectD();
6010
6011        if (t != null) {
6012            substituteAnchor(ov.getCoordsD(), o, t);
6013        }
6014
6015        // decrement block use count if any blocks in use
6016        LayoutBlock lb = o.getLayoutBlockAC();
6017
6018        if (lb != null) {
6019            lb.decrementUse();
6020        }
6021        LayoutBlock lbx = o.getLayoutBlockBD();
6022
6023        if ((lbx != null) && (lb != null) && (lbx != lb)) {
6024            lb.decrementUse();
6025        }
6026
6027        return removeLayoutTrackAndRedraw(o);
6028    }
6029
6030    private boolean noWarnSlip = false;
6031
6032    /**
6033     * Remove a slip
6034     *
6035     * @param o the LayoutSlip to remove
6036     * @return true if removed
6037     */
6038    public boolean removeLayoutSlip(@Nonnull LayoutTurnout o) {
6039        if (!(o instanceof LayoutSlip)) {
6040            return false;
6041        }
6042
6043        // First verify with the user that this is really wanted
6044        if (!noWarnSlip) {
6045            int selectedValue = JOptionPane.showOptionDialog(this,
6046                    Bundle.getMessage("Question5r"), Bundle.getMessage("WarningTitle"),
6047                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
6048                    new Object[]{Bundle.getMessage("ButtonYes"),
6049                        Bundle.getMessage("ButtonNo"),
6050                        Bundle.getMessage("ButtonYesPlus")},
6051                    Bundle.getMessage("ButtonNo"));
6052
6053            if (selectedValue == JOptionPane.NO_OPTION) {
6054                return false; // return without creating if "No" response
6055            }
6056
6057            if (selectedValue == JOptionPane.CANCEL_OPTION) {
6058                // Suppress future warnings, and continue
6059                noWarnSlip = true;
6060            }
6061        }
6062
6063        LayoutTurnoutView ov = getLayoutTurnoutView(o);
6064
6065        // remove from selection information
6066        if (selectedObject == o) {
6067            selectedObject = null;
6068        }
6069
6070        if (prevSelectedObject == o) {
6071            prevSelectedObject = null;
6072        }
6073
6074        // remove connections if any
6075        TrackSegment t = (TrackSegment) o.getConnectA();
6076
6077        if (t != null) {
6078            substituteAnchor(ov.getCoordsA(), o, t);
6079        }
6080        t = (TrackSegment) o.getConnectB();
6081
6082        if (t != null) {
6083            substituteAnchor(ov.getCoordsB(), o, t);
6084        }
6085        t = (TrackSegment) o.getConnectC();
6086
6087        if (t != null) {
6088            substituteAnchor(ov.getCoordsC(), o, t);
6089        }
6090        t = (TrackSegment) o.getConnectD();
6091
6092        if (t != null) {
6093            substituteAnchor(ov.getCoordsD(), o, t);
6094        }
6095
6096        // decrement block use count if any blocks in use
6097        LayoutBlock lb = o.getLayoutBlock();
6098
6099        if (lb != null) {
6100            lb.decrementUse();
6101        }
6102
6103        return removeLayoutTrackAndRedraw(o);
6104    }
6105
6106    private boolean noWarnTurntable = false;
6107
6108    /**
6109     * Remove a Layout Turntable
6110     *
6111     * @param o the LayoutTurntable to remove
6112     * @return true if removed
6113     */
6114    public boolean removeTurntable(@Nonnull LayoutTurntable o) {
6115        // First verify with the user that this is really wanted
6116        if (!noWarnTurntable) {
6117            int selectedValue = JOptionPane.showOptionDialog(this,
6118                    Bundle.getMessage("Question4r"), Bundle.getMessage("WarningTitle"),
6119                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
6120                    new Object[]{Bundle.getMessage("ButtonYes"),
6121                        Bundle.getMessage("ButtonNo"),
6122                        Bundle.getMessage("ButtonYesPlus")},
6123                    Bundle.getMessage("ButtonNo"));
6124
6125            if (selectedValue == JOptionPane.NO_OPTION) {
6126                return false; // return without creating if "No" response
6127            }
6128
6129            if (selectedValue == JOptionPane.CANCEL_OPTION) {
6130                // Suppress future warnings, and continue
6131                noWarnTurntable = true;
6132            }
6133        }
6134
6135        // remove from selection information
6136        if (selectedObject == o) {
6137            selectedObject = null;
6138        }
6139
6140        if (prevSelectedObject == o) {
6141            prevSelectedObject = null;
6142        }
6143
6144        // remove connections if any
6145        LayoutTurntableView ov = getLayoutTurntableView(o);
6146        for (int j = 0; j < o.getNumberRays(); j++) {
6147            TrackSegment t = ov.getRayConnectOrdered(j);
6148
6149            if (t != null) {
6150                substituteAnchor(ov.getRayCoordsIndexed(j), o, t);
6151            }
6152        }
6153
6154        return removeLayoutTrackAndRedraw(o);
6155    }
6156
6157    /**
6158     * Remove a Track Segment
6159     *
6160     * @param o the TrackSegment to remove
6161     */
6162    public void removeTrackSegment(@Nonnull TrackSegment o) {
6163        // save affected blocks
6164        LayoutBlock block1 = null;
6165        LayoutBlock block2 = null;
6166        LayoutBlock block = o.getLayoutBlock();
6167
6168        // remove any connections
6169        HitPointType type = o.getType1();
6170
6171        if (type == HitPointType.POS_POINT) {
6172            PositionablePoint p = (PositionablePoint) (o.getConnect1());
6173
6174            if (p != null) {
6175                p.removeTrackConnection(o);
6176
6177                if (p.getConnect1() != null) {
6178                    block1 = p.getConnect1().getLayoutBlock();
6179                } else if (p.getConnect2() != null) {
6180                    block1 = p.getConnect2().getLayoutBlock();
6181                }
6182            }
6183        } else {
6184            block1 = getAffectedBlock(o.getConnect1(), type);
6185            disconnect(o.getConnect1(), type);
6186        }
6187        type = o.getType2();
6188
6189        if (type == HitPointType.POS_POINT) {
6190            PositionablePoint p = (PositionablePoint) (o.getConnect2());
6191
6192            if (p != null) {
6193                p.removeTrackConnection(o);
6194
6195                if (p.getConnect1() != null) {
6196                    block2 = p.getConnect1().getLayoutBlock();
6197                } else if (p.getConnect2() != null) {
6198                    block2 = p.getConnect2().getLayoutBlock();
6199                }
6200            }
6201        } else {
6202            block2 = getAffectedBlock(o.getConnect2(), type);
6203            disconnect(o.getConnect2(), type);
6204        }
6205
6206        // delete from array
6207        removeLayoutTrack(o);
6208
6209        // update affected blocks
6210        if (block != null) {
6211            // decrement Block use count
6212            block.decrementUse();
6213            getLEAuxTools().setBlockConnectivityChanged();
6214            block.updatePaths();
6215        }
6216
6217        if ((block1 != null) && (block1 != block)) {
6218            block1.updatePaths();
6219        }
6220
6221        if ((block2 != null) && (block2 != block) && (block2 != block1)) {
6222            block2.updatePaths();
6223        }
6224
6225        //
6226        setDirty();
6227        redrawPanel();
6228    }
6229
6230    private void disconnect(@Nonnull LayoutTrack o, HitPointType type) {
6231        switch (type) {
6232            case TURNOUT_A:
6233            case TURNOUT_B:
6234            case TURNOUT_C:
6235            case TURNOUT_D:
6236            case SLIP_A:
6237            case SLIP_B:
6238            case SLIP_C:
6239            case SLIP_D:
6240            case LEVEL_XING_A:
6241            case LEVEL_XING_B:
6242            case LEVEL_XING_C:
6243            case LEVEL_XING_D: {
6244                try {
6245                    o.setConnection(type, null, HitPointType.NONE);
6246                } catch (jmri.JmriException e) {
6247                    // ignore (log.error in setConnection method)
6248                }
6249                break;
6250            }
6251
6252            default: {
6253                if (HitPointType.isTurntableRayHitType(type)) {
6254                    ((LayoutTurntable) o).setRayConnect(null, type.turntableTrackIndex());
6255                }
6256                break;
6257            }
6258        }
6259    }
6260
6261    /**
6262     * Depending on the given type, and the real class of the given LayoutTrack,
6263     * determine the connected LayoutTrack. This provides a variable-indirect
6264     * form of e.g. trk.getLayoutBlockC() for example. Perhaps "Connected Block"
6265     * captures the idea better, but that method name is being used for
6266     * something else.
6267     *
6268     *
6269     * @param track The track who's connected blocks are being examined
6270     * @param type  This point to check for connected blocks, i.e. TURNOUT_B
6271     * @return The block at a particular point on the track object, or null if
6272     *         none.
6273     */
6274    // Temporary - this should certainly be a LayoutTrack method.
6275    public LayoutBlock getAffectedBlock(@Nonnull LayoutTrack track, HitPointType type) {
6276        LayoutBlock result = null;
6277
6278        switch (type) {
6279            case TURNOUT_A:
6280            case SLIP_A: {
6281                if (track instanceof LayoutTurnout) {
6282                    LayoutTurnout lt = (LayoutTurnout) track;
6283                    result = lt.getLayoutBlock();
6284                }
6285                break;
6286            }
6287
6288            case TURNOUT_B:
6289            case SLIP_B: {
6290                if (track instanceof LayoutTurnout) {
6291                    LayoutTurnout lt = (LayoutTurnout) track;
6292                    result = lt.getLayoutBlockB();
6293                }
6294                break;
6295            }
6296
6297            case TURNOUT_C:
6298            case SLIP_C: {
6299                if (track instanceof LayoutTurnout) {
6300                    LayoutTurnout lt = (LayoutTurnout) track;
6301                    result = lt.getLayoutBlockC();
6302                }
6303                break;
6304            }
6305
6306            case TURNOUT_D:
6307            case SLIP_D: {
6308                if (track instanceof LayoutTurnout) {
6309                    LayoutTurnout lt = (LayoutTurnout) track;
6310                    result = lt.getLayoutBlockD();
6311                }
6312                break;
6313            }
6314
6315            case LEVEL_XING_A:
6316            case LEVEL_XING_C: {
6317                if (track instanceof LevelXing) {
6318                    LevelXing lx = (LevelXing) track;
6319                    result = lx.getLayoutBlockAC();
6320                }
6321                break;
6322            }
6323
6324            case LEVEL_XING_B:
6325            case LEVEL_XING_D: {
6326                if (track instanceof LevelXing) {
6327                    LevelXing lx = (LevelXing) track;
6328                    result = lx.getLayoutBlockBD();
6329                }
6330                break;
6331            }
6332
6333            case TRACK: {
6334                if (track instanceof TrackSegment) {
6335                    TrackSegment ts = (TrackSegment) track;
6336                    result = ts.getLayoutBlock();
6337                }
6338                break;
6339            }
6340            default: {
6341                log.warn("Unhandled track type: {}", type);
6342                break;
6343            }
6344        }
6345        return result;
6346    }
6347
6348    /**
6349     * Add a sensor indicator to the Draw Panel
6350     */
6351    void addSensor() {
6352        String newName = leToolBarPanel.sensorComboBox.getSelectedItemDisplayName();
6353        if (newName == null) {
6354            newName = "";
6355        }
6356
6357        if (newName.isEmpty()) {
6358            JOptionPane.showMessageDialog(this, Bundle.getMessage("Error10"),
6359                    Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
6360            return;
6361        }
6362        SensorIcon l = new SensorIcon(new NamedIcon("resources/icons/smallschematics/tracksegments/circuit-error.gif",
6363                "resources/icons/smallschematics/tracksegments/circuit-error.gif"), this);
6364
6365        l.setIcon("SensorStateActive", leToolBarPanel.sensorIconEditor.getIcon(0));
6366        l.setIcon("SensorStateInactive", leToolBarPanel.sensorIconEditor.getIcon(1));
6367        l.setIcon("BeanStateInconsistent", leToolBarPanel.sensorIconEditor.getIcon(2));
6368        l.setIcon("BeanStateUnknown", leToolBarPanel.sensorIconEditor.getIcon(3));
6369        l.setSensor(newName);
6370        l.setDisplayLevel(Editor.SENSORS);
6371
6372        leToolBarPanel.sensorComboBox.setSelectedItem(l.getSensor());
6373        setNextLocation(l);
6374        putItem(l); // note: this calls unionToPanelBounds & setDirty()
6375    }
6376
6377    public void putSensor(@Nonnull SensorIcon l) {
6378        l.updateSize();
6379        l.setDisplayLevel(Editor.SENSORS);
6380        putItem(l); // note: this calls unionToPanelBounds & setDirty()
6381    }
6382
6383    /**
6384     * Add a signal head to the Panel
6385     */
6386    void addSignalHead() {
6387        // check for valid signal head entry
6388        String newName = leToolBarPanel.signalHeadComboBox.getSelectedItemDisplayName();
6389        if (newName == null) {
6390            newName = "";
6391        }
6392        SignalHead mHead = null;
6393
6394        if (!newName.isEmpty()) {
6395            mHead = InstanceManager.getDefault(SignalHeadManager.class).getSignalHead(newName);
6396
6397            /*if (mHead == null)
6398            mHead = InstanceManager.getDefault(SignalHeadManager.class).getByUserName(newName);
6399            else */
6400            leToolBarPanel.signalHeadComboBox.setSelectedItem(mHead);
6401        }
6402
6403        if (mHead == null) {
6404            // There is no signal head corresponding to this name
6405            JOptionPane.showMessageDialog(this,
6406                    MessageFormat.format(Bundle.getMessage("Error9"), newName),
6407                    Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
6408            return;
6409        }
6410
6411        // create and set up signal icon
6412        SignalHeadIcon l = new SignalHeadIcon(this);
6413        l.setSignalHead(newName);
6414        l.setIcon("SignalHeadStateRed", leToolBarPanel.signalIconEditor.getIcon(0));
6415        l.setIcon("SignalHeadStateFlashingRed", leToolBarPanel.signalIconEditor.getIcon(1));
6416        l.setIcon("SignalHeadStateYellow", leToolBarPanel.signalIconEditor.getIcon(2));
6417        l.setIcon("SignalHeadStateFlashingYellow", leToolBarPanel.signalIconEditor.getIcon(3));
6418        l.setIcon("SignalHeadStateGreen", leToolBarPanel.signalIconEditor.getIcon(4));
6419        l.setIcon("SignalHeadStateFlashingGreen", leToolBarPanel.signalIconEditor.getIcon(5));
6420        l.setIcon("SignalHeadStateDark", leToolBarPanel.signalIconEditor.getIcon(6));
6421        l.setIcon("SignalHeadStateHeld", leToolBarPanel.signalIconEditor.getIcon(7));
6422        l.setIcon("SignalHeadStateLunar", leToolBarPanel.signalIconEditor.getIcon(8));
6423        l.setIcon("SignalHeadStateFlashingLunar", leToolBarPanel.signalIconEditor.getIcon(9));
6424        unionToPanelBounds(l.getBounds());
6425        setNextLocation(l);
6426        setDirty();
6427        putSignal(l);
6428    }
6429
6430    public void putSignal(@Nonnull SignalHeadIcon l) {
6431        l.updateSize();
6432        l.setDisplayLevel(Editor.SIGNALS);
6433        putItem(l); // note: this calls unionToPanelBounds & setDirty()
6434    }
6435
6436    @CheckForNull
6437    SignalHead getSignalHead(@Nonnull String name) {
6438        SignalHead sh = InstanceManager.getDefault(SignalHeadManager.class).getBySystemName(name);
6439
6440        if (sh == null) {
6441            sh = InstanceManager.getDefault(SignalHeadManager.class).getByUserName(name);
6442        }
6443
6444        if (sh == null) {
6445            log.warn("did not find a SignalHead named {}", name);
6446        }
6447        return sh;
6448    }
6449
6450    public boolean containsSignalHead(@CheckForNull SignalHead head) {
6451        if (head != null) {
6452            for (SignalHeadIcon h : signalList) {
6453                if (h.getSignalHead() == head) {
6454                    return true;
6455                }
6456            }
6457        }
6458        return false;
6459    }
6460
6461    public void removeSignalHead(@CheckForNull SignalHead head) {
6462        if (head != null) {
6463            for (SignalHeadIcon h : signalList) {
6464                if (h.getSignalHead() == head) {
6465                    signalList.remove(h);
6466                    h.remove();
6467                    h.dispose();
6468                    setDirty();
6469                    redrawPanel();
6470                    break;
6471                }
6472            }
6473        }
6474    }
6475
6476    void addSignalMast() {
6477        // check for valid signal head entry
6478        String newName = leToolBarPanel.signalMastComboBox.getSelectedItemDisplayName();
6479        if (newName == null) {
6480            newName = "";
6481        }
6482        SignalMast mMast = null;
6483
6484        if (!newName.isEmpty()) {
6485            mMast = InstanceManager.getDefault(SignalMastManager.class).getSignalMast(newName);
6486            leToolBarPanel.signalMastComboBox.setSelectedItem(mMast);
6487        }
6488
6489        if (mMast == null) {
6490            // There is no signal head corresponding to this name
6491            JOptionPane.showMessageDialog(this,
6492                    MessageFormat.format(Bundle.getMessage("Error9"), newName),
6493                    Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
6494
6495            return;
6496        }
6497
6498        // create and set up signal icon
6499        SignalMastIcon l = new SignalMastIcon(this);
6500        l.setSignalMast(newName);
6501        unionToPanelBounds(l.getBounds());
6502        setNextLocation(l);
6503        setDirty();
6504        putSignalMast(l);
6505    }
6506
6507    public void putSignalMast(@Nonnull SignalMastIcon l) {
6508        l.updateSize();
6509        l.setDisplayLevel(Editor.SIGNALS);
6510        putItem(l); // note: this calls unionToPanelBounds & setDirty()
6511    }
6512
6513    SignalMast getSignalMast(@Nonnull String name) {
6514        SignalMast sh = InstanceManager.getDefault(SignalMastManager.class).getBySystemName(name);
6515
6516        if (sh == null) {
6517            sh = InstanceManager.getDefault(SignalMastManager.class).getByUserName(name);
6518        }
6519
6520        if (sh == null) {
6521            log.warn("did not find a SignalMast named {}", name);
6522        }
6523        return sh;
6524    }
6525
6526    public boolean containsSignalMast(@Nonnull SignalMast mast) {
6527        for (SignalMastIcon h : signalMastList) {
6528            if (h.getSignalMast() == mast) {
6529                return true;
6530            }
6531        }
6532        return false;
6533    }
6534
6535    /**
6536     * Add a label to the Draw Panel
6537     */
6538    void addLabel() {
6539        String labelText = leToolBarPanel.textLabelTextField.getText();
6540        labelText = (labelText != null) ? labelText.trim() : "";
6541
6542        if (labelText.isEmpty()) {
6543            JOptionPane.showMessageDialog(this, Bundle.getMessage("Error11"),
6544                    Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
6545            return;
6546        }
6547        PositionableLabel l = super.addLabel(labelText);
6548        unionToPanelBounds(l.getBounds());
6549        setDirty();
6550        l.setForeground(defaultTextColor);
6551    }
6552
6553    @Override
6554    public void putItem(@Nonnull Positionable l) {
6555        super.putItem(l);
6556
6557        if (l instanceof SensorIcon) {
6558            sensorImage.add((SensorIcon) l);
6559            sensorList.add((SensorIcon) l);
6560        } else if (l instanceof LocoIcon) {
6561            markerImage.add((LocoIcon) l);
6562        } else if (l instanceof SignalHeadIcon) {
6563            signalHeadImage.add((SignalHeadIcon) l);
6564            signalList.add((SignalHeadIcon) l);
6565        } else if (l instanceof SignalMastIcon) {
6566            signalMastList.add((SignalMastIcon) l);
6567        } else if (l instanceof MemoryIcon) {
6568            memoryLabelList.add((MemoryIcon) l);
6569        } else if (l instanceof BlockContentsIcon) {
6570            blockContentsLabelList.add((BlockContentsIcon) l);
6571        } else if (l instanceof AnalogClock2Display) {
6572            clocks.add((AnalogClock2Display) l);
6573        } else if (l instanceof MultiSensorIcon) {
6574            multiSensors.add((MultiSensorIcon) l);
6575        }
6576
6577        if (l instanceof PositionableLabel) {
6578            if (((PositionableLabel) l).isBackground()) {
6579                backgroundImage.add((PositionableLabel) l);
6580            } else {
6581                labelImage.add((PositionableLabel) l);
6582            }
6583        }
6584        unionToPanelBounds(l.getBounds(new Rectangle()));
6585        setDirty();
6586    }
6587
6588    /**
6589     * Add a memory label to the Draw Panel
6590     */
6591    void addMemory() {
6592        String memoryName = leToolBarPanel.textMemoryComboBox.getSelectedItemDisplayName();
6593        if (memoryName == null) {
6594            memoryName = "";
6595        }
6596
6597        if (memoryName.isEmpty()) {
6598            JOptionPane.showMessageDialog(this, Bundle.getMessage("Error11a"),
6599                    Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
6600            return;
6601        }
6602        MemoryIcon l = new MemoryIcon(" ", this);
6603        l.setMemory(memoryName);
6604        Memory xMemory = l.getMemory();
6605
6606        if (xMemory != null) {
6607            String uname = xMemory.getDisplayName();
6608            if (!uname.equals(memoryName)) {
6609                // put the system name in the memory field
6610                leToolBarPanel.textMemoryComboBox.setSelectedItem(xMemory);
6611            }
6612        }
6613        setNextLocation(l);
6614        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
6615        l.setDisplayLevel(Editor.LABELS);
6616        l.setForeground(defaultTextColor);
6617        unionToPanelBounds(l.getBounds());
6618        putItem(l); // note: this calls unionToPanelBounds & setDirty()
6619    }
6620
6621    void addBlockContents() {
6622        String newName = leToolBarPanel.blockContentsComboBox.getSelectedItemDisplayName();
6623        if (newName == null) {
6624            newName = "";
6625        }
6626
6627        if (newName.isEmpty()) {
6628            JOptionPane.showMessageDialog(this, Bundle.getMessage("Error11b"),
6629                    Bundle.getMessage("ErrorTitle"), JOptionPane.ERROR_MESSAGE);
6630            return;
6631        }
6632        BlockContentsIcon l = new BlockContentsIcon(" ", this);
6633        l.setBlock(newName);
6634        Block xMemory = l.getBlock();
6635
6636        if (xMemory != null) {
6637            String uname = xMemory.getDisplayName();
6638            if (!uname.equals(newName)) {
6639                // put the system name in the memory field
6640                leToolBarPanel.blockContentsComboBox.setSelectedItem(xMemory);
6641            }
6642        }
6643        setNextLocation(l);
6644        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
6645        l.setDisplayLevel(Editor.LABELS);
6646        l.setForeground(defaultTextColor);
6647        putItem(l); // note: this calls unionToPanelBounds & setDirty()
6648    }
6649
6650    /**
6651     * Add a Reporter Icon to the panel.
6652     *
6653     * @param reporter the reporter icon to add.
6654     * @param xx       the horizontal location.
6655     * @param yy       the vertical location.
6656     */
6657    public void addReporter(@Nonnull Reporter reporter, int xx, int yy) {
6658        ReporterIcon l = new ReporterIcon(this);
6659        l.setReporter(reporter);
6660        l.setLocation(xx, yy);
6661        l.setSize(l.getPreferredSize().width, l.getPreferredSize().height);
6662        l.setDisplayLevel(Editor.LABELS);
6663        unionToPanelBounds(l.getBounds());
6664        putItem(l); // note: this calls unionToPanelBounds & setDirty()
6665    }
6666
6667    /**
6668     * Add an icon to the target
6669     */
6670    void addIcon() {
6671        PositionableLabel l = new PositionableLabel(leToolBarPanel.iconEditor.getIcon(0), this);
6672        setNextLocation(l);
6673        l.setDisplayLevel(Editor.ICONS);
6674        unionToPanelBounds(l.getBounds());
6675        l.updateSize();
6676        putItem(l); // note: this calls unionToPanelBounds & setDirty()
6677    }
6678
6679    /**
6680     * Add a loco marker to the target
6681     */
6682    @Override
6683    public LocoIcon addLocoIcon(@Nonnull String name) {
6684        LocoIcon l = new LocoIcon(this);
6685        Point2D pt = windowCenter();
6686        if (selectionActive) {
6687            pt = MathUtil.midPoint(getSelectionRect());
6688        }
6689        l.setLocation((int) pt.getX(), (int) pt.getY());
6690        putLocoIcon(l, name);
6691        l.setPositionable(true);
6692        unionToPanelBounds(l.getBounds());
6693        return l;
6694    }
6695
6696    @Override
6697    public void putLocoIcon(@Nonnull LocoIcon l, @Nonnull String name) {
6698        super.putLocoIcon(l, name);
6699        markerImage.add(l);
6700        unionToPanelBounds(l.getBounds());
6701    }
6702
6703    private JFileChooser inputFileChooser = null;
6704
6705    /**
6706     * Add a background image
6707     */
6708    public void addBackground() {
6709        if (inputFileChooser == null) {
6710            inputFileChooser = new JFileChooser(
6711                    String.format("%s%sresources%sicons",
6712                            System.getProperty("user.dir"),
6713                            File.separator,
6714                            File.separator));
6715            if (false) {
6716                // TODO: Discuss with jmri-developers
6717                // this filter will allow any images supported by the current
6718                // operating system. This may not be desirable because it will
6719                // allow images that may not be supported by operating systems
6720                // other than the current one.
6721                inputFileChooser.setFileFilter(new FileNameExtensionFilter("Image files", ImageIO.getReaderFileSuffixes()));
6722            } else {
6723                // TODO: discuss with jmri-developers - support png image files?
6724                inputFileChooser.setFileFilter(new FileNameExtensionFilter("Graphics Files", "gif", "jpg", "png"));
6725            }
6726        }
6727        inputFileChooser.rescanCurrentDirectory();
6728
6729        int retVal = inputFileChooser.showOpenDialog(this);
6730
6731        if (retVal != JFileChooser.APPROVE_OPTION) {
6732            return; // give up if no file selected
6733        }
6734
6735        // NamedIcon icon = new NamedIcon(inputFileChooser.getSelectedFile().getPath(),
6736        // inputFileChooser.getSelectedFile().getPath());
6737        String name = inputFileChooser.getSelectedFile().getPath();
6738
6739        // convert to portable path
6740        name = FileUtil.getPortableFilename(name);
6741
6742        // setup icon
6743        PositionableLabel o = super.setUpBackground(name);
6744        backgroundImage.add(o);
6745        unionToPanelBounds(o.getBounds());
6746        setDirty();
6747    }
6748
6749    // there is no way to call this; could that
6750    //    private boolean remove(@Nonnull Object s)
6751    // is being used instead.
6752    //
6753    ///**
6754    // * Remove a background image from the list of background images
6755    // *
6756    // * @param b PositionableLabel to remove
6757    // */
6758    //private void removeBackground(@Nonnull PositionableLabel b) {
6759    //    if (backgroundImage.contains(b)) {
6760    //        backgroundImage.remove(b);
6761    //        setDirty();
6762    //    }
6763    //}
6764    /**
6765     * add a layout shape to the list of layout shapes
6766     *
6767     * @param p Point2D where the shape should be
6768     * @return the LayoutShape
6769     */
6770    @Nonnull
6771    private LayoutShape addLayoutShape(@Nonnull Point2D p) {
6772        // get unique name
6773        String name = finder.uniqueName("S", ++numShapes);
6774
6775        // create object
6776        LayoutShape o = new LayoutShape(name, p, this);
6777        layoutShapes.add(o);
6778        unionToPanelBounds(o.getBounds());
6779        setDirty();
6780        return o;
6781    }
6782
6783    /**
6784     * Remove a layout shape from the list of layout shapes
6785     *
6786     * @param s the LayoutShape to add
6787     * @return true if added
6788     */
6789    public boolean removeLayoutShape(@Nonnull LayoutShape s) {
6790        boolean result = false;
6791        if (layoutShapes.contains(s)) {
6792            layoutShapes.remove(s);
6793            setDirty();
6794            result = true;
6795            redrawPanel();
6796        }
6797        return result;
6798    }
6799
6800    /**
6801     * Invoke a window to allow you to add a MultiSensor indicator to the target
6802     */
6803    private int multiLocX;
6804    private int multiLocY;
6805
6806    void startMultiSensor() {
6807        multiLocX = xLoc;
6808        multiLocY = yLoc;
6809
6810        if (leToolBarPanel.multiSensorFrame == null) {
6811            // create a common edit frame
6812            leToolBarPanel.multiSensorFrame = new MultiSensorIconFrame(this);
6813            leToolBarPanel.multiSensorFrame.initComponents();
6814            leToolBarPanel.multiSensorFrame.pack();
6815        }
6816        leToolBarPanel.multiSensorFrame.setVisible(true);
6817    }
6818
6819    // Invoked when window has new multi-sensor ready
6820    public void addMultiSensor(@Nonnull MultiSensorIcon l) {
6821        l.setLocation(multiLocX, multiLocY);
6822        putItem(l); // note: this calls unionToPanelBounds & setDirty()
6823        leToolBarPanel.multiSensorFrame.dispose();
6824        leToolBarPanel.multiSensorFrame = null;
6825    }
6826
6827    /**
6828     * Set object location and size for icon and label object as it is created.
6829     * Size comes from the preferredSize; location comes from the fields where
6830     * the user can spec it.
6831     *
6832     * @param obj the positionable object.
6833     */
6834    @Override
6835    public void setNextLocation(@Nonnull Positionable obj) {
6836        obj.setLocation(xLoc, yLoc);
6837    }
6838
6839    //
6840    // singleton (one per-LayoutEditor) accessors
6841    //
6842    private ConnectivityUtil conTools = null;
6843
6844    @Nonnull
6845    public ConnectivityUtil getConnectivityUtil() {
6846        if (conTools == null) {
6847            conTools = new ConnectivityUtil(this);
6848        }
6849        return conTools;
6850    }
6851
6852    private LayoutEditorTools tools = null;
6853
6854    @Nonnull
6855    public LayoutEditorTools getLETools() {
6856        if (tools == null) {
6857            tools = new LayoutEditorTools(this);
6858        }
6859        return tools;
6860    }
6861
6862    private LayoutEditorAuxTools auxTools = null;
6863
6864    @Nonnull
6865    public LayoutEditorAuxTools getLEAuxTools() {
6866        if (auxTools == null) {
6867            auxTools = new LayoutEditorAuxTools(this);
6868        }
6869        return auxTools;
6870    }
6871
6872    private LayoutEditorChecks layoutEditorChecks = null;
6873
6874    @Nonnull
6875    public LayoutEditorChecks getLEChecks() {
6876        if (layoutEditorChecks == null) {
6877            layoutEditorChecks = new LayoutEditorChecks(this);
6878        }
6879        return layoutEditorChecks;
6880    }
6881
6882    /**
6883     * Invoked by DeletePanel menu item Validate user intent before deleting
6884     */
6885    @Override
6886    public boolean deletePanel() {
6887        // verify deletion
6888        if (!super.deletePanel()) {
6889            return false; // return without deleting if "No" response
6890        }
6891        clearLayoutTracks();
6892        return true;
6893    }
6894
6895    /**
6896     * Control whether target panel items are editable. Does this by invoking
6897     * the {@link Editor#setAllEditable} function of the parent class. This also
6898     * controls the relevant pop-up menu items (which are the primary way that
6899     * items are edited).
6900     *
6901     * @param editable true for editable.
6902     */
6903    @Override
6904    public void setAllEditable(boolean editable) {
6905        int restoreScroll = _scrollState;
6906
6907        super.setAllEditable(editable);
6908
6909        if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
6910            if (editable) {
6911                createfloatingEditToolBoxFrame();
6912                createFloatingHelpPanel();
6913            } else {
6914                deletefloatingEditToolBoxFrame();
6915            }
6916        } else {
6917            editToolBarContainerPanel.setVisible(editable);
6918        }
6919        setShowHidden(editable);
6920
6921        if (editable) {
6922            setScroll(Editor.SCROLL_BOTH);
6923            _scrollState = restoreScroll;
6924        } else {
6925            setScroll(_scrollState);
6926        }
6927
6928        // these may not be set up yet...
6929        if (helpBarPanel != null) {
6930            if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
6931                if (floatEditHelpPanel != null) {
6932                    floatEditHelpPanel.setVisible(isEditable() && getShowHelpBar());
6933                }
6934            } else {
6935                helpBarPanel.setVisible(editable && getShowHelpBar());
6936            }
6937        }
6938        awaitingIconChange = false;
6939        editModeCheckBoxMenuItem.setSelected(editable);
6940        redrawPanel();
6941    }
6942
6943    /**
6944     * Control whether panel items are positionable. Markers are always
6945     * positionable.
6946     *
6947     * @param state true for positionable.
6948     */
6949    @Override
6950    public void setAllPositionable(boolean state) {
6951        super.setAllPositionable(state);
6952
6953        markerImage.forEach((p) -> p.setPositionable(true));
6954    }
6955
6956    /**
6957     * Control whether target panel items are controlling layout items. Does
6958     * this by invoke the {@link Positionable#setControlling} function of each
6959     * item on the target panel. This also controls the relevant pop-up menu
6960     * items.
6961     *
6962     * @param state true for controlling.
6963     */
6964    public void setTurnoutAnimation(boolean state) {
6965        if (animationCheckBoxMenuItem.isSelected() != state) {
6966            animationCheckBoxMenuItem.setSelected(state);
6967        }
6968
6969        if (animatingLayout != state) {
6970            animatingLayout = state;
6971            redrawPanel();
6972        }
6973    }
6974
6975    public boolean isAnimating() {
6976        return animatingLayout;
6977    }
6978
6979    public boolean getScroll() {
6980        // deprecated but kept to allow opening files
6981        // on version 2.5.1 and earlier
6982        return _scrollState != Editor.SCROLL_NONE;
6983    }
6984
6985//    public Color getDefaultBackgroundColor() {
6986//        return defaultBackgroundColor;
6987//    }
6988    public String getDefaultTrackColor() {
6989        return ColorUtil.colorToColorName(defaultTrackColor);
6990    }
6991
6992    /**
6993     *
6994     * Getter defaultTrackColor.
6995     *
6996     * @return block default color as Color
6997     */
6998    @Nonnull
6999    public Color getDefaultTrackColorColor() {
7000        return defaultTrackColor;
7001    }
7002
7003    @Nonnull
7004    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
7005    public String getDefaultOccupiedTrackColor() {
7006        return ColorUtil.colorToColorName(defaultOccupiedTrackColor);
7007    }
7008
7009    /**
7010     *
7011     * Getter defaultOccupiedTrackColor.
7012     *
7013     * @return block default occupied color as Color
7014     */
7015    @Nonnull
7016    public Color getDefaultOccupiedTrackColorColor() {
7017        return defaultOccupiedTrackColor;
7018    }
7019
7020    @Nonnull
7021    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
7022    public String getDefaultAlternativeTrackColor() {
7023        return ColorUtil.colorToColorName(defaultAlternativeTrackColor);
7024    }
7025
7026    /**
7027     *
7028     * Getter defaultAlternativeTrackColor.
7029     *
7030     * @return block default alternative color as Color
7031     */
7032    @Nonnull
7033    public Color getDefaultAlternativeTrackColorColor() {
7034        return defaultAlternativeTrackColor;
7035    }
7036
7037    @Nonnull
7038    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
7039    public String getDefaultTextColor() {
7040        return ColorUtil.colorToColorName(defaultTextColor);
7041    }
7042
7043    @Nonnull
7044    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
7045    public String getTurnoutCircleColor() {
7046        return ColorUtil.colorToColorName(turnoutCircleColor);
7047    }
7048
7049    @Nonnull
7050    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "coloToColorName only returns null if null passed to it")
7051    public String getTurnoutCircleThrownColor() {
7052        return ColorUtil.colorToColorName(turnoutCircleThrownColor);
7053    }
7054
7055    public boolean isTurnoutFillControlCircles() {
7056        return turnoutFillControlCircles;
7057    }
7058
7059    public int getTurnoutCircleSize() {
7060        return turnoutCircleSize;
7061    }
7062
7063    public boolean isTurnoutDrawUnselectedLeg() {
7064        return turnoutDrawUnselectedLeg;
7065    }
7066
7067    public String getLayoutName() {
7068        return layoutName;
7069    }
7070
7071    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7072    public boolean getShowHelpBar() {
7073        return showHelpBar;
7074    }
7075
7076    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7077    public boolean getDrawGrid() {
7078        return drawGrid;
7079    }
7080
7081    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7082    public boolean getSnapOnAdd() {
7083        return snapToGridOnAdd;
7084    }
7085
7086    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7087    public boolean getSnapOnMove() {
7088        return snapToGridOnMove;
7089    }
7090
7091    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7092    public boolean getAntialiasingOn() {
7093        return antialiasingOn;
7094    }
7095
7096    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7097    public boolean getHighlightSelectedBlock() {
7098        return highlightSelectedBlockFlag;
7099    }
7100
7101    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7102    public boolean getTurnoutCircles() {
7103        return turnoutCirclesWithoutEditMode;
7104    }
7105
7106    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7107    public boolean getTooltipsNotEdit() {
7108        return tooltipsWithoutEditMode;
7109    }
7110
7111    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7112    public boolean getTooltipsInEdit() {
7113        return tooltipsInEditMode;
7114    }
7115
7116    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7117    public boolean getAutoBlockAssignment() {
7118        return autoAssignBlocks;
7119    }
7120
7121    public void setLayoutDimensions(int windowWidth, int windowHeight, int windowX, int windowY, int panelWidth, int panelHeight) {
7122        setLayoutDimensions(windowWidth, windowHeight, windowX, windowY, panelWidth, panelHeight, false);
7123    }
7124
7125    public void setLayoutDimensions(int windowWidth, int windowHeight, int windowX, int windowY, int panelWidth, int panelHeight, boolean merge) {
7126
7127        gContext.setUpperLeftX(windowX);
7128        gContext.setUpperLeftY(windowY);
7129        setLocation(gContext.getUpperLeftX(), gContext.getUpperLeftY());
7130
7131        gContext.setWindowWidth(windowWidth);
7132        gContext.setWindowHeight(windowHeight);
7133        setSize(windowWidth, windowHeight);
7134
7135        Rectangle2D panelBounds = new Rectangle2D.Double(0.0, 0.0, panelWidth, panelHeight);
7136
7137        if (merge) {
7138            panelBounds.add(calculateMinimumLayoutBounds());
7139        }
7140        setPanelBounds(panelBounds);
7141    }
7142
7143    @Nonnull
7144    public Rectangle2D getPanelBounds() {
7145        return new Rectangle2D.Double(0.0, 0.0, gContext.getLayoutWidth(), gContext.getLayoutHeight());
7146    }
7147
7148    public void setPanelBounds(@Nonnull Rectangle2D newBounds) {
7149        // don't let origin go negative
7150        newBounds = newBounds.createIntersection(MathUtil.zeroToInfinityRectangle2D);
7151
7152        if (!getPanelBounds().equals(newBounds)) {
7153            gContext.setLayoutWidth((int) newBounds.getWidth());
7154            gContext.setLayoutHeight((int) newBounds.getHeight());
7155            resetTargetSize();
7156        }
7157        log.debug("setPanelBounds(({})", newBounds);
7158    }
7159
7160    private void resetTargetSize() {
7161        int newTargetWidth = (int) (gContext.getLayoutWidth() * getZoom());
7162        int newTargetHeight = (int) (gContext.getLayoutHeight() * getZoom());
7163
7164        Dimension targetPanelSize = getTargetPanelSize();
7165        int oldTargetWidth = (int) targetPanelSize.getWidth();
7166        int oldTargetHeight = (int) targetPanelSize.getHeight();
7167
7168        if ((newTargetWidth != oldTargetWidth) || (newTargetHeight != oldTargetHeight)) {
7169            setTargetPanelSize(newTargetWidth, newTargetHeight);
7170            adjustScrollBars();
7171        }
7172    }
7173
7174    // this will grow the panel bounds based on items added to the layout
7175    @Nonnull
7176    public Rectangle2D unionToPanelBounds(@Nonnull Rectangle2D bounds) {
7177        Rectangle2D result = getPanelBounds();
7178
7179        // make room to expand
7180        Rectangle2D b = MathUtil.inset(bounds, gContext.getGridSize() * gContext.getGridSize2nd() / -2.0);
7181
7182        // don't let origin go negative
7183        b = b.createIntersection(MathUtil.zeroToInfinityRectangle2D);
7184
7185        result.add(b);
7186
7187        setPanelBounds(result);
7188        return result;
7189    }
7190
7191    /**
7192     * @param color value to set the default track color to.
7193     */
7194    public void setDefaultTrackColor(@Nonnull Color color) {
7195        defaultTrackColor = color;
7196        JmriColorChooser.addRecentColor(color);
7197    }
7198
7199    /**
7200     * @param color value to set the default occupied track color to.
7201     */
7202    public void setDefaultOccupiedTrackColor(@Nonnull Color color) {
7203        defaultOccupiedTrackColor = color;
7204        JmriColorChooser.addRecentColor(color);
7205    }
7206
7207    /**
7208     * @param color value to set the default alternate track color to.
7209     */
7210    public void setDefaultAlternativeTrackColor(@Nonnull Color color) {
7211        defaultAlternativeTrackColor = color;
7212        JmriColorChooser.addRecentColor(color);
7213    }
7214
7215    /**
7216     * @param color new color for turnout circle.
7217     */
7218    public void setTurnoutCircleColor(@CheckForNull Color color) {
7219        if (color == null) {
7220            turnoutCircleColor = getDefaultTrackColorColor();
7221        } else {
7222            turnoutCircleColor = color;
7223            JmriColorChooser.addRecentColor(color);
7224        }
7225    }
7226
7227    /**
7228     * @param color new color for turnout circle.
7229     */
7230    public void setTurnoutCircleThrownColor(@CheckForNull Color color) {
7231        if (color == null) {
7232            turnoutCircleThrownColor = getDefaultTrackColorColor();
7233        } else {
7234            turnoutCircleThrownColor = color;
7235            JmriColorChooser.addRecentColor(color);
7236        }
7237    }
7238
7239    /**
7240     * Should only be invoked on the GUI (Swing) thread.
7241     *
7242     * @param state true to fill in turnout control circles, else false.
7243     */
7244    @InvokeOnGuiThread
7245    public void setTurnoutFillControlCircles(boolean state) {
7246        if (turnoutFillControlCircles != state) {
7247            turnoutFillControlCircles = state;
7248            turnoutFillControlCirclesCheckBoxMenuItem.setSelected(turnoutFillControlCircles);
7249        }
7250    }
7251
7252    public void setTurnoutCircleSize(int size) {
7253        // this is an int
7254        turnoutCircleSize = size;
7255
7256        // these are doubles
7257        circleRadius = SIZE * size;
7258        circleDiameter = 2.0 * circleRadius;
7259
7260        setOptionMenuTurnoutCircleSize();
7261    }
7262
7263    /**
7264     * Should only be invoked on the GUI (Swing) thread.
7265     *
7266     * @param state true to draw unselected legs, else false.
7267     */
7268    @InvokeOnGuiThread
7269    public void setTurnoutDrawUnselectedLeg(boolean state) {
7270        if (turnoutDrawUnselectedLeg != state) {
7271            turnoutDrawUnselectedLeg = state;
7272            turnoutDrawUnselectedLegCheckBoxMenuItem.setSelected(turnoutDrawUnselectedLeg);
7273        }
7274    }
7275
7276    /**
7277     * @param color value to set the default text color to.
7278     */
7279    public void setDefaultTextColor(@Nonnull Color color) {
7280        defaultTextColor = color;
7281        JmriColorChooser.addRecentColor(color);
7282    }
7283
7284    /**
7285     * @param color value to set the panel background to.
7286     */
7287    public void setDefaultBackgroundColor(@Nonnull Color color) {
7288        defaultBackgroundColor = color;
7289        JmriColorChooser.addRecentColor(color);
7290    }
7291
7292    public void setLayoutName(@Nonnull String name) {
7293        layoutName = name;
7294    }
7295
7296    /**
7297     * Should only be invoked on the GUI (Swing) thread.
7298     *
7299     * @param state true to show the help bar, else false.
7300     */
7301    @InvokeOnGuiThread  // due to the setSelected call on a possibly-visible item
7302    public void setShowHelpBar(boolean state) {
7303        if (showHelpBar != state) {
7304            showHelpBar = state;
7305
7306            // these may not be set up yet...
7307            if (showHelpCheckBoxMenuItem != null) {
7308                showHelpCheckBoxMenuItem.setSelected(showHelpBar);
7309            }
7310
7311            if (toolBarSide.equals(ToolBarSide.eFLOAT)) {
7312                if (floatEditHelpPanel != null) {
7313                    floatEditHelpPanel.setVisible(isEditable() && showHelpBar);
7314                }
7315            } else {
7316                if (helpBarPanel != null) {
7317                    helpBarPanel.setVisible(isEditable() && showHelpBar);
7318
7319                }
7320            }
7321            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".showHelpBar", showHelpBar));
7322        }
7323    }
7324
7325    /**
7326     * Should only be invoked on the GUI (Swing) thread.
7327     *
7328     * @param state true to show the draw grid, else false.
7329     */
7330    @InvokeOnGuiThread
7331    public void setDrawGrid(boolean state) {
7332        if (drawGrid != state) {
7333            drawGrid = state;
7334            showGridCheckBoxMenuItem.setSelected(drawGrid);
7335        }
7336    }
7337
7338    /**
7339     * Should only be invoked on the GUI (Swing) thread.
7340     *
7341     * @param state true to set snap to grid on add, else false.
7342     */
7343    @InvokeOnGuiThread
7344    public void setSnapOnAdd(boolean state) {
7345        if (snapToGridOnAdd != state) {
7346            snapToGridOnAdd = state;
7347            snapToGridOnAddCheckBoxMenuItem.setSelected(snapToGridOnAdd);
7348        }
7349    }
7350
7351    /**
7352     * Should only be invoked on the GUI (Swing) thread.
7353     *
7354     * @param state true to set snap on move, else false.
7355     */
7356    @InvokeOnGuiThread
7357    public void setSnapOnMove(boolean state) {
7358        if (snapToGridOnMove != state) {
7359            snapToGridOnMove = state;
7360            snapToGridOnMoveCheckBoxMenuItem.setSelected(snapToGridOnMove);
7361        }
7362    }
7363
7364    /**
7365     * Should only be invoked on the GUI (Swing) thread.
7366     *
7367     * @param state true to set anti-aliasing flag on, else false.
7368     */
7369    @InvokeOnGuiThread
7370    public void setAntialiasingOn(boolean state) {
7371        if (antialiasingOn != state) {
7372            antialiasingOn = state;
7373
7374            // this may not be set up yet...
7375            if (antialiasingOnCheckBoxMenuItem != null) {
7376                antialiasingOnCheckBoxMenuItem.setSelected(antialiasingOn);
7377
7378            }
7379            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".antialiasingOn", antialiasingOn));
7380        }
7381    }
7382
7383    // enable/disable using the "Extra" color to highlight the selected block
7384    public void setHighlightSelectedBlock(boolean state) {
7385        if (highlightSelectedBlockFlag != state) {
7386            highlightSelectedBlockFlag = state;
7387
7388            // this may not be set up yet...
7389            if (leToolBarPanel.highlightBlockCheckBox != null) {
7390                leToolBarPanel.highlightBlockCheckBox.setSelected(highlightSelectedBlockFlag);
7391
7392            }
7393
7394            InstanceManager.getOptionalDefault(UserPreferencesManager.class).ifPresent((prefsMgr) -> prefsMgr.setSimplePreferenceState(getWindowFrameRef() + ".highlightSelectedBlock", highlightSelectedBlockFlag));
7395
7396            // thread this so it won't break the AppVeyor checks
7397            jmri.util.ThreadingUtil.newThread(() -> {
7398                if (highlightSelectedBlockFlag) {
7399                    // use the "Extra" color to highlight the selected block
7400                    if (!highlightBlockInComboBox(leToolBarPanel.blockIDComboBox)) {
7401                        highlightBlockInComboBox(leToolBarPanel.blockContentsComboBox);
7402                    }
7403                } else {
7404                    // undo using the "Extra" color to highlight the selected block
7405                    Block block = leToolBarPanel.blockIDComboBox.getSelectedItem();
7406                    highlightBlock(null);
7407                    leToolBarPanel.blockIDComboBox.setSelectedItem(block);
7408                }
7409            }).start();
7410        }
7411    }
7412
7413    //
7414    // highlight the block selected by the specified combo Box
7415    //
7416    public boolean highlightBlockInComboBox(@Nonnull NamedBeanComboBox<Block> inComboBox) {
7417        return highlightBlock(inComboBox.getSelectedItem());
7418    }
7419
7420    /**
7421     * highlight the specified block
7422     *
7423     * @param inBlock the block
7424     * @return true if block was highlighted
7425     */
7426    public boolean highlightBlock(@CheckForNull Block inBlock) {
7427        boolean result = false; // assume failure (pessimist!)
7428
7429        if (leToolBarPanel.blockIDComboBox.getSelectedItem() != inBlock) {
7430            leToolBarPanel.blockIDComboBox.setSelectedItem(inBlock);
7431        }
7432
7433        LayoutBlockManager lbm = InstanceManager.getDefault(LayoutBlockManager.class
7434        );
7435        Set<Block> l = leToolBarPanel.blockIDComboBox.getManager().getNamedBeanSet();
7436        for (Block b : l) {
7437            LayoutBlock lb = lbm.getLayoutBlock(b);
7438            if (lb != null) {
7439                boolean enable = ((inBlock != null) && b.equals(inBlock));
7440                lb.setUseExtraColor(enable);
7441                result |= enable;
7442            }
7443        }
7444        return result;
7445    }
7446
7447    /**
7448     * highlight the specified layout block
7449     *
7450     * @param inLayoutBlock the layout block
7451     * @return true if layout block was highlighted
7452     */
7453    public boolean highlightLayoutBlock(@Nonnull LayoutBlock inLayoutBlock) {
7454        return highlightBlock(inLayoutBlock.getBlock());
7455    }
7456
7457    public void setTurnoutCircles(boolean state) {
7458        if (turnoutCirclesWithoutEditMode != state) {
7459            turnoutCirclesWithoutEditMode = state;
7460            if (turnoutCirclesOnCheckBoxMenuItem != null) {
7461                turnoutCirclesOnCheckBoxMenuItem.setSelected(turnoutCirclesWithoutEditMode);
7462            }
7463        }
7464    }
7465
7466    public void setAutoBlockAssignment(boolean boo) {
7467        if (autoAssignBlocks != boo) {
7468            autoAssignBlocks = boo;
7469            if (autoAssignBlocksCheckBoxMenuItem != null) {
7470                autoAssignBlocksCheckBoxMenuItem.setSelected(autoAssignBlocks);
7471            }
7472        }
7473    }
7474
7475    public void setTooltipsNotEdit(boolean state) {
7476        if (tooltipsWithoutEditMode != state) {
7477            tooltipsWithoutEditMode = state;
7478            setTooltipSubMenu();
7479        }
7480    }
7481
7482    public void setTooltipsInEdit(boolean state) {
7483        if (tooltipsInEditMode != state) {
7484            tooltipsInEditMode = state;
7485            setTooltipSubMenu();
7486        }
7487    }
7488
7489    private void setTooltipSubMenu() {
7490        if (tooltipNoneMenuItem != null) {
7491            tooltipNoneMenuItem.setSelected((!tooltipsInEditMode) && (!tooltipsWithoutEditMode));
7492            tooltipAlwaysMenuItem.setSelected((tooltipsInEditMode) && (tooltipsWithoutEditMode));
7493            tooltipInEditMenuItem.setSelected((tooltipsInEditMode) && (!tooltipsWithoutEditMode));
7494            tooltipNotInEditMenuItem.setSelected((!tooltipsInEditMode) && (tooltipsWithoutEditMode));
7495        }
7496    }
7497
7498    // accessor routines for turnout size parameters
7499    public void setTurnoutBX(double bx) {
7500        turnoutBX = bx;
7501        setDirty();
7502    }
7503
7504    public double getTurnoutBX() {
7505        return turnoutBX;
7506    }
7507
7508    public void setTurnoutCX(double cx) {
7509        turnoutCX = cx;
7510        setDirty();
7511    }
7512
7513    public double getTurnoutCX() {
7514        return turnoutCX;
7515    }
7516
7517    public void setTurnoutWid(double wid) {
7518        turnoutWid = wid;
7519        setDirty();
7520    }
7521
7522    public double getTurnoutWid() {
7523        return turnoutWid;
7524    }
7525
7526    public void setXOverLong(double lg) {
7527        xOverLong = lg;
7528        setDirty();
7529    }
7530
7531    public double getXOverLong() {
7532        return xOverLong;
7533    }
7534
7535    public void setXOverHWid(double hwid) {
7536        xOverHWid = hwid;
7537        setDirty();
7538    }
7539
7540    public double getXOverHWid() {
7541        return xOverHWid;
7542    }
7543
7544    public void setXOverShort(double sh) {
7545        xOverShort = sh;
7546        setDirty();
7547    }
7548
7549    public double getXOverShort() {
7550        return xOverShort;
7551    }
7552
7553    // reset turnout sizes to program defaults
7554    private void resetTurnoutSize() {
7555        turnoutBX = LayoutTurnout.turnoutBXDefault;
7556        turnoutCX = LayoutTurnout.turnoutCXDefault;
7557        turnoutWid = LayoutTurnout.turnoutWidDefault;
7558        xOverLong = LayoutTurnout.xOverLongDefault;
7559        xOverHWid = LayoutTurnout.xOverHWidDefault;
7560        xOverShort = LayoutTurnout.xOverShortDefault;
7561        setDirty();
7562    }
7563
7564    public void setDirectTurnoutControl(boolean boo) {
7565        useDirectTurnoutControl = boo;
7566        useDirectTurnoutControlCheckBoxMenuItem.setSelected(useDirectTurnoutControl);
7567    }
7568
7569    // TODO: @Deprecated // Java standard pattern for boolean getters is "isShowHelpBar()"
7570    public boolean getDirectTurnoutControl() {
7571        return useDirectTurnoutControl;
7572    }
7573
7574    // final initialization routine for loading a LayoutEditor
7575    public void setConnections() {
7576        getLayoutTracks().forEach((lt) -> lt.setObjects(this));
7577        getLEAuxTools().initializeBlockConnectivity();
7578        log.debug("Initializing Block Connectivity for {}", getLayoutName());
7579
7580        // reset the panel changed bit
7581        resetDirty();
7582    }
7583
7584    // these are convenience methods to return rectangles
7585    // to use when (hit point-in-rect testing
7586    //
7587    // compute the control point rect at inPoint
7588    public @Nonnull
7589    Rectangle2D layoutEditorControlRectAt(@Nonnull Point2D inPoint) {
7590        return new Rectangle2D.Double(inPoint.getX() - SIZE,
7591                inPoint.getY() - SIZE, SIZE2, SIZE2);
7592    }
7593
7594    // compute the turnout circle control rect at inPoint
7595    public @Nonnull
7596    Rectangle2D layoutEditorControlCircleRectAt(@Nonnull Point2D inPoint) {
7597        return new Rectangle2D.Double(inPoint.getX() - circleRadius,
7598                inPoint.getY() - circleRadius, circleDiameter, circleDiameter);
7599    }
7600
7601    /**
7602     * Special internal class to allow drawing of layout to a JLayeredPane This
7603     * is the 'target' pane where the layout is displayed
7604     */
7605    @Override
7606    public void paintTargetPanel(@Nonnull Graphics g) {
7607        // Nothing to do here
7608        // All drawing has been moved into LayoutEditorComponent
7609        // which calls draw.
7610        // This is so the layout is drawn at level three
7611        // (above or below the Positionables)
7612    }
7613
7614    // get selection rectangle
7615    @Nonnull
7616    public Rectangle2D getSelectionRect() {
7617        double selX = Math.min(selectionX, selectionX + selectionWidth);
7618        double selY = Math.min(selectionY, selectionY + selectionHeight);
7619        return new Rectangle2D.Double(selX, selY,
7620                Math.abs(selectionWidth), Math.abs(selectionHeight));
7621    }
7622
7623    // set selection rectangle
7624    public void setSelectionRect(@Nonnull Rectangle2D selectionRect) {
7625        // selectionRect = selectionRect.createIntersection(MathUtil.zeroToInfinityRectangle2D);
7626        selectionX = selectionRect.getX();
7627        selectionY = selectionRect.getY();
7628        selectionWidth = selectionRect.getWidth();
7629        selectionHeight = selectionRect.getHeight();
7630
7631        // There's already code in the super class (Editor) to draw
7632        // the selection rect... We just have to set _selectRect
7633        _selectRect = MathUtil.rectangle2DToRectangle(selectionRect);
7634
7635        selectionRect = MathUtil.scale(selectionRect, getZoom());
7636
7637        JComponent targetPanel = getTargetPanel();
7638        Rectangle targetRect = targetPanel.getVisibleRect();
7639        // this will make it the size of the targetRect
7640        // (effectively centering it onscreen)
7641        Rectangle2D selRect2D = MathUtil.inset(selectionRect,
7642                (selectionRect.getWidth() - targetRect.getWidth()) / 2.0,
7643                (selectionRect.getHeight() - targetRect.getHeight()) / 2.0);
7644        // don't let the origin go negative
7645        selRect2D = selRect2D.createIntersection(MathUtil.zeroToInfinityRectangle2D);
7646        Rectangle selRect = MathUtil.rectangle2DToRectangle(selRect2D);
7647        if (!targetRect.contains(selRect)) {
7648            targetPanel.scrollRectToVisible(selRect);
7649        }
7650
7651        clearSelectionGroups();
7652        selectionActive = true;
7653        createSelectionGroups();
7654        // redrawPanel(); // createSelectionGroups already calls this
7655    }
7656
7657    public void setSelectRect(Rectangle rectangle) {
7658        _selectRect = rectangle;
7659    }
7660
7661    /*
7662    // TODO: This compiles but I can't get the syntax correct to pass the (sub-)class
7663    public List<LayoutTrack> getLayoutTracksOfClass(@Nonnull Class<LayoutTrack> layoutTrackClass) {
7664    return getLayoutTracks().stream()
7665    .filter(item -> item instanceof PositionablePoint)
7666    .filter(layoutTrackClass::isInstance)
7667    //.map(layoutTrackClass::cast)  // TODO: Do we need this? if not dead-code-strip
7668    .collect(Collectors.toList());
7669    }
7670
7671    // TODO: This compiles but I can't get the syntax correct to pass the array of (sub-)classes
7672    public List<LayoutTrack> getLayoutTracksOfClasses(@Nonnull List<Class<? extends LayoutTrack>> layoutTrackClasses) {
7673    return getLayoutTracks().stream()
7674    .filter(o -> layoutTrackClasses.contains(o.getClass()))
7675    .collect(Collectors.toList());
7676    }
7677
7678    // TODO: This compiles but I can't get the syntax correct to pass the (sub-)class
7679    public List<LayoutTrack> getLayoutTracksOfClass(@Nonnull Class<? extends LayoutTrack> layoutTrackClass) {
7680    return getLayoutTracksOfClasses(new ArrayList<>(Arrays.asList(layoutTrackClass)));
7681    }
7682
7683    public List<PositionablePoint> getPositionablePoints() {
7684    return getLayoutTracksOfClass(PositionablePoint);
7685    }
7686     */
7687    public @Nonnull
7688    Stream<LayoutTrack> getLayoutTracksOfClass(Class<? extends LayoutTrack> layoutTrackClass) {
7689        return getLayoutTracks().stream()
7690                .filter(layoutTrackClass::isInstance)
7691                .map(layoutTrackClass::cast);
7692    }
7693
7694    public @Nonnull
7695    Stream<LayoutTrackView> getLayoutTrackViewsOfClass(Class<? extends LayoutTrackView> layoutTrackViewClass) {
7696        return getLayoutTrackViews().stream()
7697                .filter(layoutTrackViewClass::isInstance)
7698                .map(layoutTrackViewClass::cast);
7699    }
7700
7701    public @Nonnull
7702    List<PositionablePointView> getPositionablePointViews() {
7703        return getLayoutTrackViewsOfClass(PositionablePointView.class)
7704                .map(PositionablePointView.class::cast)
7705                .collect(Collectors.toCollection(ArrayList::new));
7706    }
7707
7708    public @Nonnull
7709    List<PositionablePoint> getPositionablePoints() {
7710        return getLayoutTracksOfClass(PositionablePoint.class)
7711                .map(PositionablePoint.class::cast)
7712                .collect(Collectors.toCollection(ArrayList::new));
7713    }
7714
7715    public @Nonnull
7716    List<LayoutSlipView> getLayoutSlipViews() {
7717        return getLayoutTrackViewsOfClass(LayoutSlipView.class)
7718                .map(LayoutSlipView.class::cast)
7719                .collect(Collectors.toCollection(ArrayList::new));
7720    }
7721
7722    public @Nonnull
7723    List<LayoutSlip> getLayoutSlips() {
7724        return getLayoutTracksOfClass(LayoutSlip.class)
7725                .map(LayoutSlip.class::cast)
7726                .collect(Collectors.toCollection(ArrayList::new));
7727    }
7728
7729    public @Nonnull
7730    List<TrackSegmentView> getTrackSegmentViews() {
7731        return getLayoutTrackViewsOfClass(TrackSegmentView.class)
7732                .map(TrackSegmentView.class::cast)
7733                .collect(Collectors.toCollection(ArrayList::new));
7734    }
7735
7736    public @Nonnull
7737    List<TrackSegment> getTrackSegments() {
7738        return getLayoutTracksOfClass(TrackSegment.class)
7739                .map(TrackSegment.class::cast)
7740                .collect(Collectors.toCollection(ArrayList::new));
7741    }
7742
7743    public @Nonnull
7744    List<LayoutTurnoutView> getLayoutTurnoutViews() { // this specifically does not include slips
7745        return getLayoutTrackViews().stream() // next line excludes LayoutSlips
7746                .filter((o) -> (!(o instanceof LayoutSlipView) && (o instanceof LayoutTurnoutView)))
7747                .map(LayoutTurnoutView.class::cast)
7748                .collect(Collectors.toCollection(ArrayList::new));
7749    }
7750
7751    public @Nonnull
7752    List<LayoutTurnout> getLayoutTurnouts() { // this specifically does not include slips
7753        return getLayoutTracks().stream() // next line excludes LayoutSlips
7754                .filter((o) -> (!(o instanceof LayoutSlip) && (o instanceof LayoutTurnout)))
7755                .map(LayoutTurnout.class::cast)
7756                .collect(Collectors.toCollection(ArrayList::new));
7757    }
7758
7759    public @Nonnull
7760    List<LayoutTurntable> getLayoutTurntables() {
7761        return getLayoutTracksOfClass(LayoutTurntable.class)
7762                .map(LayoutTurntable.class::cast)
7763                .collect(Collectors.toCollection(ArrayList::new));
7764    }
7765
7766    public @Nonnull
7767    List<LayoutTurntableView> getLayoutTurntableViews() {
7768        return getLayoutTrackViewsOfClass(LayoutTurntableView.class)
7769                .map(LayoutTurntableView.class::cast)
7770                .collect(Collectors.toCollection(ArrayList::new));
7771    }
7772
7773    public @Nonnull
7774    List<LevelXing> getLevelXings() {
7775        return getLayoutTracksOfClass(LevelXing.class)
7776                .map(LevelXing.class::cast)
7777                .collect(Collectors.toCollection(ArrayList::new));
7778    }
7779
7780    public @Nonnull
7781    List<LevelXingView> getLevelXingViews() {
7782        return getLayoutTrackViewsOfClass(LevelXingView.class)
7783                .map(LevelXingView.class::cast)
7784                .collect(Collectors.toCollection(ArrayList::new));
7785    }
7786
7787    /**
7788     * Read-only access to the list of LayoutTrack family objects. The returned
7789     * list will throw UnsupportedOperationException if you attempt to modify
7790     * it.
7791     *
7792     * @return unmodifiable copy of layout track list.
7793     */
7794    @Nonnull
7795    final public List<LayoutTrack> getLayoutTracks() {
7796        return Collections.unmodifiableList(layoutTrackList);
7797    }
7798
7799    public @Nonnull
7800    List<LayoutTurnoutView> getLayoutTurnoutAndSlipViews() {
7801        return getLayoutTrackViewsOfClass(LayoutTurnoutView.class
7802        )
7803                .map(LayoutTurnoutView.class::cast)
7804                .collect(Collectors.toCollection(ArrayList::new));
7805    }
7806
7807    public @Nonnull
7808    List<LayoutTurnout> getLayoutTurnoutsAndSlips() {
7809        return getLayoutTracksOfClass(LayoutTurnout.class
7810        )
7811                .map(LayoutTurnout.class::cast)
7812                .collect(Collectors.toCollection(ArrayList::new));
7813    }
7814
7815    /**
7816     * Read-only access to the list of LayoutTrackView family objects. The
7817     * returned list will throw UnsupportedOperationException if you attempt to
7818     * modify it.
7819     *
7820     * @return unmodifiable copy of track views.
7821     */
7822    @Nonnull
7823    final public List<LayoutTrackView> getLayoutTrackViews() {
7824        return Collections.unmodifiableList(layoutTrackViewList);
7825    }
7826
7827    private final List<LayoutTrack> layoutTrackList = new ArrayList<>();
7828    private final List<LayoutTrackView> layoutTrackViewList = new ArrayList<>();
7829    private final Map<LayoutTrack, LayoutTrackView> trkToView = new HashMap<>();
7830    private final Map<LayoutTrackView, LayoutTrack> viewToTrk = new HashMap<>();
7831
7832    // temporary
7833    final public LayoutTrackView getLayoutTrackView(LayoutTrack trk) {
7834        LayoutTrackView lv = trkToView.get(trk);
7835        if (lv == null) {
7836            log.warn("No View found for {} class {}", trk, trk.getClass());
7837            throw new IllegalArgumentException("No View found: " + trk.getClass());
7838        }
7839        return lv;
7840    }
7841
7842    // temporary
7843    final public LevelXingView getLevelXingView(LevelXing xing) {
7844        LayoutTrackView lv = trkToView.get(xing);
7845        if (lv == null) {
7846            log.warn("No View found for {} class {}", xing, xing.getClass());
7847            throw new IllegalArgumentException("No View found: " + xing.getClass());
7848        }
7849        if (lv instanceof LevelXingView) {
7850            return (LevelXingView) lv;
7851        } else {
7852            log.error("wrong type {} {} found {}", xing, xing.getClass(), lv);
7853        }
7854        throw new IllegalArgumentException("Wrong type: " + xing.getClass());
7855    }
7856
7857    // temporary
7858    final public LayoutTurnoutView getLayoutTurnoutView(LayoutTurnout to) {
7859        LayoutTrackView lv = trkToView.get(to);
7860        if (lv == null) {
7861            log.warn("No View found for {} class {}", to, to.getClass());
7862            throw new IllegalArgumentException("No View found: " + to);
7863        }
7864        if (lv instanceof LayoutTurnoutView) {
7865            return (LayoutTurnoutView) lv;
7866        } else {
7867            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
7868        }
7869        throw new IllegalArgumentException("Wrong type: " + to.getClass());
7870    }
7871
7872    // temporary
7873    final public LayoutTurntableView getLayoutTurntableView(LayoutTurntable to) {
7874        LayoutTrackView lv = trkToView.get(to);
7875        if (lv == null) {
7876            log.warn("No View found for {} class {}", to, to.getClass());
7877            throw new IllegalArgumentException("No matching View found: " + to);
7878        }
7879        if (lv instanceof LayoutTurntableView) {
7880            return (LayoutTurntableView) lv;
7881        } else {
7882            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
7883        }
7884        throw new IllegalArgumentException("Wrong type: " + to.getClass());
7885    }
7886
7887    // temporary
7888    final public LayoutSlipView getLayoutSlipView(LayoutSlip to) {
7889        LayoutTrackView lv = trkToView.get(to);
7890        if (lv == null) {
7891            log.warn("No View found for {} class {}", to, to.getClass());
7892            throw new IllegalArgumentException("No matching View found: " + to);
7893        }
7894        if (lv instanceof LayoutSlipView) {
7895            return (LayoutSlipView) lv;
7896        } else {
7897            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
7898        }
7899        throw new IllegalArgumentException("Wrong type: " + to.getClass());
7900    }
7901
7902    // temporary
7903    final public TrackSegmentView getTrackSegmentView(TrackSegment to) {
7904        LayoutTrackView lv = trkToView.get(to);
7905        if (lv == null) {
7906            log.warn("No View found for {} class {}", to, to.getClass());
7907            throw new IllegalArgumentException("No matching View found: " + to);
7908        }
7909        if (lv instanceof TrackSegmentView) {
7910            return (TrackSegmentView) lv;
7911        } else {
7912            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
7913        }
7914        throw new IllegalArgumentException("Wrong type: " + to.getClass());
7915    }
7916
7917    // temporary
7918    final public PositionablePointView getPositionablePointView(PositionablePoint to) {
7919        LayoutTrackView lv = trkToView.get(to);
7920        if (lv == null) {
7921            log.warn("No View found for {} class {}", to, to.getClass());
7922            throw new IllegalArgumentException("No matching View found: " + to);
7923        }
7924        if (lv instanceof PositionablePointView) {
7925            return (PositionablePointView) lv;
7926        } else {
7927            log.error("wrong type {} {} found {}", to, to.getClass(), lv);
7928        }
7929        throw new IllegalArgumentException("Wrong type: " + to.getClass());
7930    }
7931
7932    /**
7933     * Add a LayoutTrack and LayoutTrackView to the list of LayoutTrack family
7934     * objects.
7935     *
7936     * @param trk the layout track to add.
7937     */
7938    final public void addLayoutTrack(@Nonnull LayoutTrack trk, @Nonnull LayoutTrackView v) {
7939        log.trace("addLayoutTrack {}", trk);
7940        if (layoutTrackList.contains(trk)) {
7941            log.warn("LayoutTrack {} already being maintained", trk.getName());
7942        }
7943
7944        layoutTrackList.add(trk);
7945        layoutTrackViewList.add(v);
7946        trkToView.put(trk, v);
7947        viewToTrk.put(v, trk);
7948
7949        unionToPanelBounds(v.getBounds()); // temporary - this should probably _not_ be in the topological part
7950    }
7951
7952    /**
7953     * If item present, delete from the list of LayoutTracks and force a dirty
7954     * redraw.
7955     *
7956     * @param trk the layout track to remove and redraw.
7957     * @return true is item was deleted and a redraw done.
7958     */
7959    final public boolean removeLayoutTrackAndRedraw(@Nonnull LayoutTrack trk) {
7960        log.trace("removeLayoutTrackAndRedraw {}", trk);
7961        if (layoutTrackList.contains(trk)) {
7962            removeLayoutTrack(trk);
7963            setDirty();
7964            redrawPanel();
7965            log.trace("removeLayoutTrackAndRedraw present {}", trk);
7966            return true;
7967        }
7968        log.trace("removeLayoutTrackAndRedraw absent {}", trk);
7969        return false;
7970    }
7971
7972    /**
7973     * If item present, delete from the list of LayoutTracks and force a dirty
7974     * redraw.
7975     *
7976     * @param trk the layout track to remove.
7977     */
7978    final public void removeLayoutTrack(@Nonnull LayoutTrack trk) {
7979        log.trace("removeLayoutTrack {}", trk);
7980        layoutTrackList.remove(trk);
7981        LayoutTrackView v = trkToView.get(trk);
7982        layoutTrackViewList.remove(v);
7983        trkToView.remove(trk);
7984        viewToTrk.remove(v);
7985    }
7986
7987    /**
7988     * Clear the list of layout tracks. Not intended for general use.
7989     * <p>
7990     */
7991    private void clearLayoutTracks() {
7992        layoutTrackList.clear();
7993        layoutTrackViewList.clear();
7994        trkToView.clear();
7995        viewToTrk.clear();
7996    }
7997
7998    public @Nonnull
7999    List<LayoutShape> getLayoutShapes() {
8000        return layoutShapes;
8001    }
8002
8003    public void sortLayoutShapesByLevel() {
8004        layoutShapes.sort((lhs, rhs) -> {
8005            // -1 == less than, 0 == equal, +1 == greater than
8006            return Integer.signum(lhs.getLevel() - rhs.getLevel());
8007        });
8008    }
8009
8010    /**
8011     * {@inheritDoc}
8012     * <p>
8013     * This implementation is temporary, using the on-screen points from the
8014     * LayoutTrackViews via @{link LayoutEditor#getCoords}.
8015     */
8016    @Override
8017    public int computeDirection(LayoutTrack trk1, HitPointType h1, LayoutTrack trk2, HitPointType h2) {
8018        return Path.computeDirection(
8019                getCoords(trk1, h1),
8020                getCoords(trk2, h2)
8021        );
8022    }
8023
8024    @Override
8025    public int computeDirectionToCenter(@Nonnull LayoutTrack trk1, @Nonnull HitPointType h1, @Nonnull PositionablePoint p) {
8026        return Path.computeDirection(
8027                getCoords(trk1, h1),
8028                getPositionablePointView(p).getCoordsCenter()
8029        );
8030    }
8031
8032    @Override
8033    public int computeDirectionFromCenter(@Nonnull PositionablePoint p, @Nonnull LayoutTrack trk1, @Nonnull HitPointType h1) {
8034        return Path.computeDirection(
8035                getPositionablePointView(p).getCoordsCenter(),
8036                getCoords(trk1, h1)
8037        );
8038    }
8039
8040    @Override
8041    public boolean showAlignPopup(@Nonnull Positionable l) {
8042        return false;
8043    }
8044
8045    @Override
8046    public void showToolTip(
8047            @Nonnull Positionable selection,
8048            @Nonnull MouseEvent event) {
8049        ToolTip tip = selection.getToolTip();
8050        String txt = tip.getText();
8051        if ((txt != null) && !txt.isEmpty()) {
8052            tip.setLocation(selection.getX() + selection.getWidth() / 2, selection.getY() + selection.getHeight());
8053            setToolTip(tip);
8054        }
8055    }
8056
8057    @Override
8058    public void addToPopUpMenu(
8059            @Nonnull NamedBean nb,
8060            @Nonnull JMenuItem item,
8061            int menu) {
8062        if ((nb == null) || (item == null)) {
8063            return;
8064        }
8065
8066        List theList = null;
8067
8068        if (nb instanceof Sensor) {
8069            theList = sensorList;
8070        } else if (nb instanceof SignalHead) {
8071            theList = signalList;
8072        } else if (nb instanceof SignalMast) {
8073            theList = signalMastList;
8074        } else if (nb instanceof Block) {
8075            theList = blockContentsLabelList;
8076        } else if (nb instanceof Memory) {
8077            theList = memoryLabelList;
8078        }
8079        if (theList != null) {
8080            for (Object o : theList) {
8081                PositionableLabel si = (PositionableLabel) o;
8082                if ((si.getNamedBean() == nb) && (si.getPopupUtility() != null)) {
8083                    if (menu != Editor.VIEWPOPUPONLY) {
8084                        si.getPopupUtility().addEditPopUpMenu(item);
8085                    }
8086                    if (menu != Editor.EDITPOPUPONLY) {
8087                        si.getPopupUtility().addViewPopUpMenu(item);
8088                    }
8089                }
8090            }
8091        } else if (nb instanceof Turnout) {
8092            for (LayoutTurnoutView ltv : getLayoutTurnoutAndSlipViews()) {
8093                if (ltv.getTurnout().equals(nb)) {
8094                    if (menu != Editor.VIEWPOPUPONLY) {
8095                        ltv.addEditPopUpMenu(item);
8096                    }
8097                    if (menu != Editor.EDITPOPUPONLY) {
8098                        ltv.addViewPopUpMenu(item);
8099                    }
8100                }
8101            }
8102        }
8103    }
8104
8105    @Override
8106    public @Nonnull
8107    String toString() {
8108        return String.format("LayoutEditor: %s", getLayoutName());
8109    }
8110
8111    @Override
8112    public void vetoableChange(
8113            @Nonnull PropertyChangeEvent evt)
8114            throws PropertyVetoException {
8115        NamedBean nb = (NamedBean) evt.getOldValue();
8116
8117        if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N
8118            StringBuilder message = new StringBuilder();
8119            message.append(Bundle.getMessage("VetoInUseLayoutEditorHeader", toString())); // NOI18N
8120            message.append("<ul>");
8121            boolean found = false;
8122
8123            if (nb instanceof SignalHead) {
8124                if (containsSignalHead((SignalHead) nb)) {
8125                    found = true;
8126                    message.append("<li>");
8127                    message.append(Bundle.getMessage("VetoSignalHeadIconFound"));
8128                    message.append("</li>");
8129                }
8130                LayoutTurnout lt = finder.findLayoutTurnoutByBean(nb);
8131
8132                if (lt != null) {
8133                    message.append("<li>");
8134                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToTurnout", lt.getTurnoutName()));
8135                    message.append("</li>");
8136                }
8137                PositionablePoint p = finder.findPositionablePointByBean(nb);
8138
8139                if (p != null) {
8140                    message.append("<li>");
8141                    // Need to expand to get the names of blocks
8142                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToPoint"));
8143                    message.append("</li>");
8144                }
8145                LevelXing lx = finder.findLevelXingByBean(nb);
8146
8147                if (lx != null) {
8148                    message.append("<li>");
8149                    // Need to expand to get the names of blocks
8150                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToLevelXing"));
8151                    message.append("</li>");
8152                }
8153                LayoutSlip ls = finder.findLayoutSlipByBean(nb);
8154
8155                if (ls != null) {
8156                    message.append("<li>");
8157                    message.append(Bundle.getMessage("VetoSignalHeadAssignedToLayoutSlip", ls.getTurnoutName()));
8158                    message.append("</li>");
8159                }
8160            } else if (nb instanceof Turnout) {
8161                LayoutTurnout lt = finder.findLayoutTurnoutByBean(nb);
8162
8163                if (lt != null) {
8164                    found = true;
8165                    message.append("<li>");
8166                    message.append(Bundle.getMessage("VetoTurnoutIconFound"));
8167                    message.append("</li>");
8168                }
8169
8170                for (LayoutTurnout t : getLayoutTurnouts()) {
8171                    if (t.getLinkedTurnoutName() != null) {
8172                        String uname = nb.getUserName();
8173
8174                        if (nb.getSystemName().equals(t.getLinkedTurnoutName())
8175                                || ((uname != null) && uname.equals(t.getLinkedTurnoutName()))) {
8176                            found = true;
8177                            message.append("<li>");
8178                            message.append(Bundle.getMessage("VetoLinkedTurnout", t.getTurnoutName()));
8179                            message.append("</li>");
8180                        }
8181                    }
8182
8183                    if (nb.equals(t.getSecondTurnout())) {
8184                        found = true;
8185                        message.append("<li>");
8186                        message.append(Bundle.getMessage("VetoSecondTurnout", t.getTurnoutName()));
8187                        message.append("</li>");
8188                    }
8189                }
8190                LayoutSlip ls = finder.findLayoutSlipByBean(nb);
8191
8192                if (ls != null) {
8193                    found = true;
8194                    message.append("<li>");
8195                    message.append(Bundle.getMessage("VetoSlipIconFound", ls.getDisplayName()));
8196                    message.append("</li>");
8197                }
8198
8199                for (LayoutTurntable lx : getLayoutTurntables()) {
8200                    if (lx.isTurnoutControlled()) {
8201                        for (int i = 0; i < lx.getNumberRays(); i++) {
8202                            if (nb.equals(lx.getRayTurnout(i))) {
8203                                found = true;
8204                                message.append("<li>");
8205                                message.append(Bundle.getMessage("VetoRayTurntableControl", lx.getId()));
8206                                message.append("</li>");
8207                                break;
8208                            }
8209                        }
8210                    }
8211                }
8212            }
8213
8214            if (nb instanceof SignalMast) {
8215                if (containsSignalMast((SignalMast) nb)) {
8216                    message.append("<li>");
8217                    message.append("As an Icon");
8218                    message.append("</li>");
8219                    found = true;
8220                }
8221                String foundelsewhere = findBeanUsage(nb);
8222
8223                if (foundelsewhere != null) {
8224                    message.append(foundelsewhere);
8225                    found = true;
8226                }
8227            }
8228
8229            if (nb instanceof Sensor) {
8230                int count = 0;
8231
8232                for (SensorIcon si : sensorList) {
8233                    if (nb.equals(si.getNamedBean())) {
8234                        count++;
8235                        found = true;
8236                    }
8237                }
8238
8239                if (count > 0) {
8240                    message.append("<li>");
8241                    message.append(String.format("As an Icon %s times", count));
8242                    message.append("</li>");
8243                }
8244                String foundelsewhere = findBeanUsage(nb);
8245
8246                if (foundelsewhere != null) {
8247                    message.append(foundelsewhere);
8248                    found = true;
8249                }
8250            }
8251
8252            if (nb instanceof Memory) {
8253                for (MemoryIcon si : memoryLabelList) {
8254                    if (nb.equals(si.getMemory())) {
8255                        found = true;
8256                        message.append("<li>");
8257                        message.append(Bundle.getMessage("VetoMemoryIconFound"));
8258                        message.append("</li>");
8259                    }
8260                }
8261            }
8262
8263            if (found) {
8264                message.append("</ul>");
8265                message.append(Bundle.getMessage("VetoReferencesWillBeRemoved")); // NOI18N
8266                throw new PropertyVetoException(message.toString(), evt);
8267            }
8268        } else if ("DoDelete".equals(evt.getPropertyName())) { // NOI18N
8269            if (nb instanceof SignalHead) {
8270                removeSignalHead((SignalHead) nb);
8271                removeBeanRefs(nb);
8272            }
8273
8274            if (nb instanceof Turnout) {
8275                LayoutTurnout lt = finder.findLayoutTurnoutByBean(nb);
8276
8277                if (lt != null) {
8278                    lt.setTurnout("");
8279                }
8280
8281                for (LayoutTurnout t : getLayoutTurnouts()) {
8282                    if (t.getLinkedTurnoutName() != null) {
8283                        if (t.getLinkedTurnoutName().equals(nb.getSystemName())
8284                                || ((nb.getUserName() != null) && t.getLinkedTurnoutName().equals(nb.getUserName()))) {
8285                            t.setLinkedTurnoutName("");
8286                        }
8287                    }
8288
8289                    if (nb.equals(t.getSecondTurnout())) {
8290                        t.setSecondTurnout("");
8291                    }
8292                }
8293
8294                for (LayoutSlip sl : getLayoutSlips()) {
8295                    if (nb.equals(sl.getTurnout())) {
8296                        sl.setTurnout("");
8297                    }
8298
8299                    if (nb.equals(sl.getTurnoutB())) {
8300                        sl.setTurnoutB("");
8301                    }
8302                }
8303
8304                for (LayoutTurntable lx : getLayoutTurntables()) {
8305                    if (lx.isTurnoutControlled()) {
8306                        for (int i = 0; i < lx.getNumberRays(); i++) {
8307                            if (nb.equals(lx.getRayTurnout(i))) {
8308                                lx.setRayTurnout(i, null, NamedBean.UNKNOWN);
8309                            }
8310                        }
8311                    }
8312                }
8313            }
8314
8315            if (nb instanceof SignalMast) {
8316                removeBeanRefs(nb);
8317
8318                if (containsSignalMast((SignalMast) nb)) {
8319                    Iterator<SignalMastIcon> icon = signalMastList.iterator();
8320
8321                    while (icon.hasNext()) {
8322                        SignalMastIcon i = icon.next();
8323
8324                        if (i.getSignalMast().equals(nb)) {
8325                            icon.remove();
8326                            super.removeFromContents(i);
8327                        }
8328                    }
8329                    setDirty();
8330                    redrawPanel();
8331                }
8332            }
8333
8334            if (nb instanceof Sensor) {
8335                removeBeanRefs(nb);
8336                Iterator<SensorIcon> icon = sensorImage.iterator();
8337
8338                while (icon.hasNext()) {
8339                    SensorIcon i = icon.next();
8340
8341                    if (nb.equals(i.getSensor())) {
8342                        icon.remove();
8343                        super.removeFromContents(i);
8344                    }
8345                }
8346                setDirty();
8347                redrawPanel();
8348            }
8349
8350            if (nb instanceof Memory) {
8351                Iterator<MemoryIcon> icon = memoryLabelList.iterator();
8352
8353                while (icon.hasNext()) {
8354                    MemoryIcon i = icon.next();
8355
8356                    if (nb.equals(i.getMemory())) {
8357                        icon.remove();
8358                        super.removeFromContents(i);
8359                    }
8360                }
8361            }
8362        }
8363    }
8364
8365//    private void rename(String inFrom, String inTo) {
8366//
8367//    }
8368    @Override
8369    public void dispose() {
8370        if (leToolBarPanel.sensorFrame != null) {
8371            leToolBarPanel.sensorFrame.dispose();
8372            leToolBarPanel.sensorFrame = null;
8373        }
8374        if (leToolBarPanel.signalFrame != null) {
8375            leToolBarPanel.signalFrame.dispose();
8376            leToolBarPanel.signalFrame = null;
8377        }
8378        if (leToolBarPanel.iconFrame != null) {
8379            leToolBarPanel.iconFrame.dispose();
8380            leToolBarPanel.iconFrame = null;
8381        }
8382        super.dispose();
8383
8384    }
8385
8386    // package protected
8387    class TurnoutComboBoxPopupMenuListener implements PopupMenuListener {
8388
8389        private final NamedBeanComboBox<Turnout> comboBox;
8390        private final List<Turnout> currentTurnouts;
8391
8392        public TurnoutComboBoxPopupMenuListener(NamedBeanComboBox<Turnout> comboBox, List<Turnout> currentTurnouts) {
8393            this.comboBox = comboBox;
8394            this.currentTurnouts = currentTurnouts;
8395        }
8396
8397        @Override
8398        public void popupMenuWillBecomeVisible(PopupMenuEvent event) {
8399            // This method is called before the popup menu becomes visible.
8400            log.debug("PopupMenuWillBecomeVisible");
8401            Set<Turnout> l = new HashSet<>();
8402            comboBox.getManager().getNamedBeanSet().forEach((turnout) -> {
8403                if (!currentTurnouts.contains(turnout)) {
8404                    if (!validatePhysicalTurnout(turnout.getDisplayName(), null)) {
8405                        l.add(turnout);
8406                    }
8407                }
8408            });
8409            comboBox.setExcludedItems(l);
8410        }
8411
8412        @Override
8413        public void popupMenuWillBecomeInvisible(PopupMenuEvent event) {
8414            // This method is called before the popup menu becomes invisible
8415            log.debug("PopupMenuWillBecomeInvisible");
8416        }
8417
8418        @Override
8419        public void popupMenuCanceled(PopupMenuEvent event) {
8420            // This method is called when the popup menu is canceled
8421            log.debug("PopupMenuCanceled");
8422        }
8423    }
8424
8425    /**
8426     * Create a listener that will exclude turnouts that are present in the
8427     * current panel.
8428     *
8429     * @param comboBox The NamedBeanComboBox that contains the turnout list.
8430     * @return A PopupMenuListener
8431     */
8432    public TurnoutComboBoxPopupMenuListener newTurnoutComboBoxPopupMenuListener(NamedBeanComboBox<Turnout> comboBox) {
8433        return new TurnoutComboBoxPopupMenuListener(comboBox, new ArrayList<>());
8434    }
8435
8436    /**
8437     * Create a listener that will exclude turnouts that are present in the
8438     * current panel. The list of current turnouts are not excluded.
8439     *
8440     * @param comboBox        The NamedBeanComboBox that contains the turnout
8441     *                        list.
8442     * @param currentTurnouts The turnouts to be left in the turnout list.
8443     * @return A PopupMenuListener
8444     */
8445    public TurnoutComboBoxPopupMenuListener newTurnoutComboBoxPopupMenuListener(NamedBeanComboBox<Turnout> comboBox, List<Turnout> currentTurnouts) {
8446        return new TurnoutComboBoxPopupMenuListener(comboBox, currentTurnouts);
8447    }
8448
8449    List<NamedBeanUsageReport> usageReport;
8450
8451    @Override
8452    public List<NamedBeanUsageReport> getUsageReport(NamedBean bean) {
8453        usageReport = new ArrayList<>();
8454        if (bean != null) {
8455            usageReport = super.getUsageReport(bean);
8456
8457            // LE Specific checks
8458            // Turnouts
8459            findTurnoutUsage(bean);
8460
8461            // Check A, EB, EC for sensors, masts, heads
8462            findPositionalUsage(bean);
8463
8464            // Level Crossings
8465            findXingWhereUsed(bean);
8466
8467            // Track segments
8468            findSegmentWhereUsed(bean);
8469        }
8470        return usageReport;
8471    }
8472
8473    void findTurnoutUsage(NamedBean bean) {
8474        for (LayoutTurnout turnout : getLayoutTurnoutsAndSlips()) {
8475            String data = getUsageData(turnout);
8476
8477            if (bean.equals(turnout.getTurnout())) {
8478                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnout", data));
8479            }
8480            if (bean.equals(turnout.getSecondTurnout())) {
8481                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnout2", data));
8482            }
8483
8484            if (isLBLockUsed(bean, turnout.getLayoutBlock())) {
8485                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
8486            }
8487            if (turnout.hasEnteringDoubleTrack()) {
8488                if (isLBLockUsed(bean, turnout.getLayoutBlockB())) {
8489                    usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
8490                }
8491                if (isLBLockUsed(bean, turnout.getLayoutBlockC())) {
8492                    usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
8493                }
8494                if (isLBLockUsed(bean, turnout.getLayoutBlockD())) {
8495                    usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutBlock", data));
8496                }
8497            }
8498
8499            if (bean.equals(turnout.getSensorA())) {
8500                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
8501            }
8502            if (bean.equals(turnout.getSensorB())) {
8503                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
8504            }
8505            if (bean.equals(turnout.getSensorC())) {
8506                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
8507            }
8508            if (bean.equals(turnout.getSensorD())) {
8509                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSensor", data));
8510            }
8511
8512            if (bean.equals(turnout.getSignalAMast())) {
8513                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
8514            }
8515            if (bean.equals(turnout.getSignalBMast())) {
8516                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
8517            }
8518            if (bean.equals(turnout.getSignalCMast())) {
8519                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
8520            }
8521            if (bean.equals(turnout.getSignalDMast())) {
8522                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalMast", data));
8523            }
8524
8525            if (bean.equals(turnout.getSignalA1())) {
8526                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
8527            }
8528            if (bean.equals(turnout.getSignalA2())) {
8529                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
8530            }
8531            if (bean.equals(turnout.getSignalA3())) {
8532                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
8533            }
8534            if (bean.equals(turnout.getSignalB1())) {
8535                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
8536            }
8537            if (bean.equals(turnout.getSignalB2())) {
8538                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
8539            }
8540            if (bean.equals(turnout.getSignalC1())) {
8541                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
8542            }
8543            if (bean.equals(turnout.getSignalC2())) {
8544                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
8545            }
8546            if (bean.equals(turnout.getSignalD1())) {
8547                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
8548            }
8549            if (bean.equals(turnout.getSignalD2())) {
8550                usageReport.add(new NamedBeanUsageReport("LayoutEditorTurnoutSignalHead", data));
8551            }
8552        }
8553    }
8554
8555    void findPositionalUsage(NamedBean bean) {
8556        for (PositionablePoint point : getPositionablePoints()) {
8557            String data = getUsageData(point);
8558            if (bean.equals(point.getEastBoundSensor())) {
8559                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSensor", data));
8560            }
8561            if (bean.equals(point.getWestBoundSensor())) {
8562                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSensor", data));
8563            }
8564            if (bean.equals(point.getEastBoundSignalHead())) {
8565                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalHead", data));
8566            }
8567            if (bean.equals(point.getWestBoundSignalHead())) {
8568                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalHead", data));
8569            }
8570            if (bean.equals(point.getEastBoundSignalMast())) {
8571                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalMast", data));
8572            }
8573            if (bean.equals(point.getWestBoundSignalMast())) {
8574                usageReport.add(new NamedBeanUsageReport("LayoutEditorPointSignalMast", data));
8575            }
8576        }
8577    }
8578
8579    void findSegmentWhereUsed(NamedBean bean) {
8580        for (TrackSegment segment : getTrackSegments()) {
8581            if (isLBLockUsed(bean, segment.getLayoutBlock())) {
8582                String data = getUsageData(segment);
8583                usageReport.add(new NamedBeanUsageReport("LayoutEditorSegmentBlock", data));
8584            }
8585        }
8586    }
8587
8588    void findXingWhereUsed(NamedBean bean) {
8589        for (LevelXing xing : getLevelXings()) {
8590            String data = getUsageData(xing);
8591            if (isLBLockUsed(bean, xing.getLayoutBlockAC())) {
8592                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingBlock", data));
8593            }
8594            if (isLBLockUsed(bean, xing.getLayoutBlockBD())) {
8595                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingBlock", data));
8596            }
8597            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTA)) {
8598                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
8599            }
8600            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTB)) {
8601                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
8602            }
8603            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTC)) {
8604                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
8605            }
8606            if (isUsedInXing(bean, xing, LevelXing.Geometry.POINTD)) {
8607                usageReport.add(new NamedBeanUsageReport("LayoutEditorXingOther", data));
8608            }
8609        }
8610    }
8611
8612    String getUsageData(LayoutTrack track) {
8613        LayoutTrackView trackView = getLayoutTrackView(track);
8614        Point2D point = trackView.getCoordsCenter();
8615        if (trackView instanceof TrackSegmentView) {
8616            TrackSegmentView segmentView = (TrackSegmentView) trackView;
8617            point = new Point2D.Double(segmentView.getCentreSegX(), segmentView.getCentreSegY());
8618        }
8619        return String.format("%s :: x=%d, y=%d",
8620                track.getClass().getSimpleName(),
8621                Math.round(point.getX()),
8622                Math.round(point.getY()));
8623    }
8624
8625    boolean isLBLockUsed(NamedBean bean, LayoutBlock lblock) {
8626        boolean result = false;
8627        if (lblock != null) {
8628            if (bean.equals(lblock.getBlock())) {
8629                result = true;
8630            }
8631        }
8632        return result;
8633    }
8634
8635    boolean isUsedInXing(NamedBean bean, LevelXing xing, LevelXing.Geometry point) {
8636        boolean result = false;
8637        if (bean.equals(xing.getSensor(point))) {
8638            result = true;
8639        }
8640        if (bean.equals(xing.getSignalHead(point))) {
8641            result = true;
8642        }
8643        if (bean.equals(xing.getSignalMast(point))) {
8644            result = true;
8645        }
8646        return result;
8647    }
8648
8649    // initialize logging
8650    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LayoutEditor.class);
8651}