001package jmri.jmrit.display;
002
003import java.awt.event.ActionEvent;
004import java.awt.event.ActionListener;
005import java.util.ArrayList;
006import java.util.HashMap;
007import java.util.Map.Entry;
008import javax.annotation.Nonnull;
009import javax.swing.JPopupMenu;
010import jmri.InstanceManager;
011import jmri.NamedBeanHandle;
012import jmri.NamedBeanHandleManager;
013import jmri.Sensor;
014import jmri.jmrit.catalog.NamedIcon;
015import jmri.jmrit.display.palette.IndicatorItemPanel;
016import jmri.jmrit.logix.OBlock;
017import jmri.util.ThreadingUtil;
018
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021
022/**
023 * An icon to display the status of a track segment in a block.
024 * <p>
025 * This responds to the following conditions:
026 * <ol>
027 *   <li>KnownState of an occupancy sensor of the block where the track segment appears
028 *   <li>Allocation of a route by a Warrant where the track segment appears
029 *   <li>Current position of a train being run under a Warrant where the track segment
030 *   appears in a block of the route
031 *   <li>Out of Service for a block that cannot or should not be used
032 *   <li>An error state of the block where the track segment appears (short/no power
033 *   etc.)
034 * </ol>
035 * A click on the icon does not change any of the above conditions.
036 *
037 * @author Pete Cressman Copyright (c) 2010
038 */
039public class IndicatorTrackIcon extends PositionableIcon
040        implements java.beans.PropertyChangeListener, IndicatorTrack {
041
042    private NamedBeanHandle<Sensor> namedOccSensor = null;
043    private NamedBeanHandle<OBlock> namedOccBlock = null;
044
045    private IndicatorTrackPaths _pathUtil;
046    private IndicatorItemPanel _trackPanel;
047    private String _status;     // is a key for _iconMap
048
049    public IndicatorTrackIcon(Editor editor) {
050        // super ctor call to make sure this is an icon label
051        super(editor);
052        _pathUtil = new IndicatorTrackPaths();
053        _status = "ClearTrack";
054        _iconMap = new HashMap<>();
055    }
056
057    @Override
058    @Nonnull
059    public Positionable deepClone() {
060        IndicatorTrackIcon pos = new IndicatorTrackIcon(_editor);
061        return finishClone(pos);
062    }
063
064    protected Positionable finishClone(IndicatorTrackIcon pos) {
065        pos.setOccSensorHandle(namedOccSensor);
066        pos.setOccBlockHandle(namedOccBlock);
067        pos._iconMap = cloneMap(_iconMap, pos);
068        pos._pathUtil = _pathUtil.deepClone();
069        pos._iconFamily = _iconFamily;
070        pos._namedIcon = null;
071        pos._status = _status;
072        return super.finishClone(pos);
073    }
074
075    /**
076     * Attach a named sensor to display status.
077     *
078     * @param pName Used as a system/user name to lookup the sensor object
079     */
080    @Override
081    public void setOccSensor(String pName) {
082        if (pName == null || pName.trim().isEmpty()) {
083            setOccSensorHandle(null);
084            return;
085        }
086        if (InstanceManager.getNullableDefault(jmri.SensorManager.class) != null) {
087            try {
088                Sensor sensor = InstanceManager.sensorManagerInstance().provideSensor(pName);
089                setOccSensorHandle(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, sensor));
090            } catch (IllegalArgumentException ex) {
091                log.error("Occupancy Sensor '{}' not available, icon won't see changes", pName);
092            }
093        } else {
094            log.error("No SensorManager for this protocol, block icons won't see changes");
095        }
096    }
097
098    @Override
099    public void setOccSensorHandle(NamedBeanHandle<Sensor> senHandle) {
100        if (namedOccSensor != null) {
101            getOccSensor().removePropertyChangeListener(this);
102        }
103        namedOccSensor = senHandle;
104        if (namedOccSensor != null) {
105            if (_iconMap == null) {
106                _iconMap = new HashMap<>();
107            }
108            Sensor sensor = getOccSensor();
109            sensor.addPropertyChangeListener(this, namedOccSensor.getName(), "Indicator Track");
110            _status = _pathUtil.getStatus(sensor.getKnownState());
111            displayState(_status);
112        }
113    }
114
115    @Override
116    public Sensor getOccSensor() {
117        if (namedOccSensor == null) {
118            return null;
119        }
120        return namedOccSensor.getBean();
121    }
122
123    @Override
124    public NamedBeanHandle<Sensor> getNamedOccSensor() {
125        return namedOccSensor;
126    }
127
128    /**
129     * Attach a named OBlock to display status.
130     *
131     * @param pName Used as a system/user name to look up the OBlock object
132     */
133    @Override
134    public void setOccBlock(String pName) {
135        if (pName == null || pName.trim().isEmpty()) {
136            setOccBlockHandle(null);
137            return;
138        }
139        OBlock block = InstanceManager.getDefault(jmri.jmrit.logix.OBlockManager.class).getOBlock(pName);
140        if (block != null) {
141            setOccBlockHandle(InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(pName, block));
142        } else {
143            log.error("Detection OBlock '{}' not available, icon won't see changes", pName);
144        }
145    }
146
147    @Override
148    public void setOccBlockHandle(NamedBeanHandle<OBlock> blockHandle) {
149        if (namedOccBlock != null) {
150            getOccBlock().removePropertyChangeListener(this);
151        }
152        namedOccBlock = blockHandle;
153        if (namedOccBlock != null) {
154            if (_iconMap == null) {
155                _iconMap = new HashMap<>();
156            }
157            OBlock block = getOccBlock();
158            block.addPropertyChangeListener(this, namedOccBlock.getName(), "Indicator Track");
159            setStatus(block, block.getState());
160            displayState(_status);
161            setToolTip(new ToolTip(block.getDescription(), 0, 0, this));
162        } else {
163            setToolTip(new ToolTip(null, 0, 0, this));
164        }
165    }
166
167    @Override
168    public OBlock getOccBlock() {
169        if (namedOccBlock == null) {
170            return null;
171        }
172        return namedOccBlock.getBean();
173    }
174
175    @Override
176    public NamedBeanHandle<OBlock> getNamedOccBlock() {
177        return namedOccBlock;
178    }
179
180    @Override
181    public void setShowTrain(boolean set) {
182        _pathUtil.setShowTrain(set);
183    }
184
185    @Override
186    public boolean showTrain() {
187        return _pathUtil.showTrain();
188    }
189
190    @Override
191    public ArrayList<String> getPaths() {
192        return _pathUtil.getPaths();
193    }
194
195    public void setPaths(ArrayList<String> paths) {
196        _pathUtil.setPaths(paths);
197    }
198
199    @Override
200    public void addPath(String path) {
201        _pathUtil.addPath(path);
202    }
203
204    @Override
205    public void removePath(String path) {
206        _pathUtil.removePath(path);
207    }
208
209    /**
210     * Get track name for known state of occupancy sensor
211     */
212    @Override
213    public void setStatus(int state) {
214        _status = _pathUtil.getStatus(state);
215     }
216
217    /*
218     * Place icon by its bean state name
219     */
220    public void setIcon(String name, NamedIcon icon) {
221        log.debug("set \"{}\" icon= {}", name, icon);
222        _iconMap.put(name, icon);
223        if (_status.equals(name)) {
224            setIcon(icon);
225        }
226    }
227
228    public String getStatus() {
229        return _status;
230    }
231
232    @Override
233    public int maxHeight() {
234        if (_iconMap == null) {
235            return 0;
236        }
237        int max = 0;
238        for (NamedIcon namedIcon : _iconMap.values()) {
239            max = Math.max(namedIcon.getIconHeight(), max);
240        }
241        return max;
242    }
243
244    @Override
245    public int maxWidth() {
246        if (_iconMap == null) {
247            return 0;
248        }
249        int max = 0;
250        for (NamedIcon namedIcon : _iconMap.values()) {
251            max = Math.max(namedIcon.getIconWidth(), max);
252        }
253        return max;
254    }
255
256    @Override
257    public void propertyChange(java.beans.PropertyChangeEvent evt) {
258        if (log.isDebugEnabled()) {
259            log.debug("property change: {} property {} is now {} from {}", getNameString(), evt.getPropertyName(),
260                    evt.getNewValue(), evt.getSource().getClass().getName());
261        }
262
263        Object source = evt.getSource();
264        if (source instanceof OBlock) {
265            String property = evt.getPropertyName();
266            if ("state".equals(property) || "pathState".equals(property)) {
267                int now = ((Integer) evt.getNewValue());
268                setStatus((OBlock) source, now);
269            } else if ("pathName".equals(property)) {
270                _pathUtil.removePath((String) evt.getOldValue());
271                _pathUtil.addPath((String) evt.getNewValue());
272            }
273        } else if (source instanceof Sensor) {
274            if (evt.getPropertyName().equals("KnownState")) {
275                int now = ((Integer) evt.getNewValue());
276                if (source.equals(getOccSensor())) {
277                    _status = _pathUtil.getStatus(now);
278                }
279            }
280        }
281        displayState(_status);
282    }
283
284    private void setStatus(OBlock block, int state) {
285        _status = _pathUtil.getStatus(block, state);
286        if ((state & (OBlock.OCCUPIED | OBlock.RUNNING)) != 0) {
287            // _pathUtil.setLocoIcon must run on GUI. LocoLabel ctor causes editor to draw a graphic
288            ThreadingUtil.runOnLayoutEventually(() -> {
289                _pathUtil.setLocoIcon(block, getLocation(), getSize(), _editor);
290                repaint();
291            });
292        }
293        if ((block.getState() & OBlock.OUT_OF_SERVICE) != 0) {
294            setControlling(false);
295        } else {
296            setControlling(true);
297        }
298    }
299
300    @Override
301    @Nonnull
302    public String getTypeString() {
303        return Bundle.getMessage("PositionableType_IndicatorTrackIcon");
304    }
305
306    @Override
307    @Nonnull
308    public String getNameString() {
309        String str = "";
310        if (namedOccBlock != null) {
311            str = "in " + namedOccBlock.getBean().getDisplayName();
312        } else if (namedOccSensor != null) {
313            str = "on " + namedOccSensor.getBean().getDisplayName();
314        }
315        return "ITrack " + str;
316    }
317
318    /**
319     * Pop-up displays unique attributes.
320     */
321    @Override
322    public boolean showPopUp(JPopupMenu popup) {
323        return false;
324    }
325
326    /*
327     * Drive the current state of the display from the status.
328     */
329    public void displayState(String status) {
330        log.debug("{} displayStatus {}", getNameString(), _status);
331        NamedIcon icon = getIcon(status);
332        if (icon != null) {
333            super.setIcon(icon);
334        }
335        updateSize();
336    }
337
338    @Override
339    public void rotate(int deg) {
340        super.rotate(deg);
341        displayState(_status);
342    }
343
344    @Override
345    public boolean setEditItemMenu(JPopupMenu popup) {
346        String txt = java.text.MessageFormat.format(Bundle.getMessage("EditItem"), Bundle.getMessage("IndicatorTrack"));
347        popup.add(new javax.swing.AbstractAction(txt) {
348            @Override
349            public void actionPerformed(ActionEvent e) {
350                editItem();
351            }
352        });
353        return true;
354    }
355
356    protected void editItem() {
357        _paletteFrame = makePaletteFrame(java.text.MessageFormat.format(Bundle.getMessage("EditItem"),
358                Bundle.getMessage("IndicatorTrack")));
359        _trackPanel = new IndicatorItemPanel(_paletteFrame, "IndicatorTrack", _iconFamily);
360
361        ActionListener updateAction = a -> updateItem();
362        // duplicate _iconMap map with unscaled and unrotated icons
363        HashMap<String, NamedIcon> map = new HashMap<>();
364
365        for (Entry<String, NamedIcon> entry : _iconMap.entrySet()) {
366            NamedIcon oldIcon = entry.getValue();
367            NamedIcon newIcon = cloneIcon(oldIcon, this);
368            newIcon.rotate(0, this);
369            newIcon.scale(1.0, this);
370            newIcon.setRotation(4, this);
371            map.put(entry.getKey(), newIcon);
372        }
373        _trackPanel.init(updateAction, map);
374        if (namedOccSensor != null) {
375            _trackPanel.setOccDetector(namedOccSensor.getBean().getDisplayName());
376        }
377        if (namedOccBlock != null) {
378            _trackPanel.setOccDetector(namedOccBlock.getBean().getDisplayName());
379        }
380        _trackPanel.setShowTrainName(_pathUtil.showTrain());
381        _trackPanel.setPaths(_pathUtil.getPaths());
382        initPaletteFrame(_paletteFrame, _trackPanel);
383    }
384
385    private void updateItem() {
386        setOccSensor(_trackPanel.getOccSensor());
387        setOccBlock(_trackPanel.getOccBlock());
388        _pathUtil.setShowTrain(_trackPanel.getShowTrainName());
389        _iconFamily = _trackPanel.getFamilyName();
390        _pathUtil.setPaths(_trackPanel.getPaths());
391        HashMap<String, NamedIcon> iconMap = _trackPanel.getIconMap();
392        if (iconMap != null) {
393            HashMap<String, NamedIcon> oldMap = cloneMap(_iconMap, this);
394            for (Entry<String, NamedIcon> entry : _iconMap.entrySet()) {
395                if (log.isDebugEnabled()) {
396                    log.debug("key= {}", entry.getKey());
397                }
398                NamedIcon newIcon = entry.getValue();
399                NamedIcon oldIcon = oldMap.get(entry.getKey());
400                newIcon.setLoad(oldIcon.getDegrees(), oldIcon.getScale(), this);
401                newIcon.setRotation(oldIcon.getRotation(), this);
402                setIcon(entry.getKey(), newIcon);
403            }
404        }   // otherwise retain current map
405        finishItemUpdate(_paletteFrame, _trackPanel);
406        displayState(_status);
407    }
408
409    @Override
410    public void dispose() {
411        if (namedOccSensor != null) {
412            getOccSensor().removePropertyChangeListener(this);
413        }
414        namedOccSensor = null;
415        if (namedOccBlock != null) {
416            getOccBlock().removePropertyChangeListener(this);
417        }
418        namedOccBlock = null;
419        _iconMap = null;
420        super.dispose();
421    }
422
423    @Override
424    public jmri.NamedBean getNamedBean() {
425        if (namedOccBlock != null) {
426            return namedOccBlock.getBean();
427        } else if (namedOccSensor != null) {
428            return namedOccSensor.getBean();
429        }
430        return null;
431    }
432
433    private final static Logger log = LoggerFactory.getLogger(IndicatorTrackIcon.class);
434
435}