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