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