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