001package jmri.jmrit.display;
002
003import java.awt.event.ActionListener;
004import java.util.ArrayList;
005import java.util.HashMap;
006import java.util.Map.Entry;
007
008import javax.annotation.Nonnull;
009
010import jmri.InstanceManager;
011import jmri.NamedBeanHandle;
012import jmri.NamedBeanHandleManager;
013import jmri.Sensor;
014import jmri.Turnout;
015import jmri.jmrit.catalog.NamedIcon;
016import jmri.jmrit.display.palette.IndicatorTOItemPanel;
017import jmri.jmrit.logix.OBlock;
018import jmri.jmrit.picker.PickListModel;
019import jmri.util.ThreadingUtil;
020
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024/**
025 * An icon to display a status and state of a color coded turnout.<p>
026 * This responds to only KnownState, leaving CommandedState to some other
027 * graphic representation later.
028 * <p>
029 * "state" is the state of the underlying turnout ("closed", "thrown", etc.)
030 * <p>
031 * "status" is the operating condition of the track ("clear", "occupied", etc.)
032 * <p>
033 * A click on the icon will command a state change. Specifically, it will set
034 * the CommandedState to the opposite (THROWN vs CLOSED) of the current
035 * KnownState. This will display the setting of the turnout points.
036 * <p>
037 * The status is indicated by color and changes are done only done by the
038 * occupancy sensing - OBlock or other sensor.
039 * <p>
040 * The default icons are for a left-handed turnout, facing point for east-bound
041 * traffic.
042 *
043 * @author Bob Jacobsen Copyright (c) 2002
044 * @author Pete Cressman Copyright (c) 2010 2012
045 */
046public class IndicatorTurnoutIcon extends TurnoutIcon implements IndicatorTrack {
047
048    HashMap<String, HashMap<Integer, NamedIcon>> _iconMaps;
049
050    private NamedBeanHandle<Sensor> namedOccSensor = null;
051    private NamedBeanHandle<OBlock> namedOccBlock = null;
052
053    private IndicatorTrackPaths _pathUtil;
054    private IndicatorTOItemPanel _itemPanel;
055    private String _status;
056
057    public IndicatorTurnoutIcon(Editor editor) {
058        super(editor);
059        log.debug("IndicatorTurnoutIcon ctor: isIcon()= {}, isText()= {}", isIcon(), isText());
060        _pathUtil = new IndicatorTrackPaths();
061        _status = "DontUseTrack";
062        _iconMaps = initMaps();
063
064    }
065
066    static HashMap<String, HashMap<Integer, NamedIcon>> initMaps() {
067        HashMap<String, HashMap<Integer, NamedIcon>> iconMaps = new HashMap<>();
068        iconMaps.put("ClearTrack", new HashMap<>());
069        iconMaps.put("OccupiedTrack", new HashMap<>());
070        iconMaps.put("PositionTrack", new HashMap<>());
071        iconMaps.put("AllocatedTrack", new HashMap<>());
072        iconMaps.put("DontUseTrack", new HashMap<>());
073        iconMaps.put("ErrorTrack", new HashMap<>());
074        return iconMaps;
075    }
076
077    HashMap<String, HashMap<Integer, NamedIcon>> cloneMaps(IndicatorTurnoutIcon pos) {
078        HashMap<String, HashMap<Integer, NamedIcon>> iconMaps = initMaps();
079        for (Entry<String, HashMap<Integer, NamedIcon>> entry : _iconMaps.entrySet()) {
080            HashMap<Integer, NamedIcon> clone = iconMaps.get(entry.getKey());
081            for (Entry<Integer, NamedIcon> ent : entry.getValue().entrySet()) {
082                //                if (log.isDebugEnabled()) log.debug("key= "+ent.getKey());
083                clone.put(ent.getKey(), cloneIcon(ent.getValue(), pos));
084            }
085        }
086        return iconMaps;
087    }
088
089    @Override
090    public Positionable deepClone() {
091        IndicatorTurnoutIcon pos = new IndicatorTurnoutIcon(_editor);
092        return finishClone(pos);
093    }
094
095    protected Positionable finishClone(IndicatorTurnoutIcon pos) {
096        pos.setOccBlockHandle(namedOccBlock);
097        pos.setOccSensorHandle(namedOccSensor);
098        pos._iconMaps = cloneMaps(pos);
099        pos._pathUtil = _pathUtil.deepClone();
100        pos._iconFamily = _iconFamily;
101        return super.finishClone(pos);
102    }
103
104    public HashMap<String, HashMap<Integer, NamedIcon>> getIconMaps() {
105        return new HashMap<>(_iconMaps);
106    }
107
108    /**
109     * Attached a named sensor to display status from OBlocks
110     *
111     * @param pName Used as a system/user name to lookup the sensor object
112     */
113    @Override
114    public void setOccSensor(String pName) {
115        if (pName == null || pName.trim().length() == 0) {
116            setOccSensorHandle(null);
117            return;
118        }
119        if (InstanceManager.getNullableDefault(jmri.SensorManager.class) != null) {
120            try {
121                Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName);
122                setOccSensorHandle(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, sensor));
123            } catch (IllegalArgumentException ex) {
124                log.error("Occupancy Sensor '{}' not available, icon won't see changes", pName);
125            }
126        } else {
127            log.error("No SensorManager for this protocol, block icons won't see changes");
128        }
129    }
130
131    @Override
132    public void setOccSensorHandle(NamedBeanHandle<Sensor> sen) {
133        if (namedOccSensor != null) {
134            getOccSensor().removePropertyChangeListener(this);
135        }
136        namedOccSensor = sen;
137        if (namedOccSensor != null) {
138            Sensor sensor = getOccSensor();
139            sensor.addPropertyChangeListener(this, namedOccSensor.getName(), "Indicator Turnout Icon");
140            _status = _pathUtil.getStatus(sensor.getKnownState());
141            if (_iconMaps != null) {
142                displayState(turnoutState());
143            }
144        }
145    }
146
147    @Override
148    public Sensor getOccSensor() {
149        if (namedOccSensor == null) {
150            return null;
151        }
152        return namedOccSensor.getBean();
153    }
154
155    @Override
156    public NamedBeanHandle<Sensor> getNamedOccSensor() {
157        return namedOccSensor;
158    }
159
160    /**
161     * Attached a named OBlock to display status
162     *
163     * @param pName Used as a system/user name to lookup the OBlock object
164     */
165    @Override
166    public void setOccBlock(String pName) {
167        if (pName == null || pName.trim().length() == 0) {
168            setOccBlockHandle(null);
169            return;
170        }
171        OBlock block = InstanceManager.getDefault(jmri.jmrit.logix.OBlockManager.class).getOBlock(pName);
172        if (block != null) {
173            setOccBlockHandle(InstanceManager.getDefault(NamedBeanHandleManager.class)
174                    .getNamedBeanHandle(pName, block));
175        } else {
176            log.error("Detection OBlock '{}' not available, icon won't see changes", pName);
177        }
178    }
179
180    @Override
181    public void setOccBlockHandle(NamedBeanHandle<OBlock> blockHandle) {
182        if (namedOccBlock != null) {
183            getOccBlock().removePropertyChangeListener(this);
184        }
185        namedOccBlock = blockHandle;
186        if (namedOccBlock != null) {
187            OBlock block = getOccBlock();
188            block.addPropertyChangeListener(this, namedOccBlock.getName(), "Indicator Turnout Icon");
189            setStatus(block, block.getState());
190            if (_iconMaps != null) {
191                displayState(turnoutState());
192            }
193            setToolTip(new ToolTip(block.getDescription(), 0, 0, this));
194        } else {
195            setToolTip(new ToolTip(null, 0, 0, this));
196        }
197    }
198
199    @Override
200    public OBlock getOccBlock() {
201        if (namedOccBlock == null) {
202            return null;
203        }
204        return namedOccBlock.getBean();
205    }
206
207    @Override
208    public NamedBeanHandle<OBlock> getNamedOccBlock() {
209        return namedOccBlock;
210    }
211
212    @Override
213    public void setShowTrain(boolean set) {
214        _pathUtil.setShowTrain(set);
215    }
216
217    @Override
218    public boolean showTrain() {
219        return _pathUtil.showTrain();
220    }
221
222    @Override
223    public ArrayList<String> getPaths() {
224        return _pathUtil.getPaths();
225    }
226
227    public void setPaths(ArrayList<String> paths) {
228        _pathUtil.setPaths(paths);
229    }
230
231    @Override
232    public void addPath(String path) {
233        _pathUtil.addPath(path);
234    }
235
236    @Override
237    public void removePath(String path) {
238        _pathUtil.removePath(path);
239    }
240
241    /**
242     * get track name for known state of occupancy sensor
243     */
244    @Override
245    public void setStatus(int state) {
246        _status = _pathUtil.getStatus(state);
247    }
248
249    /**
250     * Place icon by its localized bean state name
251     *
252     * @param status     the track condition of the icon
253     * @param stateName  NamedBean name of turnout state
254     * @param icon       icon corresponding to status and state
255     */
256    public void setIcon(String status, String stateName, NamedIcon icon) {
257        if (log.isDebugEnabled()) {
258            log.debug("setIcon for status \"{}\", stateName= \"{} icom= {}", status, stateName, icon.getURL());
259        }
260//                                            ") state= "+_name2stateMap.get(stateName)+
261//                                            " icon: w= "+icon.getIconWidth()+" h= "+icon.getIconHeight());
262        if (_iconMaps == null) {
263            _iconMaps = initMaps();
264        }
265        _iconMaps.get(status).put(_name2stateMap.get(stateName), icon);
266        setIcon(_iconMaps.get("ClearTrack").get(_name2stateMap.get("BeanStateInconsistent")));
267    }
268
269    /*
270     * Get icon by its localized bean state name
271     */
272    public NamedIcon getIcon(String status, int state) {
273        log.debug("getIcon: status= {}, state= {}", status, state);
274        HashMap<Integer, NamedIcon> map = _iconMaps.get(status);
275        if (map == null) {
276            return null;
277        }
278        return map.get(state);
279    }
280
281    public String getStateName(Integer state) {
282        return _state2nameMap.get(state);
283    }
284
285    public String getStatus() {
286        return _status;
287    }
288
289    @Override
290    public int maxHeight() {
291        int max = 0;
292        if (_iconMaps != null) {
293            for (HashMap<Integer, NamedIcon> integerNamedIconHashMap : _iconMaps.values()) {
294                for (NamedIcon namedIcon : integerNamedIconHashMap.values()) {
295                    max = Math.max(namedIcon.getIconHeight(), max);
296                }
297            }
298        }
299        return max;
300    }
301
302    @Override
303    public int maxWidth() {
304        int max = 0;
305        if (_iconMaps != null) {
306            for (HashMap<Integer, NamedIcon> integerNamedIconHashMap : _iconMaps.values()) {
307                for (NamedIcon namedIcon : integerNamedIconHashMap.values()) {
308                    max = Math.max(namedIcon.getIconWidth(), max);
309                }
310            }
311        }
312        return max;
313    }
314
315    /**
316     * ****** popup AbstractAction.actionPerformed method overrides ********
317     */
318    @Override
319    protected void rotateOrthogonal() {
320        if (_iconMaps != null) {
321            for (HashMap<Integer, NamedIcon> integerNamedIconHashMap : _iconMaps.values()) {
322                for (NamedIcon icon : integerNamedIconHashMap.values()) {
323                    icon.setRotation(icon.getRotation() + 1, this);
324                }
325            }
326        }
327        displayState(turnoutState());
328    }
329
330    @Override
331    public void setScale(double s) {
332        _scale = s;
333        if (_iconMaps != null) {
334            for (HashMap<Integer, NamedIcon> integerNamedIconHashMap : _iconMaps.values()) {
335                for (NamedIcon namedIcon : integerNamedIconHashMap.values()) {
336                    namedIcon.scale(s, this);
337                }
338            }
339        }
340        displayState(turnoutState());
341    }
342
343    @Override
344    public void rotate(int deg) {
345        if (_iconMaps != null) {
346            for (HashMap<Integer, NamedIcon> integerNamedIconHashMap : _iconMaps.values()) {
347                for (NamedIcon namedIcon : integerNamedIconHashMap.values()) {
348                    namedIcon.rotate(deg, this);
349                }
350            }
351        }
352        setDegrees(deg %360);
353        displayState(turnoutState());
354    }
355
356    /**
357     * Drive the current state of the display from the state of the turnout and
358     * status of track.
359     */
360    @Override
361    public void displayState(int state) {
362        if (getNamedTurnout() == null) {
363            log.debug("Display state {}, disconnected", state);
364        } else {
365            if (_status != null && _iconMaps != null) {
366                NamedIcon icon = getIcon(_status, state);
367                if (icon != null) {
368                    super.setIcon(icon);
369                }
370            }
371        }
372        super.displayState(state);
373        updateSize();
374    }
375
376    @Override
377    @Nonnull
378    public String getTypeString() {
379        return Bundle.getMessage("PositionableType_IndicatorTurnoutIcon");
380    }
381
382    @Override
383    public String getNameString() {
384        String str = "";
385        if (namedOccBlock != null) {
386            str = " in " + namedOccBlock.getBean().getDisplayName();
387        } else if (namedOccSensor != null) {
388            str = " on " + namedOccSensor.getBean().getDisplayName();
389        }
390        return "ITrack " + super.getNameString() + str;
391    }
392
393    // update icon as state of turnout changes and status of track changes
394    // Override
395    @Override
396    public void propertyChange(java.beans.PropertyChangeEvent evt) {
397        if (log.isDebugEnabled()) {
398            log.debug("property change: {} property \"{}\"= {} from {}", getNameString(), evt.getPropertyName(), evt.getNewValue(), evt.getSource().getClass().getName());
399        }
400
401        Object source = evt.getSource();
402        if (source instanceof Turnout) {
403            super.propertyChange(evt);
404        } else if (source instanceof OBlock) {
405            String property = evt.getPropertyName();
406            if ("state".equals(property) || "pathState".equals(property)) {
407                int now = (Integer) evt.getNewValue();
408                setStatus((OBlock) source, now);
409            } else if ("pathName".equals(property)) {
410                _pathUtil.removePath((String) evt.getOldValue());
411                _pathUtil.addPath((String) evt.getNewValue());
412            }
413        } else if (source instanceof Sensor) {
414            if (evt.getPropertyName().equals("KnownState")) {
415                int now = (Integer) evt.getNewValue();
416                if (source.equals(getOccSensor())) {
417                    _status = _pathUtil.getStatus(now);
418                }
419            }
420        }
421        displayState(turnoutState());
422    }
423
424    private void setStatus(OBlock block, int state) {
425        _status = _pathUtil.getStatus(block, state);
426        log.debug("setStatus _status= {} state= {} block= \"{}\"", _status, state, block.getDisplayName());
427        if ((state & (OBlock.OCCUPIED | OBlock.RUNNING)) != 0) {
428            ThreadingUtil.runOnLayoutEventually(() -> {
429                _pathUtil.setLocoIcon(block, getLocation(), getSize(), _editor);
430                repaint();
431            });
432        }
433        if ((block.getState() & OBlock.OUT_OF_SERVICE) != 0) {
434            setControlling(false);
435        } else {
436            setControlling(true);
437        }
438    }
439
440    @Override
441    protected void editItem() {
442        _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("IndicatorTO")));
443        _itemPanel = new IndicatorTOItemPanel(_paletteFrame, "IndicatorTO", _iconFamily,
444                PickListModel.turnoutPickModelInstance());
445        ActionListener updateAction = a -> updateItem();
446        // Convert _iconMaps state (ints) to Palette's bean names
447        HashMap<String, HashMap<String, NamedIcon>> iconMaps
448                = new HashMap<>();
449        iconMaps.put("ClearTrack", new HashMap<>());
450        iconMaps.put("OccupiedTrack", new HashMap<>());
451        iconMaps.put("PositionTrack", new HashMap<>());
452        iconMaps.put("AllocatedTrack", new HashMap<>());
453        iconMaps.put("DontUseTrack", new HashMap<>());
454        iconMaps.put("ErrorTrack", new HashMap<>());
455        for (Entry<String, HashMap<Integer, NamedIcon>> entry : _iconMaps.entrySet()) {
456            HashMap<String, NamedIcon> clone = iconMaps.get(entry.getKey());
457            for (Entry<Integer, NamedIcon> ent : entry.getValue().entrySet()) {
458                NamedIcon oldIcon = ent.getValue();
459                NamedIcon newIcon = cloneIcon(oldIcon, this);
460                newIcon.rotate(0, this);
461                newIcon.scale(1.0, this);
462                newIcon.setRotation(4, this);
463                clone.put(_state2nameMap.get(ent.getKey()), newIcon);
464            }
465        }
466        _itemPanel.initUpdate(updateAction, iconMaps);
467
468        if (namedOccSensor != null) {
469            _itemPanel.setOccDetector(namedOccSensor.getBean().getDisplayName());
470        }
471        if (namedOccBlock != null) {
472            _itemPanel.setOccDetector(namedOccBlock.getBean().getDisplayName());
473        }
474        _itemPanel.setShowTrainName(_pathUtil.showTrain());
475        _itemPanel.setPaths(_pathUtil.getPaths());
476        _itemPanel.setSelection(getTurnout());  // do after all other params set - calls resize()
477
478        initPaletteFrame(_paletteFrame, _itemPanel);
479    }
480
481    @Override
482    void updateItem() {
483        if (log.isDebugEnabled()) {
484            log.debug("updateItem: {} family= {}", getNameString(), _itemPanel.getFamilyName());
485        }
486        setTurnout(_itemPanel.getTableSelection().getSystemName());
487        setOccSensor(_itemPanel.getOccSensor());
488        setOccBlock(_itemPanel.getOccBlock());
489        _pathUtil.setShowTrain(_itemPanel.getShowTrainName());
490        _iconFamily = _itemPanel.getFamilyName();
491        _pathUtil.setPaths(_itemPanel.getPaths());
492        HashMap<String, HashMap<String, NamedIcon>> iconMap = _itemPanel.getIconMaps();
493        if (iconMap != null) {
494            for (Entry<String, HashMap<String, NamedIcon>> entry : iconMap.entrySet()) {
495                String status = entry.getKey();
496                HashMap<Integer, NamedIcon> oldMap = _iconMaps.get(entry.getKey());
497                for (Entry<String, NamedIcon> ent : entry.getValue().entrySet()) {
498                    if (log.isDebugEnabled()) {
499                        log.debug("key= {}", ent.getKey());
500                    }
501                    NamedIcon newIcon = cloneIcon(ent.getValue(), this);
502                    NamedIcon oldIcon = oldMap.get(_name2stateMap.get(ent.getKey()));
503                    newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
504                    newIcon.setRotation(oldIcon.getRotation(), this);
505                    setIcon(status, ent.getKey(), newIcon);
506                }
507            }
508        }   // otherwise retain current map
509        finishItemUpdate(_paletteFrame, _itemPanel);
510        displayState(turnoutState());
511    }
512
513    @Override
514    public void dispose() {
515        if (namedOccSensor != null) {
516            getOccSensor().removePropertyChangeListener(this);
517        }
518        if (namedOccBlock != null) {
519            getOccBlock().removePropertyChangeListener(this);
520        }
521        namedOccSensor = null;
522        super.dispose();
523    }
524
525    private final static Logger log = LoggerFactory.getLogger(IndicatorTurnoutIcon.class);
526}