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