001package jmri.jmrix.dccpp.swing.virtuallcd;
002
003import java.awt.*;
004import java.beans.PropertyChangeListener;
005import java.io.*;
006import java.util.List;
007import java.util.*;
008
009import javax.annotation.CheckForNull;
010import javax.swing.*;
011
012import jmri.jmrit.display.Positionable;
013import jmri.jmrix.ConnectionStatus;
014import jmri.jmrix.dccpp.*;
015import jmri.util.JmriJFrame;
016
017/**
018 * Panel to image the DCC-EX command station's OLED display.
019 * Also sends request to DCC-EX to send copies of all LCD messages to this
020 * instance of JMRI.
021 *
022 * @author Bob Jacobsen      Copyright (C) 2023
023 * @author M Steve Todd      Copyright (C) 2023
024 * @author Daniel Bergqvist  Copyright (C) 2026
025 */
026public class VirtualLCDPanel extends JPanel
027        implements DCCppListener, VirtualLCDConfiguration  {
028
029    private final boolean _isMemoEditable;
030    private final JmriJFrame _frame;
031    private final Positionable _positionable;
032    private DCCppTrafficController _tc;
033    private final PropertyChangeListener _listener;
034    private Font font;
035    private DCCppSystemConnectionMemo _memo;
036    private DisplayConfig _displayConfig = DisplayConfig.ConfigureVirtualLCD_AllDisplays;
037    private int displayNo = -1;
038    private int _minDisplayNo;
039    private int _maxDisplayNo;
040    private final Set<Integer> _selectedDisplays = new HashSet<>();
041    private Dimension lcdSize;
042
043    static final int TOTALLINES = 64;
044    private final Map<Integer, List<JLabel>> linesMap = new HashMap<>();
045    private final JLabel noVisibleDisplays = new JLabel(Bundle.getMessage("VirtualLcdNoVisibleDisplays"));
046
047    public VirtualLCDPanel(JmriJFrame frame, boolean isMemoEditable) {
048        this(frame, null, isMemoEditable);
049    }
050
051    public VirtualLCDPanel(JmriJFrame frame, Positionable pos, boolean isMemoEditable) {
052
053        _isMemoEditable = isMemoEditable;
054        _frame = frame;
055        _positionable = pos;
056
057        _listener = evt -> {
058            if (ConnectionStatus.CONNECTION_UP.equals(ConnectionStatus.instance().getConnectionState(_memo))) {
059                _tc.sendDCCppMessage(DCCppMessage.makeLCDRequestMsg(), null);
060            }
061        };
062    }
063
064    public void initComponents() {
065        _tc = _memo.getDCCppTrafficController();
066        _tc.addDCCppListener(DCCppInterface.CS_INFO, this);
067
068        _tc.sendDCCppMessage(DCCppMessage.makeLCDRequestMsg(), null);
069        ConnectionStatus.instance().addPropertyChangeListener(_memo, _listener);
070
071        setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
072
073        // load the custom 5x8 found
074        try {
075            InputStream stream = new FileInputStream(new File("resources/fonts/5x8_lcd_hd44780u_a02.ttf"));
076            font = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(16f).deriveFont(Font.BOLD);
077        } catch (IOException e1) { log.error("failed to find or open font file");
078        } catch (FontFormatException e2) { log.error("font file not valid");
079        }
080
081        noVisibleDisplays.setOpaque(true);
082        noVisibleDisplays.setBackground(Color.BLACK);
083        noVisibleDisplays.setForeground(new Color(255,63,63));    // Red
084        if (font != null) noVisibleDisplays.setFont(font);
085        add(noVisibleDisplays);
086        packOrResize();
087    }
088
089    public void reset() {
090        this.removeAll();
091        add(noVisibleDisplays);
092        packOrResize();
093        linesMap.clear();
094    }
095
096    public void dispose() {
097        ConnectionStatus.instance().removePropertyChangeListener(_memo, _listener);
098    }
099
100    @Override
101    public void setMemo(DCCppSystemConnectionMemo memo) {
102        ConnectionStatus.instance().removePropertyChangeListener(_memo, _listener);
103        if (_tc != null) {
104            _tc.removeDCCppListener(DCCppInterface.CS_INFO, this);
105        }
106        _memo = memo;
107        _tc = memo.getDCCppTrafficController();
108        _tc.addDCCppListener(DCCppInterface.CS_INFO, this);
109        _tc.sendDCCppMessage(DCCppMessage.makeLCDRequestMsg(), null);
110        ConnectionStatus.instance().addPropertyChangeListener(_memo, _listener);
111        reset();
112    }
113
114    @Override
115    public DCCppSystemConnectionMemo getMemo() {
116        return _memo;
117    }
118
119    @Override
120    public boolean isMemoEditable() {
121        return _isMemoEditable;
122    }
123
124    @Override
125    public void setDisplayConfig(DisplayConfig displayConfig) {
126        this._displayConfig = displayConfig;
127        reset();
128    }
129
130    @Override
131    public DisplayConfig getDisplayConfig() {
132        return _displayConfig;
133    }
134
135    @Override
136    public void setDisplayNo(int displayNo) {
137        this.displayNo = displayNo;
138        reset();
139    }
140
141    @Override
142    public int getDisplayNo() {
143        return displayNo;
144    }
145
146    @Override
147    public void setMinDisplayNo(int minDisplayNo) {
148        this._minDisplayNo = minDisplayNo;
149        reset();
150    }
151
152    @Override
153    public int getMinDisplayNo() {
154        return _minDisplayNo;
155    }
156
157    @Override
158    public void setMaxDisplayNo(int maxDisplayNo) {
159        this._maxDisplayNo = maxDisplayNo;
160        reset();
161    }
162
163    @Override
164    public int getMaxDisplayNo() {
165        return _maxDisplayNo;
166    }
167
168    @Override
169    public void setSelectedDisplays(Set<Integer> displays) {
170        _selectedDisplays.clear();
171        _selectedDisplays.addAll(displays);
172        reset();
173    }
174
175    @Override
176    public Set<Integer> getSelectedDisplays() {
177        return _selectedDisplays;
178    }
179
180    @Override
181    public void setLCDSize(@CheckForNull Dimension d) {
182        if (d != null && d.width > 0 && d.height > 0) {
183            lcdSize = d;
184        } else {
185            lcdSize = null;
186        }
187        reset();
188    }
189
190    @CheckForNull
191    @Override
192    public Dimension getLCDSize() {
193        return lcdSize;
194    }
195
196    public String getNameString() {
197        switch (_displayConfig) {
198            case ConfigureVirtualLCD_AllDisplays:
199                return Bundle.getMessage("VirtualLcdPositionable_AllDisplays");
200
201            case ConfigureVirtualLCD_OneDisplay:
202                return Bundle.getMessage(
203                        "VirtualLcdPositionable_OneDisplay", displayNo);
204
205            case ConfigureVirtualLCD_IntervalDisplay:
206                return Bundle.getMessage(
207                        "VirtualLcdPositionable_IntervalDisplay",
208                        _minDisplayNo, _maxDisplayNo);
209
210            case ConfigureVirtualLCD_SelectedDisplays:
211                StringBuilder sb = new StringBuilder();
212                for (int i : _selectedDisplays) {
213                    if (sb.length() > 0) {
214                        sb.append(",");
215                    }
216                    sb.append(i);
217                }
218                return Bundle.getMessage(
219                        "VirtualLcdPositionable_SelectedDisplays", sb.toString());
220
221            default:
222                throw new IllegalArgumentException("Unknown displayConfig: "+_displayConfig.name());
223        }
224    }
225
226    /**
227     * {@inheritDoc}
228     */
229    @Override
230    public void message(DCCppMessage msg) {
231    }
232
233    private List<JLabel> createNewDisplay() {
234        // noVisibleDisplays is shown until at least one display is added.
235        this.remove(noVisibleDisplays);
236
237        // Add space between displays if this is not the first display
238        if (getComponentCount() > 0) {
239            Component c = Box.createHorizontalStrut(10);
240            this.add(c);
241        }
242
243        var lines = new ArrayList<JLabel>(TOTALLINES + 1);
244        var pane = new JPanel();
245        pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS));
246        // initialize the list of display lines
247        for (int i = 0; i<TOTALLINES; i++) {
248            var label = new JLabel();
249            if (lcdSize != null && lines.size() < lcdSize.height) {
250                label.setText(cutIfNeeded(""));
251            }
252            if (font != null) label.setFont(font);
253            label.setOpaque(true);
254            label.setBackground(Color.BLACK);
255            label.setForeground(Color.WHITE);
256            lines.add(label);
257            pane.add(lines.get(i));
258        }
259        pane.setOpaque(true);
260        pane.setBackground(Color.BLACK);
261        pane.setAlignmentY(Component.TOP_ALIGNMENT);
262        this.add(pane);
263        packOrResize();
264        return lines;
265    }
266
267    private boolean showDisplay(int displayNumber) {
268        switch (_displayConfig) {
269            case ConfigureVirtualLCD_AllDisplays:
270                return true;
271
272            case ConfigureVirtualLCD_OneDisplay:
273                return displayNo == displayNumber;
274
275            case ConfigureVirtualLCD_IntervalDisplay:
276                return _minDisplayNo <= displayNumber && displayNumber <= _maxDisplayNo;
277
278            case ConfigureVirtualLCD_SelectedDisplays:
279                return _selectedDisplays.contains(displayNumber);
280
281            default:
282                throw new IllegalArgumentException("Unknown displayConfig: "+_displayConfig.name());
283        }
284    }
285
286    private String cutIfNeeded(String s) {
287        if (lcdSize != null) {
288            while (s.length() < lcdSize.width) {
289                s += " ";
290            }
291            if (s.length() > lcdSize.width) {
292                s = s.substring(0, lcdSize.width);
293            }
294        }
295        return s;
296    }
297
298    private void packOrResize() {
299        if (_positionable != null) {
300            var d = this.getPreferredSize();
301            this.setSize(d);
302            _positionable.setSize(d);
303        } else {
304            _frame.pack();
305        }
306    }
307
308    /**
309     * {@inheritDoc}
310     */
311    @Override
312    public void message(DCCppReply msg) {
313        if (msg.isLCDTextReply()) { // <@ display# line# "message text">
314            int displayNumber = msg.getLCDDisplayNumInt();
315
316            if (showDisplay(displayNumber)) {
317                var lines = linesMap.computeIfAbsent(displayNumber, display -> createNewDisplay());
318                int lineNumber = msg.getLCDLineNumInt();
319                if (lcdSize == null || lineNumber < lcdSize.height) {
320                    if (lineNumber < TOTALLINES) {
321                        lines.get(lineNumber).setText(cutIfNeeded(msg.getLCDTextString()+"   ")); // padding for appearance
322                        packOrResize();
323                    } else {
324                        log.warn("Received LCD message for line {}, but configured for TOTALLINES limit of {}",
325                                    lineNumber, TOTALLINES-1);
326                    }
327                }
328                log.debug("Received LCD message for display# {}.", displayNumber);
329            }
330        }
331    }
332
333    /**
334     * {@inheritDoc}
335     */
336    @Override
337    public void notifyTimeout(DCCppMessage msg) {
338    }
339
340    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VirtualLCDPanel.class);
341
342}