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