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