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        for (NamedIcon namedIcon : _iconStateMap.values()) {
154            max = Math.max(namedIcon.getIconHeight(), max);
155        }
156        return max;
157    }
158
159    @Override
160    public int maxWidth() {
161        int max = 0;
162        for (NamedIcon namedIcon : _iconStateMap.values()) {
163            max = Math.max(namedIcon.getIconWidth(), max);
164        }
165        return max;
166    }
167
168    /**
169     * Get current state of attached turnout
170     *
171     * @return A state variable from a Turnout, e.g. Turnout.CLOSED
172     */
173    int turnoutState() {
174        if (namedTurnout != null) {
175            return getTurnout().getKnownState();
176        } else {
177            return Turnout.UNKNOWN;
178        }
179    }
180
181    // update icon as state of turnout changes
182    @Override
183    public void propertyChange(java.beans.PropertyChangeEvent e) {
184        if (log.isDebugEnabled()) {
185            log.debug("property change: {} {} is now {}", getNameString(), e.getPropertyName(), e.getNewValue());
186        }
187
188        // when there's feedback, transition through inconsistent icon for better
189        // animation
190        if (getTristate()
191                && (getTurnout().getFeedbackMode() != Turnout.DIRECT)
192                && (e.getPropertyName().equals(Turnout.PROPERTY_COMMANDED_STATE))) {
193            if (getTurnout().getCommandedState() != getTurnout().getKnownState()) {
194                int now = Turnout.INCONSISTENT;
195                displayState(now);
196            }
197            // this takes care of the quick double click
198            if (getTurnout().getCommandedState() == getTurnout().getKnownState()) {
199                int now = (Integer) e.getNewValue();
200                displayState(now);
201            }
202        }
203
204        if (e.getPropertyName().equals(Turnout.PROPERTY_KNOWN_STATE)) {
205            int now = (Integer) e.getNewValue();
206            displayState(now);
207        }
208    }
209
210    public String getStateName(int state) {
211        return _state2nameMap.get(state);
212
213    }
214
215    @Override
216    @Nonnull
217    public String getTypeString() {
218        return Bundle.getMessage("PositionableType_TurnoutIcon");
219    }
220
221    @Override
222    public String getNameString() {
223        String name;
224        if (namedTurnout == null) {
225            name = Bundle.getMessage("NotConnected");
226        } else {
227            name = getTurnout().getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME);
228        }
229        return name;
230    }
231
232    public void setTristate(boolean set) {
233        tristate = set;
234    }
235
236    public boolean getTristate() {
237        return tristate;
238    }
239    private boolean tristate = false;
240
241    private boolean momentary = false;
242
243    public boolean getMomentary() {
244        return momentary;
245    }
246
247    public void setMomentary(boolean m) {
248        momentary = m;
249    }
250
251    private boolean directControl = false;
252
253    public boolean getDirectControl() {
254        return directControl;
255    }
256
257    public void setDirectControl(boolean m) {
258        directControl = m;
259    }
260
261    private final JCheckBoxMenuItem momentaryItem = new JCheckBoxMenuItem(Bundle.getMessage("Momentary"));
262    private final JCheckBoxMenuItem directControlItem = new JCheckBoxMenuItem(Bundle.getMessage("DirectControl"));
263
264    /**
265     * Pop-up displays unique attributes of turnouts
266     */
267    @Override
268    public boolean showPopUp(JPopupMenu popup) {
269        if (isEditable()) {
270            // add tristate option if turnout has feedback
271            if (namedTurnout != null && getTurnout().getFeedbackMode() != Turnout.DIRECT) {
272                addTristateEntry(popup);
273            }
274
275            popup.add(momentaryItem);
276            momentaryItem.setSelected(getMomentary());
277            momentaryItem.addActionListener(e -> setMomentary(momentaryItem.isSelected()));
278
279            popup.add(directControlItem);
280            directControlItem.setSelected(getDirectControl());
281            directControlItem.addActionListener(e -> setDirectControl(directControlItem.isSelected()));
282        } else if (getDirectControl()) {
283            getTurnout().setCommandedState(Turnout.THROWN);
284        }
285        return true;
286    }
287
288    private javax.swing.JCheckBoxMenuItem tristateItem = null;
289
290    void addTristateEntry(JPopupMenu popup) {
291        tristateItem = new javax.swing.JCheckBoxMenuItem(Bundle.getMessage("Tristate"));
292        tristateItem.setSelected(getTristate());
293        popup.add(tristateItem);
294        tristateItem.addActionListener(e -> setTristate(tristateItem.isSelected()));
295    }
296
297    /**
298     * ****** popup AbstractAction method overrides ********
299     */
300    @Override
301    protected void rotateOrthogonal() {
302        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
303            entry.getValue().setRotation(entry.getValue().getRotation() + 1, this);
304        }
305        displayState(turnoutState());
306        // bug fix, must repaint icons that have same width and height
307        repaint();
308    }
309
310    @Override
311    public void setScale(double s) {
312        _scale = s;
313        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
314            entry.getValue().scale(s, this);
315        }
316        displayState(turnoutState());
317    }
318
319    @Override
320    public void rotate(int deg) {
321        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
322            entry.getValue().rotate(deg, this);
323        }
324        setDegrees(deg);
325        displayState(turnoutState());
326    }
327
328    /**
329     * Drive the current state of the display from the state of the turnout.
330     * {@inheritDoc}
331     */
332    @Override
333    public void displayState(int state) {
334        if (getNamedTurnout() == null) {
335            log.debug("Display state {}, disconnected", state);
336        } else {
337            // log.debug("{} displayState {}", getNameString(), _state2nameMap.get(state));
338            if (isText()) {
339                super.setText(_state2nameMap.get(state));
340            }
341            if (isIcon()) {
342                NamedIcon icon = getIcon(state);
343                if (icon != null) {
344                    super.setIcon(icon);
345                }
346            }
347        }
348        updateSize();
349    }
350
351    TableItemPanel<Turnout> _itemPanel;
352
353    @Override
354    public boolean setEditItemMenu(JPopupMenu popup) {
355        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout"));
356        popup.add(new javax.swing.AbstractAction(txt) {
357            @Override
358            public void actionPerformed(ActionEvent e) {
359                editItem();
360            }
361        });
362        return true;
363    }
364
365    protected void editItem() {
366        _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"),
367                Bundle.getMessage("BeanNameTurnout")));
368        _itemPanel = new TableItemPanel<>(_paletteFrame, "Turnout", _iconFamily,
369                PickListModel.turnoutPickModelInstance()); // NOI18N
370        ActionListener updateAction = a -> updateItem();
371        // duplicate icon map with state names rather than int states and unscaled and unrotated
372        HashMap<String, NamedIcon> strMap = new HashMap<>();
373        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
374            NamedIcon oldIcon = entry.getValue();
375            NamedIcon newIcon = cloneIcon(oldIcon, this);
376            newIcon.rotate(0, this);
377            newIcon.scale(1.0, this);
378            newIcon.setRotation(4, this);
379            strMap.put(_state2nameMap.get(entry.getKey()), newIcon);
380        }
381        _itemPanel.init(updateAction, strMap);
382        _itemPanel.setSelection(getTurnout());
383        initPaletteFrame(_paletteFrame, _itemPanel);
384    }
385
386    void updateItem() {
387        HashMap<Integer, NamedIcon> oldMap = cloneMap(_iconStateMap, this);
388        setTurnout(_itemPanel.getTableSelection().getSystemName());
389        _iconFamily = _itemPanel.getFamilyName();
390        HashMap<String, NamedIcon> iconMap = _itemPanel.getIconMap();
391        if (iconMap != null) {
392            for (Entry<String, NamedIcon> entry : iconMap.entrySet()) {
393                if (log.isDebugEnabled()) {
394                    log.debug("key= {}", entry.getKey());
395                }
396                NamedIcon newIcon = entry.getValue();
397                NamedIcon oldIcon = oldMap.get(_name2stateMap.get(entry.getKey()));
398                newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
399                newIcon.setRotation(oldIcon.getRotation(), this);
400                setIcon(entry.getKey(), newIcon);
401            }
402        }   // otherwise retain current map
403        finishItemUpdate(_paletteFrame, _itemPanel);
404    }
405
406    @Override
407    public boolean setEditIconMenu(JPopupMenu popup) {
408        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("BeanNameTurnout"));
409        popup.add(new javax.swing.AbstractAction(txt) {
410            @Override
411            public void actionPerformed(ActionEvent e) {
412                edit();
413            }
414        });
415        return true;
416    }
417
418    @Override
419    protected void edit() {
420        makeIconEditorFrame(this, "Turnout", true, null); // NOI18N
421        _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.turnoutPickModelInstance());
422        int i = 0;
423        for (Entry<Integer, NamedIcon> entry : _iconStateMap.entrySet()) {
424            _iconEditor.setIcon(i++, _state2nameMap.get(entry.getKey()), entry.getValue());
425        }
426        _iconEditor.makeIconPanel(false);
427
428        // set default icons, then override with this turnout's icons
429        ActionListener addIconAction = a -> updateTurnout();
430        _iconEditor.complete(addIconAction, true, true, true);
431        _iconEditor.setSelection(getTurnout());
432    }
433
434    void updateTurnout() {
435        HashMap<Integer, NamedIcon> oldMap = cloneMap(_iconStateMap, this);
436        setTurnout(_iconEditor.getTableSelection().getDisplayName());
437        Hashtable<String, NamedIcon> iconMap = _iconEditor.getIconMap();
438
439        for (Entry<String, NamedIcon> entry : iconMap.entrySet()) {
440            if (log.isDebugEnabled()) {
441                log.debug("key= {}", entry.getKey());
442            }
443            NamedIcon newIcon = entry.getValue();
444            NamedIcon oldIcon = oldMap.get(_name2stateMap.get(entry.getKey()));
445            newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
446            newIcon.setRotation(oldIcon.getRotation(), this);
447            setIcon(entry.getKey(), newIcon);
448        }
449        _iconEditorFrame.dispose();
450        _iconEditorFrame = null;
451        _iconEditor = null;
452        invalidate();
453    }
454
455    public boolean buttonLive() {
456        if (namedTurnout == null) {
457            log.error("No turnout connection, can't process click");
458            return false;
459        }
460        return true;
461    }
462
463    @Override
464    public void doMousePressed(JmriMouseEvent e) {
465        if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) {
466            // this is a momentary button press
467            getTurnout().setCommandedState(Turnout.THROWN);
468        }
469        super.doMousePressed(e);
470    }
471
472    @Override
473    public void doMouseReleased(JmriMouseEvent e) {
474        if (getMomentary() && buttonLive() && !e.isMetaDown() && !e.isAltDown()) {
475            // this is a momentary button release
476            getTurnout().setCommandedState(Turnout.CLOSED);
477        }
478        super.doMouseReleased(e);
479    }
480
481    @Override
482    public void doMouseClicked(JmriMouseEvent e) {
483        if (!_editor.getFlag(Editor.OPTION_CONTROLS, isControlling())) {
484            return;
485        }
486        if (e.isMetaDown() || e.isAltDown() || !buttonLive() || getMomentary()) {
487            return;
488        }
489
490        if (getDirectControl() && !isEditable()) {
491            getTurnout().setCommandedState(Turnout.CLOSED);
492        } else {
493            alternateOnClick();
494        }
495    }
496
497    void alternateOnClick() {
498        if (getTurnout().getKnownState() == Turnout.CLOSED) {  // if clear known state, set to opposite
499            getTurnout().setCommandedState(Turnout.THROWN);
500        } else if (getTurnout().getKnownState() == Turnout.THROWN) {
501            getTurnout().setCommandedState(Turnout.CLOSED);
502        } else if (getTurnout().getCommandedState() == Turnout.CLOSED) {
503            getTurnout().setCommandedState(Turnout.THROWN);  // otherwise, set to opposite of current commanded state if known
504        } else {
505            getTurnout().setCommandedState(Turnout.CLOSED);  // just force closed.
506        }
507    }
508
509    @Override
510    public void dispose() {
511        if (namedTurnout != null) {
512            getTurnout().removePropertyChangeListener(this);
513        }
514        namedTurnout = null;
515        _iconStateMap = null;
516        _name2stateMap = null;
517        _state2nameMap = null;
518
519        super.dispose();
520    }
521
522    protected HashMap<Integer, NamedIcon> cloneMap(HashMap<Integer, NamedIcon> map,
523            TurnoutIcon pos) {
524        HashMap<Integer, NamedIcon> clone = new HashMap<>();
525        if (map != null) {
526            for (Entry<Integer, NamedIcon> entry : map.entrySet()) {
527                clone.put(entry.getKey(), cloneIcon(entry.getValue(), pos));
528                if (pos != null) {
529                    pos.setIcon(_state2nameMap.get(entry.getKey()), _iconStateMap.get(entry.getKey()));
530                }
531            }
532        }
533        return clone;
534    }
535
536    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TurnoutIcon.class);
537}