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