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    public String getSensorName(int i) {
117        return entries.get(i).namedSensor.getName();
118    }
119
120    public NamedIcon getSensorIcon(int i) {
121        return entries.get(i).icon;
122    }
123
124    public String getFamily() {
125        return _iconFamily;
126    }
127
128    public void setFamily(String family) {
129        _iconFamily = family;
130    }
131
132    // display icons
133    String inactiveName = "resources/icons/USS/plate/levers/l-inactive.gif";
134    NamedIcon inactive = new NamedIcon(inactiveName, inactiveName);
135
136    String inconsistentName = "resources/icons/USS/plate/levers/l-inconsistent.gif";
137    NamedIcon inconsistent = new NamedIcon(inconsistentName, inconsistentName);
138
139    String unknownName = "resources/icons/USS/plate/levers/l-unknown.gif";
140    NamedIcon unknown = new NamedIcon(unknownName, unknownName);
141
142    public NamedIcon getInactiveIcon() {
143        return inactive;
144    }
145
146    public void setInactiveIcon(NamedIcon i) {
147        inactive = i;
148    }
149
150    public NamedIcon getInconsistentIcon() {
151        return inconsistent;
152    }
153
154    public void setInconsistentIcon(NamedIcon i) {
155        inconsistent = i;
156    }
157
158    public NamedIcon getUnknownIcon() {
159        return unknown;
160    }
161
162    public void setUnknownIcon(NamedIcon i) {
163        unknown = i;
164    }
165
166    // update icon as state of turnout changes
167    @Override
168    public void propertyChange(java.beans.PropertyChangeEvent e) {
169        if (log.isDebugEnabled()) {
170            String prop = e.getPropertyName();
171            Sensor sen = (Sensor) e.getSource();
172            log.debug("property change({}) Sensor state= {} - old= {}, new= {}",
173                    prop, sen.getKnownState(), e.getOldValue(), e.getNewValue());
174        }
175        if (e.getPropertyName().equals("KnownState")) {
176            Rectangle dirty = getBounds();
177            displayState();
178            dirty.add(getBounds()); // union with the new bounds, in case displayState resized this icon
179            _editor.repaintTargetPanel(dirty);
180        }
181    }
182
183    @Override
184    @Nonnull
185    public String getTypeString() {
186        return Bundle.getMessage("PositionableType_MultiSensorIcon");
187    }
188
189    @Override
190    public String getNameString() {
191        StringBuilder name = new StringBuilder();
192        if ((entries == null) || (entries.size() < 1)) {
193            name.append(Bundle.getMessage("NotConnected"));
194        } else {
195            name.append(entries.get(0).namedSensor.getName());
196            entries.forEach((entry) -> name.append(",").append(entry.namedSensor.getName()));
197        }
198        return name.toString();
199    }
200
201    /**
202     * ****** popup AbstractAction.actionPerformed method overrides ********
203     */
204    @Override
205    protected void rotateOrthogonal() {
206        for (Entry entry : entries) {
207            NamedIcon icon = entry.icon;
208            icon.setRotation(icon.getRotation() + 1, this);
209        }
210        inactive.setRotation(inactive.getRotation() + 1, this);
211        unknown.setRotation(unknown.getRotation() + 1, this);
212        inconsistent.setRotation(inconsistent.getRotation() + 1, this);
213        displayState();
214        // bug fix, must repaint icons that have same width and height
215        repaint();
216    }
217
218    @Override
219    public void setScale(double s) {
220        for (Entry entry : entries) {
221            NamedIcon icon = entry.icon;
222            icon.scale(s, this);
223        }
224        inactive.scale(s, this);
225        unknown.scale(s, this);
226        inconsistent.scale(s, this);
227        displayState();
228    }
229
230    @Override
231    public void rotate(int deg) {
232        for (Entry entry : entries) {
233            NamedIcon icon = entry.icon;
234            icon.rotate(deg, this);
235        }
236        inactive.rotate(deg, this);
237        unknown.rotate(deg, this);
238        inconsistent.rotate(deg, this);
239        displayState();
240    }
241
242    @Override
243    public boolean setEditItemMenu(JPopupMenu popup) {
244        String txt = Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor"));
245        popup.add(new javax.swing.AbstractAction(txt) {
246            @Override
247            public void actionPerformed(ActionEvent e) {
248                editItem();
249            }
250        });
251        return true;
252    }
253
254    MultiSensorItemPanel _itemPanel;
255
256    protected void editItem() {
257        _paletteFrame = makePaletteFrame(Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor")));
258        _itemPanel = new MultiSensorItemPanel(_paletteFrame, "MultiSensor", _iconFamily,
259                PickListModel.multiSensorPickModelInstance());
260        ActionListener updateAction = (ActionEvent a) -> updateItem();
261        // duplicate _iconMap map with unscaled and unrotated icons
262        HashMap<String, NamedIcon> map = new HashMap<>();
263        map.put("SensorStateInactive", inactive);
264        map.put("BeanStateInconsistent", inconsistent);
265        map.put("BeanStateUnknown", unknown);
266        for (int i = 0; i < entries.size(); i++) {
267            map.put(MultiSensorItemPanel.getPositionName(i), entries.get(i).icon);
268        }
269        _itemPanel.init(updateAction, map);
270        for (Entry entry : entries) {
271            _itemPanel.setSelection(entry.namedSensor.getBean());
272        }
273        _itemPanel.setUpDown(getUpDown());
274        initPaletteFrame(_paletteFrame, _itemPanel);
275    }
276
277    void updateItem() {
278        if (!_itemPanel.oktoUpdate()) {
279            return;
280        }
281        HashMap<String, NamedIcon> iconMap = _itemPanel.getIconMap();
282        ArrayList<Sensor> selections = _itemPanel.getTableSelections();
283        setInactiveIcon(new NamedIcon(iconMap.get("SensorStateInactive")));
284        setInconsistentIcon(new NamedIcon(iconMap.get("BeanStateInconsistent")));
285        setUnknownIcon(new NamedIcon(iconMap.get("BeanStateUnknown")));
286        entries = new ArrayList<>(selections.size());
287        for (int i = 0; i < selections.size(); i++) {
288            addEntry(selections.get(i).getDisplayName(), new NamedIcon(iconMap.get(MultiSensorItemPanel.getPositionName(i))));
289        }
290        _iconFamily = _itemPanel.getFamilyName();
291        _itemPanel.clearSelections();
292        setUpDown(_itemPanel.getUpDown());
293        finishItemUpdate(_paletteFrame, _itemPanel);
294    }
295
296    @Override
297    public boolean setEditIconMenu(JPopupMenu popup) {
298        String txt = Bundle.getMessage("EditItem", Bundle.getMessage("MultiSensor"));
299        popup.add(new AbstractAction(txt) {
300            @Override
301            public void actionPerformed(ActionEvent e) {
302                edit();
303            }
304        });
305        return true;
306    }
307
308    @Override
309    protected void edit() {
310        MultiSensorIconAdder iconEditor = new MultiSensorIconAdder("MultiSensor");
311        makeIconEditorFrame(this, "MultiSensor", false, iconEditor);
312        _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.sensorPickModelInstance());
313        _iconEditor.setIcon(2, "SensorStateInactive", inactive);
314        _iconEditor.setIcon(0, "BeanStateInconsistent", inconsistent);
315        _iconEditor.setIcon(1, "BeanStateUnknown", unknown);
316        if (_iconEditor instanceof MultiSensorIconAdder) {
317            ((MultiSensorIconAdder) _iconEditor).setMultiIcon(entries);
318            _iconEditor.makeIconPanel(false);
319
320            ActionListener addIconAction = (ActionEvent a) -> updateSensor();
321            iconEditor.complete(addIconAction, true, true, true);
322        }
323    }
324
325    void updateSensor() {
326        if (_iconEditor instanceof MultiSensorIconAdder) {
327            MultiSensorIconAdder iconEditor = (MultiSensorIconAdder) _iconEditor;
328            setInactiveIcon(iconEditor.getIcon("SensorStateInactive"));
329            setInconsistentIcon(iconEditor.getIcon("BeanStateInconsistent"));
330            setUnknownIcon(iconEditor.getIcon("BeanStateUnknown"));
331            for (Entry entry : entries) {
332                entry.namedSensor.getBean().removePropertyChangeListener(this);
333            }
334            int numPositions = iconEditor.getNumIcons();
335            entries = new ArrayList<>(numPositions);
336            for (int i = 3; i < numPositions; i++) {
337                NamedIcon icon = iconEditor.getIcon(i);
338                NamedBeanHandle<Sensor> namedSensor = iconEditor.getSensor(i);
339                addEntry(namedSensor, icon);
340            }
341            setUpDown(iconEditor.getUpDown());
342        }
343        _iconEditorFrame.dispose();
344        _iconEditorFrame = null;
345        _iconEditor = null;
346        invalidate();
347    }
348    /**
349     * *********** end popup action methods ***************
350     */
351
352    int displaying = -1;
353
354    /**
355     * Drive the current state of the display from the state of the turnout.
356     */
357    public void displayState() {
358
359        updateSize();
360
361        // run the entries
362        boolean foundActive = false;
363
364        for (int i = 0; i < entries.size(); i++) {
365            Entry e = entries.get(i);
366
367            int state = e.namedSensor.getBean().getKnownState();
368
369            switch (state) {
370                case Sensor.ACTIVE:
371                    if (isText()) {
372                        super.setText(Bundle.getMessage("SensorStateActive"));
373                    }
374                    if (isIcon()) {
375                        super.setIcon(e.icon);
376                    }
377                    foundActive = true;
378                    displaying = i;
379                    break;  // look at the next ones too
380                case Sensor.UNKNOWN:
381                    if (isText()) {
382                        super.setText(Bundle.getMessage("BeanStateUnknown"));
383                    }
384                    if (isIcon()) {
385                        super.setIcon(unknown);
386                    }
387                    return;  // this trumps all others
388                case Sensor.INCONSISTENT:
389                    if (isText()) {
390                        super.setText(Bundle.getMessage("BeanStateInconsistent"));
391                    }
392                    if (isIcon()) {
393                        super.setIcon(inconsistent);
394                    }
395                    break;
396                default:
397                    break;
398            }
399        }
400        // loop has gotten to here
401        if (foundActive) {
402            return;  // set active
403        }        // only case left is all inactive
404        if (isText()) {
405            super.setText(Bundle.getMessage("SensorStateInactive"));
406        }
407        if (isIcon()) {
408            super.setIcon(inactive);
409        }
410    }
411
412    // Use largest size. If icons are not same size,
413    // this can result in drawing artifacts.
414    @Override
415    public int maxHeight() {
416        int size = Math.max(
417                ((inactive != null) ? inactive.getIconHeight() : 0),
418                Math.max((unknown != null) ? unknown.getIconHeight() : 0,
419                        (inconsistent != null) ? inconsistent.getIconHeight() : 0)
420        );
421        if (entries != null) {
422            for (Entry entry : entries) {
423                size = Math.max(size, entry.icon.getIconHeight());
424            }
425        }
426        return size;
427    }
428
429    // Use largest size. If icons are not same size,
430    // this can result in drawing artifacts.
431    @Override
432    public int maxWidth() {
433        int size = Math.max(
434                ((inactive != null) ? inactive.getIconWidth() : 0),
435                Math.max((unknown != null) ? unknown.getIconWidth() : 0,
436                        (inconsistent != null) ? inconsistent.getIconWidth() : 0)
437        );
438        if (entries != null) {
439            for (Entry entry : entries) {
440                size = Math.max(size, entry.icon.getIconWidth());
441            }
442        }
443        return size;
444    }
445
446    public void performMouseClicked(JmriMouseEvent e, int xx, int yy) {
447        if (log.isDebugEnabled()) {
448            log.debug("performMouseClicked: location ({}, {}), click from ({}, {}) displaying={}",
449                    getX(), getY(), xx, yy, displaying);
450        }
451        if (!buttonLive() || (entries == null || entries.size() < 1)) {
452            if (log.isDebugEnabled()) {
453                log.debug("performMouseClicked: buttonLive={}, entries={}", buttonLive(), entries.size());
454            }
455            return;
456        }
457
458        // find if we want to increment or decrement
459        // regardless of the zooming scale, (getX(), getY()) is the un-zoomed position in _editor._contents
460        // but the click is at the zoomed position
461        double ratio = _editor.getPaintScale();
462        boolean dec = false;
463        if (updown) {
464            if ((yy/ratio - getY()) > (double)(maxHeight()) / 2) {
465                dec = true;
466            }
467        } else {
468           if ((xx/ratio - getX()) < (double)(maxWidth()) / 2) {
469                dec = true;
470            }
471        }
472
473        // get new index
474        int next;
475        if (dec) {
476            next = displaying - 1;
477        } else {
478            next = displaying + 1;
479        }
480        if (next < 0) {
481            next = 0;
482        }
483        if (next >= entries.size()) {
484            next = entries.size() - 1;
485        }
486
487        int drop = displaying;
488        if (log.isDebugEnabled()) {
489            log.debug("dec= {} displaying={} next= {}", dec, displaying, next);
490        }
491        try {
492            entries.get(next).namedSensor.getBean().setKnownState(Sensor.ACTIVE);
493            if (drop >= 0 && drop != next) {
494                entries.get(drop).namedSensor.getBean().setKnownState(Sensor.INACTIVE);
495            }
496        } catch (jmri.JmriException ex) {
497            log.error("Click failed to set sensor: ", ex);
498        }
499    }
500
501    boolean buttonLive() {
502        return _editor.getFlag(Editor.OPTION_CONTROLS, isControlling());
503    }
504
505    @Override
506    public void doMouseClicked(JmriMouseEvent e) {
507        if (!e.isAltDown() && !e.isMetaDown()) {
508            performMouseClicked(e, e.getX(), e.getY());
509        }
510    }
511
512    @Override
513    public void dispose() {
514        // remove listeners
515        for (Entry entry : entries) {
516            entry.namedSensor.getBean().removePropertyChangeListener(this);
517        }
518        super.dispose();
519    }
520
521    static class Entry {
522
523        NamedBeanHandle<Sensor> namedSensor;
524        NamedIcon icon;
525    }
526
527    private static final Logger log = LoggerFactory.getLogger(MultiSensorIcon.class);
528
529}