001package jmri.jmrit.display;
002
003import java.awt.event.ActionEvent;
004import java.awt.event.ActionListener;
005import java.util.ArrayList;
006import java.util.HashMap;
007import java.util.List;
008
009import javax.annotation.Nonnull;
010import javax.swing.AbstractAction;
011import javax.swing.JPopupMenu;
012
013import jmri.InstanceManager;
014import jmri.NamedBeanHandle;
015import jmri.Sensor;
016import jmri.jmrit.catalog.NamedIcon;
017import jmri.jmrit.display.palette.MultiSensorItemPanel;
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 set of Sensors.
026 * <p>
027 * Each sensor has an associated image. Normally, only one sensor will be active
028 * at a time, and in that case the associated image will be shown. If more than
029 * one is active, one of the corresponding images will be shown, but which one
030 * is not guaranteed.
031 *
032 * @author Bob Jacobsen Copyright (C) 2001, 2007
033 */
034public class MultiSensorIcon extends PositionableLabel implements java.beans.PropertyChangeListener {
035
036    String _iconFamily;
037
038    public MultiSensorIcon(Editor editor) {
039        // super ctor call to make sure this is an icon label
040        super(new NamedIcon("resources/icons/smallschematics/tracksegments/circuit-error.gif",
041                "resources/icons/smallschematics/tracksegments/circuit-error.gif"), editor);
042        _control = true;
043        displayState();
044        setPopupUtility(null);
045    }
046
047    boolean updown = false;
048
049    // if not updown, is rightleft
050    public void setUpDown(boolean b) {
051        updown = b;
052    }
053
054    public boolean getUpDown() {
055        return updown;
056    }
057
058    ArrayList<Entry> entries = new ArrayList<>();
059
060    @Override
061    public Positionable deepClone() {
062        MultiSensorIcon pos = new MultiSensorIcon(_editor);
063        return finishClone(pos);
064    }
065
066    protected Positionable finishClone(MultiSensorIcon pos) {
067        pos.setInactiveIcon(cloneIcon(getInactiveIcon(), pos));
068        pos.setInconsistentIcon(cloneIcon(getInconsistentIcon(), pos));
069        pos.setUnknownIcon(cloneIcon(getUnknownIcon(), pos));
070        for (int i = 0; i < entries.size(); i++) {
071            pos.addEntry(getSensorName(i), cloneIcon(getSensorIcon(i), pos));
072        }
073        return super.finishClone(pos);
074    }
075
076    public void addEntry(NamedBeanHandle<Sensor> sensor, NamedIcon icon) {
077        if (sensor != null) {
078            if (log.isDebugEnabled()) {
079                log.debug("addEntry: sensor= {}", sensor.getName());
080            }
081            Entry e = new Entry();
082            sensor.getBean().addPropertyChangeListener(this, sensor.getName(), "MultiSensor Icon");
083            e.namedSensor = sensor;
084            e.icon = icon;
085            entries.add(e);
086            displayState();
087        } else {
088            log.error("Sensor not available, icon won't see changes");
089        }
090    }
091
092    public void addEntry(String pName, NamedIcon icon) {
093        NamedBeanHandle<Sensor> sensor;
094        if (InstanceManager.getNullableDefault(jmri.SensorManager.class) != null) {
095            sensor = jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class)
096                    .getNamedBeanHandle(pName, InstanceManager.sensorManagerInstance().provideSensor(pName));
097            addEntry(sensor, icon);
098        } else {
099            log.error("No SensorManager for this protocol, icon won't see changes");
100        }
101    }
102
103    public int getNumEntries() {
104        return entries.size();
105    }
106
107    public List<Sensor> getSensors() {
108        ArrayList<Sensor> list = new ArrayList<>(getNumEntries());
109        for (Entry handle : entries) {
110            list.add(handle.namedSensor.getBean());
111        }
112        return list;
113    }
114
115    public String getSensorName(int i) {
116        return entries.get(i).namedSensor.getName();
117    }
118
119    public NamedIcon getSensorIcon(int i) {
120        return entries.get(i).icon;
121    }
122
123    public String getFamily() {
124        return _iconFamily;
125    }
126
127    public void setFamily(String family) {
128        _iconFamily = family;
129    }
130
131    // display icons
132    String inactiveName = "resources/icons/USS/plate/levers/l-inactive.gif";
133    NamedIcon inactive = new NamedIcon(inactiveName, inactiveName);
134
135    String inconsistentName = "resources/icons/USS/plate/levers/l-inconsistent.gif";
136    NamedIcon inconsistent = new NamedIcon(inconsistentName, inconsistentName);
137
138    String unknownName = "resources/icons/USS/plate/levers/l-unknown.gif";
139    NamedIcon unknown = new NamedIcon(unknownName, unknownName);
140
141    public NamedIcon getInactiveIcon() {
142        return inactive;
143    }
144
145    public void setInactiveIcon(NamedIcon i) {
146        inactive = i;
147    }
148
149    public NamedIcon getInconsistentIcon() {
150        return inconsistent;
151    }
152
153    public void setInconsistentIcon(NamedIcon i) {
154        inconsistent = i;
155    }
156
157    public NamedIcon getUnknownIcon() {
158        return unknown;
159    }
160
161    public void setUnknownIcon(NamedIcon i) {
162        unknown = i;
163    }
164
165    // update icon as state of turnout changes
166    @Override
167    public void propertyChange(java.beans.PropertyChangeEvent e) {
168        if (log.isDebugEnabled()) {
169            String prop = e.getPropertyName();
170            Sensor sen = (Sensor) e.getSource();
171            log.debug("property change({}) Sensor state= {} - old= {}, new= {}",
172                    prop, sen.getKnownState(), e.getOldValue(), e.getNewValue());
173        }
174        if (e.getPropertyName().equals("KnownState")) {
175            displayState();
176            _editor.repaint();
177        }
178    }
179
180    @Override
181    @Nonnull
182    public String getTypeString() {
183        return Bundle.getMessage("PositionableType_MultiSensorIcon");
184    }
185
186    @Override
187    public String getNameString() {
188        StringBuilder name = new StringBuilder();
189        if ((entries == null) || (entries.size() < 1)) {
190            name.append(Bundle.getMessage("NotConnected"));
191        } else {
192            name.append(entries.get(0).namedSensor.getName());
193            entries.forEach((entry) -> name.append(",").append(entry.namedSensor.getName()));
194        }
195        return name.toString();
196    }
197
198    /**
199     * ****** popup AbstractAction.actionPerformed method overrides ********
200     */
201    @Override
202    protected void rotateOrthogonal() {
203        for (Entry entry : entries) {
204            NamedIcon icon = entry.icon;
205            icon.setRotation(icon.getRotation() + 1, this);
206        }
207        inactive.setRotation(inactive.getRotation() + 1, this);
208        unknown.setRotation(unknown.getRotation() + 1, this);
209        inconsistent.setRotation(inconsistent.getRotation() + 1, this);
210        displayState();
211        // bug fix, must repaint icons that have same width and height
212        repaint();
213    }
214
215    @Override
216    public void setScale(double s) {
217        for (Entry entry : entries) {
218            NamedIcon icon = entry.icon;
219            icon.scale(s, this);
220        }
221        inactive.scale(s, this);
222        unknown.scale(s, this);
223        inconsistent.scale(s, this);
224        displayState();
225    }
226
227    @Override
228    public void rotate(int deg) {
229        for (Entry entry : entries) {
230            NamedIcon icon = entry.icon;
231            icon.rotate(deg, this);
232        }
233        inactive.rotate(deg, this);
234        unknown.rotate(deg, this);
235        inconsistent.rotate(deg, this);
236        displayState();
237    }
238
239    @Override
240    public boolean setEditItemMenu(JPopupMenu popup) {
241        String txt = Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor"));
242        popup.add(new javax.swing.AbstractAction(txt) {
243            @Override
244            public void actionPerformed(ActionEvent e) {
245                editItem();
246            }
247        });
248        return true;
249    }
250
251    MultiSensorItemPanel _itemPanel;
252
253    protected void editItem() {
254        _paletteFrame = makePaletteFrame(Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor")));
255        _itemPanel = new MultiSensorItemPanel(_paletteFrame, "MultiSensor", _iconFamily,
256                PickListModel.multiSensorPickModelInstance());
257        ActionListener updateAction = (ActionEvent a) -> updateItem();
258        // duplicate _iconMap map with unscaled and unrotated icons
259        HashMap<String, NamedIcon> map = new HashMap<>();
260        map.put("SensorStateInactive", inactive);
261        map.put("BeanStateInconsistent", inconsistent);
262        map.put("BeanStateUnknown", unknown);
263        for (int i = 0; i < entries.size(); i++) {
264            map.put(MultiSensorItemPanel.getPositionName(i), entries.get(i).icon);
265        }
266        _itemPanel.init(updateAction, map);
267        for (Entry entry : entries) {
268            _itemPanel.setSelection(entry.namedSensor.getBean());
269        }
270        _itemPanel.setUpDown(getUpDown());
271        initPaletteFrame(_paletteFrame, _itemPanel);
272    }
273
274    void updateItem() {
275        if (!_itemPanel.oktoUpdate()) {
276            return;
277        }
278        HashMap<String, NamedIcon> iconMap = _itemPanel.getIconMap();
279        ArrayList<Sensor> selections = _itemPanel.getTableSelections();
280        setInactiveIcon(new NamedIcon(iconMap.get("SensorStateInactive")));
281        setInconsistentIcon(new NamedIcon(iconMap.get("BeanStateInconsistent")));
282        setUnknownIcon(new NamedIcon(iconMap.get("BeanStateUnknown")));
283        entries = new ArrayList<>(selections.size());
284        for (int i = 0; i < selections.size(); i++) {
285            addEntry(selections.get(i).getDisplayName(), new NamedIcon(iconMap.get(MultiSensorItemPanel.getPositionName(i))));
286        }
287        _iconFamily = _itemPanel.getFamilyName();
288        _itemPanel.clearSelections();
289        setUpDown(_itemPanel.getUpDown());
290        finishItemUpdate(_paletteFrame, _itemPanel);
291    }
292
293    @Override
294    public boolean setEditIconMenu(JPopupMenu popup) {
295        String txt = Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor"));
296        popup.add(new AbstractAction(txt) {
297            @Override
298            public void actionPerformed(ActionEvent e) {
299                edit();
300            }
301        });
302        return true;
303    }
304
305    @Override
306    protected void edit() {
307        MultiSensorIconAdder iconEditor = new MultiSensorIconAdder("MultiSensor");
308        makeIconEditorFrame(this, "MultiSensor", false, iconEditor);
309        _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.sensorPickModelInstance());
310        _iconEditor.setIcon(2, "SensorStateInactive", inactive);
311        _iconEditor.setIcon(0, "BeanStateInconsistent", inconsistent);
312        _iconEditor.setIcon(1, "BeanStateUnknown", unknown);
313        if (_iconEditor instanceof MultiSensorIconAdder) {
314            ((MultiSensorIconAdder) _iconEditor).setMultiIcon(entries);
315            _iconEditor.makeIconPanel(false);
316
317            ActionListener addIconAction = (ActionEvent a) -> updateSensor();
318            iconEditor.complete(addIconAction, true, true, true);
319        }
320    }
321
322    void updateSensor() {
323        if (_iconEditor instanceof MultiSensorIconAdder) {
324            MultiSensorIconAdder iconEditor = (MultiSensorIconAdder) _iconEditor;
325            setInactiveIcon(iconEditor.getIcon("SensorStateInactive"));
326            setInconsistentIcon(iconEditor.getIcon("BeanStateInconsistent"));
327            setUnknownIcon(iconEditor.getIcon("BeanStateUnknown"));
328            for (Entry entry : entries) {
329                entry.namedSensor.getBean().removePropertyChangeListener(this);
330            }
331            int numPositions = iconEditor.getNumIcons();
332            entries = new ArrayList<>(numPositions);
333            for (int i = 3; i < numPositions; i++) {
334                NamedIcon icon = iconEditor.getIcon(i);
335                NamedBeanHandle<Sensor> namedSensor = iconEditor.getSensor(i);
336                addEntry(namedSensor, icon);
337            }
338            setUpDown(iconEditor.getUpDown());
339        }
340        _iconEditorFrame.dispose();
341        _iconEditorFrame = null;
342        _iconEditor = null;
343        invalidate();
344    }
345    /**
346     * *********** end popup action methods ***************
347     */
348
349    int displaying = -1;
350
351    /**
352     * Drive the current state of the display from the state of the turnout.
353     */
354    public void displayState() {
355
356        updateSize();
357
358        // run the entries
359        boolean foundActive = false;
360
361        for (int i = 0; i < entries.size(); i++) {
362            Entry e = entries.get(i);
363
364            int state = e.namedSensor.getBean().getKnownState();
365
366            switch (state) {
367                case Sensor.ACTIVE:
368                    if (isText()) {
369                        super.setText(Bundle.getMessage("SensorStateActive"));
370                    }
371                    if (isIcon()) {
372                        super.setIcon(e.icon);
373                    }
374                    foundActive = true;
375                    displaying = i;
376                    break;  // look at the next ones too
377                case Sensor.UNKNOWN:
378                    if (isText()) {
379                        super.setText(Bundle.getMessage("BeanStateUnknown"));
380                    }
381                    if (isIcon()) {
382                        super.setIcon(unknown);
383                    }
384                    return;  // this trumps all others
385                case Sensor.INCONSISTENT:
386                    if (isText()) {
387                        super.setText(Bundle.getMessage("BeanStateInconsistent"));
388                    }
389                    if (isIcon()) {
390                        super.setIcon(inconsistent);
391                    }
392                    break;
393                default:
394                    break;
395            }
396        }
397        // loop has gotten to here
398        if (foundActive) {
399            return;  // set active
400        }        // only case left is all inactive
401        if (isText()) {
402            super.setText(Bundle.getMessage("SensorStateInactive"));
403        }
404        if (isIcon()) {
405            super.setIcon(inactive);
406        }
407    }
408
409    // Use largest size. If icons are not same size,
410    // this can result in drawing artifacts.
411    @Override
412    public int maxHeight() {
413        int size = Math.max(
414                ((inactive != null) ? inactive.getIconHeight() : 0),
415                Math.max((unknown != null) ? unknown.getIconHeight() : 0,
416                        (inconsistent != null) ? inconsistent.getIconHeight() : 0)
417        );
418        if (entries != null) {
419            for (Entry entry : entries) {
420                size = Math.max(size, entry.icon.getIconHeight());
421            }
422        }
423        return size;
424    }
425
426    // Use largest size. If icons are not same size,
427    // this can result in drawing artifacts.
428    @Override
429    public int maxWidth() {
430        int size = Math.max(
431                ((inactive != null) ? inactive.getIconWidth() : 0),
432                Math.max((unknown != null) ? unknown.getIconWidth() : 0,
433                        (inconsistent != null) ? inconsistent.getIconWidth() : 0)
434        );
435        if (entries != null) {
436            for (Entry entry : entries) {
437                size = Math.max(size, entry.icon.getIconWidth());
438            }
439        }
440        return size;
441    }
442
443    public void performMouseClicked(JmriMouseEvent e, int xx, int yy) {
444        if (log.isDebugEnabled()) {
445            log.debug("performMouseClicked: location ({}, {}), click from ({}, {}) displaying={}",
446                    getX(), getY(), xx, yy, displaying);
447        }
448        if (!buttonLive() || (entries == null || entries.size() < 1)) {
449            if (log.isDebugEnabled()) {
450                log.debug("performMouseClicked: buttonLive={}, entries={}", buttonLive(), entries.size());
451            }
452            return;
453        }
454
455        // find if we want to increment or decrement
456        // regardless of the zooming scale, (getX(), getY()) is the un-zoomed position in _editor._contents
457        // but the click is at the zoomed position
458        double ratio = _editor.getPaintScale();
459        boolean dec = false;
460        if (updown) {
461            if ((yy/ratio - getY()) > (double)(maxHeight()) / 2) {
462                dec = true;
463            }
464        } else {
465           if ((xx/ratio - getX()) < (double)(maxWidth()) / 2) {
466                dec = true;
467            }
468        }
469
470        // get new index
471        int next;
472        if (dec) {
473            next = displaying - 1;
474        } else {
475            next = displaying + 1;
476        }
477        if (next < 0) {
478            next = 0;
479        }
480        if (next >= entries.size()) {
481            next = entries.size() - 1;
482        }
483
484        int drop = displaying;
485        if (log.isDebugEnabled()) {
486            log.debug("dec= {} displaying={} next= {}", dec, displaying, next);
487        }
488        try {
489            entries.get(next).namedSensor.getBean().setKnownState(Sensor.ACTIVE);
490            if (drop >= 0 && drop != next) {
491                entries.get(drop).namedSensor.getBean().setKnownState(Sensor.INACTIVE);
492            }
493        } catch (jmri.JmriException ex) {
494            log.error("Click failed to set sensor: ", ex);
495        }
496    }
497
498    boolean buttonLive() {
499        return _editor.getFlag(Editor.OPTION_CONTROLS, isControlling());
500    }
501
502    @Override
503    public void doMouseClicked(JmriMouseEvent e) {
504        if (!e.isAltDown() && !e.isMetaDown()) {
505            performMouseClicked(e, e.getX(), e.getY());
506        }
507    }
508
509    @Override
510    public void dispose() {
511        // remove listeners
512        for (Entry entry : entries) {
513            entry.namedSensor.getBean().removePropertyChangeListener(this);
514        }
515        super.dispose();
516    }
517
518    static class Entry {
519
520        NamedBeanHandle<Sensor> namedSensor;
521        NamedIcon icon;
522    }
523
524    private final static Logger log = LoggerFactory.getLogger(MultiSensorIcon.class);
525
526}