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    @Override
053    public Positionable deepClone() {
054        TurnoutIcon pos = new TurnoutIcon(_editor);
055        return finishClone(pos);
056    }
057
058    protected Positionable finishClone(TurnoutIcon pos) {
059        pos.setTurnout(getNamedTurnout().getName());
060        pos._iconStateMap = cloneMap(_iconStateMap, pos);
061        pos.setTristate(getTristate());
062        pos.setMomentary(getMomentary());
063        pos.setDirectControl(getDirectControl());
064        pos._iconFamily = _iconFamily;
065        return super.finishClone(pos);
066    }
067
068    // the associated Turnout object
069    private NamedBeanHandle<Turnout> namedTurnout = null;
070
071    /**
072     * Attach a named turnout to this display item.
073     *
074     * @param pName Used as a system/user name to lookup the turnout object
075     */
076    public void setTurnout(String pName) {
077        if (InstanceManager.getNullableDefault(jmri.TurnoutManager.class) != null) {
078            try {
079                Turnout turnout = InstanceManager.turnoutManagerInstance().provideTurnout(pName);
080                setTurnout(InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, turnout));
081            } catch (IllegalArgumentException ex) {
082                log.error("Turnout '{}' not available, icon won't see changes", pName);
083            }
084        } else {
085            log.error("No TurnoutManager for this protocol, icon won't see changes");
086        }
087    }
088
089    public void setTurnout(NamedBeanHandle<Turnout> to) {
090        if (namedTurnout != null) {
091            getTurnout().removePropertyChangeListener(this);
092        }
093        namedTurnout = to;
094        if (namedTurnout != null) {
095            _iconStateMap = new HashMap<>();
096            _name2stateMap = new HashMap<>();
097            _name2stateMap.put("BeanStateUnknown", Turnout.UNKNOWN);
098            _name2stateMap.put("BeanStateInconsistent", Turnout.INCONSISTENT);
099            _name2stateMap.put("TurnoutStateClosed", Turnout.CLOSED);
100            _name2stateMap.put("TurnoutStateThrown", Turnout.THROWN);
101            _state2nameMap = new HashMap<>();
102            _state2nameMap.put(Turnout.UNKNOWN, "BeanStateUnknown");
103            _state2nameMap.put(Turnout.INCONSISTENT, "BeanStateInconsistent");
104            _state2nameMap.put(Turnout.CLOSED, "TurnoutStateClosed");
105            _state2nameMap.put(Turnout.THROWN, "TurnoutStateThrown");
106            displayState(turnoutState());
107            getTurnout().addPropertyChangeListener(this, namedTurnout.getName(), "Panel Editor Turnout Icon");
108        }
109    }
110
111    public Turnout getTurnout() {
112        return namedTurnout.getBean();
113    }
114
115    public NamedBeanHandle<Turnout> getNamedTurnout() {
116        return namedTurnout;
117    }
118
119    @Override
120    public jmri.NamedBean getNamedBean() {
121        return getTurnout();
122    }
123
124    /**
125     * Place icon by its localized bean state name.
126     *
127     * @param name the state name
128     * @param icon the icon to place
129     */
130    public void setIcon(String name, NamedIcon icon) {
131        if (log.isDebugEnabled()) {
132            log.debug("setIcon for name \"{}\" state= {}", name, _name2stateMap.get(name));
133        }
134        _iconStateMap.put(_name2stateMap.get(name), icon);
135        displayState(turnoutState());
136    }
137
138    /**
139     * Get icon by its localized bean state name.
140     */
141    @Override
142    public NamedIcon getIcon(String state) {
143        return _iconStateMap.get(_name2stateMap.get(state));
144    }
145
146    public NamedIcon getIcon(int state) {
147        return _iconStateMap.get(state);
148    }
149
150    @Override
151    public int maxHeight() {
152        int max = 0;
153        if (_iconStateMap != null) {
154            for (NamedIcon namedIcon : _iconStateMap.values()) {
155                max = Math.max(namedIcon.getIconHeight(), max);
156            }
157        }
158        return max;
159    }
160
161    @Override
162    public int maxWidth() {
163        int max = 0;
164        if ( _iconStateMap != null ) {
165            for (NamedIcon namedIcon : _iconStateMap.values()) {
166                max = Math.max(namedIcon.getIconWidth(), max);
167            }
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(Turnout.PROPERTY_COMMANDED_STATE))) {
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(Turnout.PROPERTY_KNOWN_STATE)) {
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    private 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    private 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    private final JCheckBoxMenuItem momentaryItem = new JCheckBoxMenuItem(Bundle.getMessage("Momentary"));
266    private final 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(Turnout.THROWN);
288        }
289        return true;
290    }
291
292    private 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     * {@inheritDoc}
335     */
336    @Override
337    public void displayState(int state) {
338        if (getNamedTurnout() == null) {
339            log.debug("Display state {}, disconnected", state);
340        } else {
341            // log.debug("{} displayState {}", getNameString(), _state2nameMap.get(state));
342            if (isText()) {
343                super.setText(_state2nameMap.get(state));
344            }
345            if (isIcon()) {
346                NamedIcon icon = getIcon(state);
347                if (icon != null) {
348                    super.setIcon(icon);
349                }
350            }
351        }
352        updateSize();
353    }
354
355    TableItemPanel<Turnout> _itemPanel;
356
357    @Override
358    public boolean setEditItemMenu(JPopupMenu popup) {
359        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout"));
360        popup.add(new javax.swing.AbstractAction(txt) {
361            @Override
362            public void actionPerformed(ActionEvent e) {
363                editItem();
364            }
365        });
366        return true;
367    }
368
369    protected void editItem() {
370        _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"),
371                Bundle.getMessage("BeanNameTurnout")));
372        _itemPanel = new TableItemPanel<>(_paletteFrame, "Turnout", _iconFamily,
373                PickListModel.turnoutPickModelInstance()); // NOI18N
374        ActionListener updateAction = a -> updateItem();
375        // duplicate icon map with state names rather than int states and unscaled and unrotated
376        HashMap<String, NamedIcon> strMap = new HashMap<>();
377        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
378            NamedIcon oldIcon = entry.getValue();
379            NamedIcon newIcon = cloneIcon(oldIcon, this);
380            newIcon.rotate(0, this);
381            newIcon.scale(1.0, this);
382            newIcon.setRotation(4, this);
383            strMap.put(_state2nameMap.get(entry.getKey()), newIcon);
384        }
385        _itemPanel.init(updateAction, strMap);
386        _itemPanel.setSelection(getTurnout());
387        initPaletteFrame(_paletteFrame, _itemPanel);
388    }
389
390    void updateItem() {
391        HashMap<Integer, NamedIcon> oldMap = cloneMap(_iconStateMap, this);
392        setTurnout(_itemPanel.getTableSelection().getSystemName());
393        _iconFamily = _itemPanel.getFamilyName();
394        HashMap<String, NamedIcon> iconMap = _itemPanel.getIconMap();
395        if (iconMap != null) {
396            for (Entry<String, NamedIcon> entry : iconMap.entrySet()) {
397                if (log.isDebugEnabled()) {
398                    log.debug("key= {}", entry.getKey());
399                }
400                NamedIcon newIcon = entry.getValue();
401                NamedIcon oldIcon = oldMap.get(_name2stateMap.get(entry.getKey()));
402                newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
403                newIcon.setRotation(oldIcon.getRotation(), this);
404                setIcon(entry.getKey(), newIcon);
405            }
406        }   // otherwise retain current map
407        finishItemUpdate(_paletteFrame, _itemPanel);
408    }
409
410    @Override
411    public boolean setEditIconMenu(JPopupMenu popup) {
412        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout"));
413        popup.add(new javax.swing.AbstractAction(txt) {
414            @Override
415            public void actionPerformed(ActionEvent e) {
416                edit();
417            }
418        });
419        return true;
420    }
421
422    @Override
423    protected void edit() {
424        makeIconEditorFrame(this, "Turnout", true, null); // NOI18N
425        _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.turnoutPickModelInstance());
426        int i = 0;
427        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
428            _iconEditor.setIcon(i++, _state2nameMap.get(entry.getKey()), entry.getValue());
429        }
430        _iconEditor.makeIconPanel(false);
431
432        // set default icons, then override with this turnout's icons
433        ActionListener addIconAction = a -> updateTurnout();
434        _iconEditor.complete(addIconAction, true, true, true);
435        _iconEditor.setSelection(getTurnout());
436    }
437
438    void updateTurnout() {
439        HashMap<Integer, NamedIcon> oldMap = cloneMap(_iconStateMap, this);
440        setTurnout(_iconEditor.getTableSelection().getDisplayName());
441        Hashtable<String, NamedIcon> iconMap = _iconEditor.getIconMap();
442
443        for (Entry<String, NamedIcon> entry : iconMap.entrySet()) {
444            if (log.isDebugEnabled()) {
445                log.debug("key= {}", entry.getKey());
446            }
447            NamedIcon newIcon = entry.getValue();
448            NamedIcon oldIcon = oldMap.get(_name2stateMap.get(entry.getKey()));
449            newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
450            newIcon.setRotation(oldIcon.getRotation(), this);
451            setIcon(entry.getKey(), newIcon);
452        }
453        _iconEditorFrame.dispose();
454        _iconEditorFrame = null;
455        _iconEditor = null;
456        invalidate();
457    }
458
459    public boolean buttonLive() {
460        if (namedTurnout == null) {
461            log.error("No turnout connection, can't process click");
462            return false;
463        }
464        return true;
465    }
466
467    @Override
468    public void doMousePressed(JmriMouseEvent e) {
469        if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) {
470            // this is a momentary button press
471            getTurnout().setCommandedState(Turnout.THROWN);
472        }
473        super.doMousePressed(e);
474    }
475
476    @Override
477    public void doMouseReleased(JmriMouseEvent e) {
478        if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) {
479            // this is a momentary button release
480            getTurnout().setCommandedState(Turnout.CLOSED);
481        }
482        super.doMouseReleased(e);
483    }
484
485    @Override
486    public void doMouseClicked(JmriMouseEvent e) {
487        if (!_editor.getFlag(Editor.OPTION_CONTROLS, isControlling())) {
488            return;
489        }
490        if (e.isMetaDown() || e.isAltDown() || !buttonLive() || getMomentary()) {
491            return;
492        }
493
494        if (getDirectControl() && !isEditable()) {
495            getTurnout().setCommandedState(Turnout.CLOSED);
496        } else {
497            alternateOnClick();
498        }
499    }
500
501    void alternateOnClick() {
502        if (getTurnout().getKnownState() == Turnout.CLOSED) {  // if clear known state, set to opposite
503            getTurnout().setCommandedState(Turnout.THROWN);
504        } else if (getTurnout().getKnownState() == Turnout.THROWN) {
505            getTurnout().setCommandedState(Turnout.CLOSED);
506        } else if (getTurnout().getCommandedState() == Turnout.CLOSED) {
507            getTurnout().setCommandedState(Turnout.THROWN);  // otherwise, set to opposite of current commanded state if known
508        } else {
509            getTurnout().setCommandedState(Turnout.CLOSED);  // just force closed.
510        }
511    }
512
513    @Override
514    public void dispose() {
515        if (namedTurnout != null) {
516            getTurnout().removePropertyChangeListener(this);
517        }
518        namedTurnout = null;
519        _iconStateMap = null;
520        _name2stateMap = null;
521        _state2nameMap = null;
522
523        super.dispose();
524    }
525
526    protected HashMap<Integer, NamedIcon> cloneMap(HashMap<Integer, NamedIcon> map,
527            TurnoutIcon pos) {
528        HashMap<Integer, NamedIcon> clone = new HashMap<>();
529        if (map != null) {
530            for (Entry<Integer, NamedIcon> entry : map.entrySet()) {
531                clone.put(entry.getKey(), cloneIcon(entry.getValue(), pos));
532                if (pos != null) {
533                    pos.setIcon(_state2nameMap.get(entry.getKey()), _iconStateMap.get(entry.getKey()));
534                }
535            }
536        }
537        return clone;
538    }
539
540    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TurnoutIcon.class);
541}