001package jmri.jmrit.display;
002
003import java.awt.event.ActionEvent;
004import java.awt.event.ActionListener;
005import java.util.HashMap;
006import java.util.Hashtable;
007import java.util.Map.Entry;
008
009import javax.annotation.Nonnull;
010import javax.swing.JCheckBoxMenuItem;
011import javax.swing.JPopupMenu;
012
013import jmri.InstanceManager;
014import jmri.NamedBeanHandle;
015import jmri.Turnout;
016import jmri.NamedBean.DisplayOptions;
017import jmri.jmrit.catalog.NamedIcon;
018import jmri.jmrit.display.palette.TableItemPanel;
019import jmri.jmrit.picker.PickListModel;
020import jmri.util.swing.JmriMouseEvent;
021
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025/**
026 * An icon to display a status of a turnout.
027 * <p>
028 * This responds to only KnownState, leaving CommandedState to some other
029 * graphic representation later.
030 * <p>
031 * A click on the icon will command a state change. Specifically, it will set
032 * the CommandedState to the opposite (THROWN vs CLOSED) of the current
033 * KnownState.
034 * <p>
035 * The default icons are for a left-handed turnout, facing point for east-bound
036 * traffic.
037 *
038 * @author Bob Jacobsen Copyright (c) 2002
039 * @author PeteCressman Copyright (C) 2010, 2011
040 */
041public class TurnoutIcon extends PositionableIcon implements java.beans.PropertyChangeListener {
042
043    protected HashMap<Integer, NamedIcon> _iconStateMap;     // state int to icon
044    protected HashMap<String, Integer> _name2stateMap;       // name to state
045    protected HashMap<Integer, String> _state2nameMap;       // state to name
046
047    public TurnoutIcon(Editor editor) {
048        // super ctor call to make sure this is an icon label
049        super(new NamedIcon("resources/icons/smallschematics/tracksegments/os-lefthand-east-closed.gif",
050                "resources/icons/smallschematics/tracksegments/os-lefthand-east-closed.gif"), editor);
051        _control = true;
052        setPopupUtility(null);
053    }
054
055    @Override
056    public Positionable deepClone() {
057        TurnoutIcon pos = new TurnoutIcon(_editor);
058        return finishClone(pos);
059    }
060
061    protected Positionable finishClone(TurnoutIcon pos) {
062        pos.setTurnout(getNamedTurnout().getName());
063        pos._iconStateMap = cloneMap(_iconStateMap, pos);
064        pos.setTristate(getTristate());
065        pos.setMomentary(getMomentary());
066        pos.setDirectControl(getDirectControl());
067        pos._iconFamily = _iconFamily;
068        return super.finishClone(pos);
069    }
070
071    // the associated Turnout object
072    //Turnout turnout = null;
073    private NamedBeanHandle<Turnout> namedTurnout = null;
074
075    /**
076     * Attach a named turnout to this display item.
077     *
078     * @param pName Used as a system/user name to lookup the turnout object
079     */
080    public void setTurnout(String pName) {
081        if (InstanceManager.getNullableDefault(jmri.TurnoutManager.class) != null) {
082            try {
083                Turnout turnout = InstanceManager.turnoutManagerInstance().provideTurnout(pName);
084                setTurnout(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, turnout));
085            } catch (IllegalArgumentException ex) {
086                log.error("Turnout '{}' not available, icon won't see changes", pName);
087            }
088        } else {
089            log.error("No TurnoutManager for this protocol, icon won't see changes");
090        }
091    }
092
093    public void setTurnout(NamedBeanHandle<Turnout> to) {
094        if (namedTurnout != null) {
095            getTurnout().removePropertyChangeListener(this);
096        }
097        namedTurnout = to;
098        if (namedTurnout != null) {
099            _iconStateMap = new HashMap<>();
100            _name2stateMap = new HashMap<>();
101            _name2stateMap.put("BeanStateUnknown", Turnout.UNKNOWN);
102            _name2stateMap.put("BeanStateInconsistent", Turnout.INCONSISTENT);
103            _name2stateMap.put("TurnoutStateClosed", Turnout.CLOSED);
104            _name2stateMap.put("TurnoutStateThrown", Turnout.THROWN);
105            _state2nameMap = new HashMap<>();
106            _state2nameMap.put(Turnout.UNKNOWN, "BeanStateUnknown");
107            _state2nameMap.put(Turnout.INCONSISTENT, "BeanStateInconsistent");
108            _state2nameMap.put(Turnout.CLOSED, "TurnoutStateClosed");
109            _state2nameMap.put(Turnout.THROWN, "TurnoutStateThrown");
110            displayState(turnoutState());
111            getTurnout().addPropertyChangeListener(this, namedTurnout.getName(), "Panel Editor Turnout Icon");
112        }
113    }
114
115    public Turnout getTurnout() {
116        return namedTurnout.getBean();
117    }
118
119    public NamedBeanHandle<Turnout> getNamedTurnout() {
120        return namedTurnout;
121    }
122
123    @Override
124    public jmri.NamedBean getNamedBean() {
125        return getTurnout();
126    }
127
128    /**
129     * Place icon by its localized bean state name.
130     *
131     * @param name the state name
132     * @param icon the icon to place
133     */
134    public void setIcon(String name, NamedIcon icon) {
135        if (log.isDebugEnabled()) {
136            log.debug("setIcon for name \"{}\" state= {}", name, _name2stateMap.get(name));
137        }
138        _iconStateMap.put(_name2stateMap.get(name), icon);
139        displayState(turnoutState());
140    }
141
142    /**
143     * Get icon by its localized bean state name.
144     */
145    @Override
146    public NamedIcon getIcon(String state) {
147        return _iconStateMap.get(_name2stateMap.get(state));
148    }
149
150    public NamedIcon getIcon(int state) {
151        return _iconStateMap.get(state);
152    }
153
154    @Override
155    public int maxHeight() {
156        int max = 0;
157        for (NamedIcon namedIcon : _iconStateMap.values()) {
158            max = Math.max(namedIcon.getIconHeight(), max);
159        }
160        return max;
161    }
162
163    @Override
164    public int maxWidth() {
165        int max = 0;
166        for (NamedIcon namedIcon : _iconStateMap.values()) {
167            max = Math.max(namedIcon.getIconWidth(), max);
168        }
169        return max;
170    }
171
172    /**
173     * Get current state of attached turnout
174     *
175     * @return A state variable from a Turnout, e.g. Turnout.CLOSED
176     */
177    int turnoutState() {
178        if (namedTurnout != null) {
179            return getTurnout().getKnownState();
180        } else {
181            return Turnout.UNKNOWN;
182        }
183    }
184
185    // update icon as state of turnout changes
186    @Override
187    public void propertyChange(java.beans.PropertyChangeEvent e) {
188        if (log.isDebugEnabled()) {
189            log.debug("property change: {} {} is now {}", getNameString(), e.getPropertyName(), e.getNewValue());
190        }
191
192        // when there's feedback, transition through inconsistent icon for better
193        // animation
194        if (getTristate()
195                && (getTurnout().getFeedbackMode() != Turnout.DIRECT)
196                && (e.getPropertyName().equals("CommandedState"))) {
197            if (getTurnout().getCommandedState() != getTurnout().getKnownState()) {
198                int now = Turnout.INCONSISTENT;
199                displayState(now);
200            }
201            // this takes care of the quick double click
202            if (getTurnout().getCommandedState() == getTurnout().getKnownState()) {
203                int now = (Integer) e.getNewValue();
204                displayState(now);
205            }
206        }
207
208        if (e.getPropertyName().equals("KnownState")) {
209            int now = (Integer) e.getNewValue();
210            displayState(now);
211        }
212    }
213
214    public String getStateName(int state) {
215        return _state2nameMap.get(state);
216
217    }
218
219    @Override
220    @Nonnull
221    public String getTypeString() {
222        return Bundle.getMessage("PositionableType_TurnoutIcon");
223    }
224
225    @Override
226    public String getNameString() {
227        String name;
228        if (namedTurnout == null) {
229            name = Bundle.getMessage("NotConnected");
230        } else {
231            name = getTurnout().getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME);
232        }
233        return name;
234    }
235
236    public void setTristate(boolean set) {
237        tristate = set;
238    }
239
240    public boolean getTristate() {
241        return tristate;
242    }
243    private boolean tristate = false;
244
245    boolean momentary = false;
246
247    public boolean getMomentary() {
248        return momentary;
249    }
250
251    public void setMomentary(boolean m) {
252        momentary = m;
253    }
254
255    boolean directControl = false;
256
257    public boolean getDirectControl() {
258        return directControl;
259    }
260
261    public void setDirectControl(boolean m) {
262        directControl = m;
263    }
264
265    JCheckBoxMenuItem momentaryItem = new JCheckBoxMenuItem(Bundle.getMessage("Momentary"));
266    JCheckBoxMenuItem directControlItem = new JCheckBoxMenuItem(Bundle.getMessage("DirectControl"));
267
268    /**
269     * Pop-up displays unique attributes of turnouts
270     */
271    @Override
272    public boolean showPopUp(JPopupMenu popup) {
273        if (isEditable()) {
274            // add tristate option if turnout has feedback
275            if (namedTurnout != null && getTurnout().getFeedbackMode() != Turnout.DIRECT) {
276                addTristateEntry(popup);
277            }
278
279            popup.add(momentaryItem);
280            momentaryItem.setSelected(getMomentary());
281            momentaryItem.addActionListener(e -> setMomentary(momentaryItem.isSelected()));
282
283            popup.add(directControlItem);
284            directControlItem.setSelected(getDirectControl());
285            directControlItem.addActionListener(e -> setDirectControl(directControlItem.isSelected()));
286        } else if (getDirectControl()) {
287            getTurnout().setCommandedState(jmri.Turnout.THROWN);
288        }
289        return true;
290    }
291
292    javax.swing.JCheckBoxMenuItem tristateItem = null;
293
294    void addTristateEntry(JPopupMenu popup) {
295        tristateItem = new javax.swing.JCheckBoxMenuItem(Bundle.getMessage("Tristate"));
296        tristateItem.setSelected(getTristate());
297        popup.add(tristateItem);
298        tristateItem.addActionListener(e -> setTristate(tristateItem.isSelected()));
299    }
300
301    /**
302     * ****** popup AbstractAction method overrides ********
303     */
304    @Override
305    protected void rotateOrthogonal() {
306        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
307            entry.getValue().setRotation(entry.getValue().getRotation() + 1, this);
308        }
309        displayState(turnoutState());
310        // bug fix, must repaint icons that have same width and height
311        repaint();
312    }
313
314    @Override
315    public void setScale(double s) {
316        _scale = s;
317        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
318            entry.getValue().scale(s, this);
319        }
320        displayState(turnoutState());
321    }
322
323    @Override
324    public void rotate(int deg) {
325        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
326            entry.getValue().rotate(deg, this);
327        }
328        setDegrees(deg);
329        displayState(turnoutState());
330    }
331
332    /**
333     * Drive the current state of the display from the state of the turnout.
334     */
335    @Override
336    public void displayState(int state) {
337        if (getNamedTurnout() == null) {
338            log.debug("Display state {}, disconnected", state);
339        } else {
340            // log.debug("{} displayState {}", getNameString(), _state2nameMap.get(state));
341            if (isText()) {
342                super.setText(_state2nameMap.get(state));
343            }
344            if (isIcon()) {
345                NamedIcon icon = getIcon(state);
346                if (icon != null) {
347                    super.setIcon(icon);
348                }
349            }
350        }
351        updateSize();
352    }
353
354    TableItemPanel<Turnout> _itemPanel;
355
356    @Override
357    public boolean setEditItemMenu(JPopupMenu popup) {
358        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout"));
359        popup.add(new javax.swing.AbstractAction(txt) {
360            @Override
361            public void actionPerformed(ActionEvent e) {
362                editItem();
363            }
364        });
365        return true;
366    }
367
368    protected void editItem() {
369        _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"),
370                Bundle.getMessage("BeanNameTurnout")));
371        _itemPanel = new TableItemPanel<>(_paletteFrame, "Turnout", _iconFamily,
372                PickListModel.turnoutPickModelInstance()); // NOI18N
373        ActionListener updateAction = a -> updateItem();
374        // duplicate icon map with state names rather than int states and unscaled and unrotated
375        HashMap<String, NamedIcon> strMap = new HashMap<>();
376        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
377            NamedIcon oldIcon = entry.getValue();
378            NamedIcon newIcon = cloneIcon(oldIcon, this);
379            newIcon.rotate(0, this);
380            newIcon.scale(1.0, this);
381            newIcon.setRotation(4, this);
382            strMap.put(_state2nameMap.get(entry.getKey()), newIcon);
383        }
384        _itemPanel.init(updateAction, strMap);
385        _itemPanel.setSelection(getTurnout());
386        initPaletteFrame(_paletteFrame, _itemPanel);
387    }
388
389    void updateItem() {
390        HashMap<Integer, NamedIcon> oldMap = cloneMap(_iconStateMap, this);
391        setTurnout(_itemPanel.getTableSelection().getSystemName());
392        _iconFamily = _itemPanel.getFamilyName();
393        HashMap<String, NamedIcon> iconMap = _itemPanel.getIconMap();
394        if (iconMap != null) {
395            for (Entry<String, NamedIcon> entry : iconMap.entrySet()) {
396                if (log.isDebugEnabled()) {
397                    log.debug("key= {}", entry.getKey());
398                }
399                NamedIcon newIcon = entry.getValue();
400                NamedIcon oldIcon = oldMap.get(_name2stateMap.get(entry.getKey()));
401                newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
402                newIcon.setRotation(oldIcon.getRotation(), this);
403                setIcon(entry.getKey(), newIcon);
404            }
405        }   // otherwise retain current map
406        finishItemUpdate(_paletteFrame, _itemPanel);
407    }
408
409    @Override
410    public boolean setEditIconMenu(JPopupMenu popup) {
411        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout"));
412        popup.add(new javax.swing.AbstractAction(txt) {
413            @Override
414            public void actionPerformed(ActionEvent e) {
415                edit();
416            }
417        });
418        return true;
419    }
420
421    @Override
422    protected void edit() {
423        makeIconEditorFrame(this, "Turnout", true, null); // NOI18N
424        _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.turnoutPickModelInstance());
425        int i = 0;
426        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
427            _iconEditor.setIcon(i++, _state2nameMap.get(entry.getKey()), entry.getValue());
428        }
429        _iconEditor.makeIconPanel(false);
430
431        // set default icons, then override with this turnout's icons
432        ActionListener addIconAction = a -> updateTurnout();
433        _iconEditor.complete(addIconAction, true, true, true);
434        _iconEditor.setSelection(getTurnout());
435    }
436
437    void updateTurnout() {
438        HashMap<Integer, NamedIcon> oldMap = cloneMap(_iconStateMap, this);
439        setTurnout(_iconEditor.getTableSelection().getDisplayName());
440        Hashtable<String, NamedIcon> iconMap = _iconEditor.getIconMap();
441
442        for (Entry<String, NamedIcon> entry : iconMap.entrySet()) {
443            if (log.isDebugEnabled()) {
444                log.debug("key= {}", entry.getKey());
445            }
446            NamedIcon newIcon = entry.getValue();
447            NamedIcon oldIcon = oldMap.get(_name2stateMap.get(entry.getKey()));
448            newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
449            newIcon.setRotation(oldIcon.getRotation(), this);
450            setIcon(entry.getKey(), newIcon);
451        }
452        _iconEditorFrame.dispose();
453        _iconEditorFrame = null;
454        _iconEditor = null;
455        invalidate();
456    }
457
458    public boolean buttonLive() {
459        if (namedTurnout == null) {
460            log.error("No turnout connection, can't process click");
461            return false;
462        }
463        return true;
464    }
465
466    @Override
467    public void doMousePressed(JmriMouseEvent e) {
468        if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) {
469            // this is a momentary button press
470            getTurnout().setCommandedState(jmri.Turnout.THROWN);
471        }
472        super.doMousePressed(e);
473    }
474
475    @Override
476    public void doMouseReleased(JmriMouseEvent e) {
477        if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) {
478            // this is a momentary button release
479            getTurnout().setCommandedState(jmri.Turnout.CLOSED);
480        }
481        super.doMouseReleased(e);
482    }
483
484    @Override
485    public void doMouseClicked(JmriMouseEvent e) {
486        if (!_editor.getFlag(Editor.OPTION_CONTROLS, isControlling())) {
487            return;
488        }
489        if (e.isMetaDown() || e.isAltDown() || !buttonLive() || getMomentary()) {
490            return;
491        }
492
493        if (getDirectControl() && !isEditable()) {
494            getTurnout().setCommandedState(jmri.Turnout.CLOSED);
495        } else {
496            alternateOnClick();
497        }
498    }
499
500    void alternateOnClick() {
501        if (getTurnout().getKnownState() == jmri.Turnout.CLOSED) {  // if clear known state, set to opposite
502            getTurnout().setCommandedState(jmri.Turnout.THROWN);
503        } else if (getTurnout().getKnownState() == jmri.Turnout.THROWN) {
504            getTurnout().setCommandedState(jmri.Turnout.CLOSED);
505        } else if (getTurnout().getCommandedState() == jmri.Turnout.CLOSED) {
506            getTurnout().setCommandedState(jmri.Turnout.THROWN);  // otherwise, set to opposite of current commanded state if known
507        } else {
508            getTurnout().setCommandedState(jmri.Turnout.CLOSED);  // just force closed.
509        }
510    }
511
512    @Override
513    public void dispose() {
514        if (namedTurnout != null) {
515            getTurnout().removePropertyChangeListener(this);
516        }
517        namedTurnout = null;
518        _iconStateMap = null;
519        _name2stateMap = null;
520        _state2nameMap = null;
521
522        super.dispose();
523    }
524
525    protected HashMap<Integer, NamedIcon> cloneMap(HashMap<Integer, NamedIcon> map,
526            TurnoutIcon pos) {
527        HashMap<Integer, NamedIcon> clone = new HashMap<>();
528        if (map != null) {
529            for (Entry<Integer, NamedIcon> entry : map.entrySet()) {
530                clone.put(entry.getKey(), cloneIcon(entry.getValue(), pos));
531                if (pos != null) {
532                    pos.setIcon(_state2nameMap.get(entry.getKey()), _iconStateMap.get(entry.getKey()));
533                }
534            }
535        }
536        return clone;
537    }
538
539    private final static Logger log = LoggerFactory.getLogger(TurnoutIcon.class);
540}