001package jmri.jmrit.beantable.block;
002
003import java.awt.*;
004import java.awt.event.MouseAdapter;
005import java.awt.event.MouseEvent;
006import java.awt.image.BufferedImage;
007
008import java.beans.PropertyChangeEvent;
009
010import java.io.File;
011import java.io.IOException;
012
013import java.text.DecimalFormat;
014
015import java.util.Arrays;
016import java.util.Set;
017import java.util.Vector;
018
019import javax.annotation.Nonnull;
020import javax.imageio.ImageIO;
021import javax.swing.*;
022import javax.swing.table.TableCellEditor;
023import javax.swing.table.TableCellRenderer;
024
025import jmri.*;
026import jmri.implementation.SignalSpeedMap;
027import jmri.jmrit.beantable.*;
028import jmri.jmrit.beantable.beanedit.BlockEditAction;
029import jmri.util.gui.GuiLafPreferencesManager;
030import jmri.util.swing.JComboBoxUtil;
031import jmri.util.swing.JmriJOptionPane;
032
033/**
034 * Data model for a Block Table.
035 * Code originally within BlockTableAction.
036 *
037 * @author Bob Jacobsen Copyright (C) 2003, 2008
038 * @author Egbert Broerse Copyright (C) 2017
039 * @author Steve Young Copyright (C) 2021
040 */
041public class BlockTableDataModel extends BeanTableDataModel<Block> {
042
043    static public final int EDITCOL = BeanTableDataModel.NUMCOLUMN;
044    static public final int DIRECTIONCOL = EDITCOL + 1;
045    static public final int LENGTHCOL = DIRECTIONCOL + 1;
046    static public final int CURVECOL = LENGTHCOL + 1;
047    static public final int STATECOL = CURVECOL + 1;
048    static public final int SENSORCOL = STATECOL + 1;
049    static public final int REPORTERCOL = SENSORCOL + 1;
050    static public final int CURRENTREPCOL = REPORTERCOL + 1;
051    static public final int PERMISCOL = CURRENTREPCOL + 1;
052    static public final int SPEEDCOL = PERMISCOL + 1;
053
054    private final boolean _graphicState = InstanceManager.getDefault(GuiLafPreferencesManager.class).isGraphicTableState();
055
056    private final DecimalFormat twoDigit = new DecimalFormat("0.00");
057
058    private Vector<String> speedList = new Vector<>();
059    private String[] sensorList;
060    private String[] reporterList;
061
062    String defaultBlockSpeedText;
063
064    public BlockTableDataModel(Manager<Block> mgr){
065        super();
066        setManager(mgr); // for consistency with other BeanTableModels, default BlockManager always used.
067
068        defaultBlockSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + InstanceManager.getDefault(BlockManager.class).getDefaultSpeed()); // first entry in drop down list
069        speedList.add(defaultBlockSpeedText);
070        Vector<String> _speedMap = InstanceManager.getDefault(SignalSpeedMap.class).getValidSpeedNames();
071        for (int i = 0; i < _speedMap.size(); i++) {
072            if (!speedList.contains(_speedMap.get(i))) {
073                speedList.add(_speedMap.get(i));
074            }
075        }
076
077        updateSensorList();
078        updateReporterList();
079    }
080
081    @Override
082    public String getValue(String name) {
083        if (name == null) {
084            log.warn("requested getValue(null)");
085            return "(no name)";
086        }
087        Block b = InstanceManager.getDefault(BlockManager.class).getBySystemName(name);
088        if (b == null) {
089            log.debug("requested getValue(\"{}\"), Block doesn't exist", name);
090            return "(no Block)";
091        }
092        Object m = b.getValue();
093        if (m != null) {
094            if ( m instanceof Reportable) {
095                return ((Reportable) m).toReportString();
096            }
097            else {
098                return m.toString();
099            }
100        } else {
101            return "";
102        }
103    }
104
105    @Override
106    public Manager<Block> getManager() {
107        return InstanceManager.getDefault(BlockManager.class);
108    }
109
110    @Override
111    public Block getBySystemName(@Nonnull String name) {
112        return InstanceManager.getDefault(BlockManager.class).getBySystemName(name);
113    }
114
115    @Override
116    public Block getByUserName(@Nonnull String name) {
117        return InstanceManager.getDefault(BlockManager.class).getByUserName(name);
118    }
119
120    @Override
121    protected String getMasterClassName() {
122        return BlockTableAction.class.getName();
123    }
124
125    @Override
126    public void clickOn(Block t) {
127        // don't do anything on click; not used in this class, because
128        // we override setValueAt
129    }
130
131    @Override
132    public int getColumnCount() {
133        return SPEEDCOL + 1;
134    }
135
136    @Override
137    public Object getValueAt(int row, int col) {
138        // some error checking
139        if (row >= sysNameList.size()) {
140            log.error("requested getValueAt(\"{}\"), row outside of range", row);
141            return "Error table size";
142        }
143        Block b = getBySystemName(sysNameList.get(row));
144        if (b == null) {
145            log.error("requested getValueAt(\"{}\"), Block doesn't exist", row);
146            return "(no Block)";
147        }
148        switch (col) {
149            case DIRECTIONCOL:
150                return Path.decodeDirection(b.getDirection());
151            case CURVECOL:
152                BlockCurvatureJComboBox box = new BlockCurvatureJComboBox(b.getCurvature());
153                box.setJTableCellClientProperties();
154                return box;
155            case LENGTHCOL:
156                return (twoDigit.format(metricUi ?  b.getLengthCm() : b.getLengthIn()));
157            case PERMISCOL:
158                return b.getPermissiveWorking();
159            case SPEEDCOL:
160                String speed = b.getBlockSpeed();
161                if (!speedList.contains(speed)) {
162                    speedList.add(speed);
163                }
164                JComboBox<String> c = new JComboBox<>(speedList);
165                c.setEditable(true);
166                c.setSelectedItem(speed);
167                JComboBoxUtil.setupComboBoxMaxRows(c);
168                return c;
169            case STATECOL:
170                return blockDescribeState(b.getState());
171            case SENSORCOL:
172                Sensor sensor = b.getSensor();
173                JComboBox<String> cs = new JComboBox<>(sensorList);
174                String name = "";
175                if (sensor != null) {
176                    name = sensor.getDisplayName();
177                }
178                cs.setSelectedItem(name);
179                JComboBoxUtil.setupComboBoxMaxRows(cs);
180                return cs;
181            case REPORTERCOL:
182                Reporter reporter = b.getReporter();
183                JComboBox<String> rs = new JComboBox<>(reporterList);
184                String repname = "";
185                if (reporter != null) {
186                    repname = reporter.getDisplayName();
187                }
188                rs.setSelectedItem(repname);
189                JComboBoxUtil.setupComboBoxMaxRows(rs);
190                return rs;
191            case CURRENTREPCOL:
192                return b.isReportingCurrent();
193            case EDITCOL:
194                return Bundle.getMessage("ButtonEdit");
195            default:
196                return super.getValueAt(row, col);
197        }
198    }
199
200    // TODO : Add Block.UNDETECTED
201    // TODO : Move to Block.describeState(int)
202    private String blockDescribeState(int blockState){
203        switch (blockState) {
204            case (Block.OCCUPIED):
205                return Bundle.getMessage("BlockOccupied");
206            case (Block.UNOCCUPIED):
207                return Bundle.getMessage("BlockUnOccupied");
208            case (Block.UNKNOWN):
209                return Bundle.getMessage("BlockUnknown");
210            default:
211                return Bundle.getMessage("BlockInconsistent");
212        }
213    }
214
215    @Override
216    public void setValueAt(Object value, int row, int col) {
217        // no setting of block state from table
218        Block b = getBySystemName(sysNameList.get(row));
219        switch (col) {
220            case VALUECOL:
221                b.setValue(value);
222                break;
223            case LENGTHCOL:
224                float len = 0.0f;
225                try {
226                    len = jmri.util.IntlUtilities.floatValue(value.toString());
227                } catch (java.text.ParseException ex2) {
228                    log.error("Error parsing length value of \"{}\"", value);
229                }   // block setLength() expecting value in mm, TODO: unit testing around this.
230                b.setLength( metricUi ? len * 10.0f : len * 25.4f);
231                break;
232            case CURVECOL:
233                b.setCurvature(BlockCurvatureJComboBox.getCurvatureFromObject(value));
234                break;
235            case PERMISCOL:
236                b.setPermissiveWorking((Boolean) value);
237                break;
238            case SPEEDCOL:
239                @SuppressWarnings("unchecked")
240                String speed = (String) ((JComboBox<String>) value).getSelectedItem();
241                try {
242                    b.setBlockSpeed(speed);
243                } catch (JmriException ex) {
244                    JmriJOptionPane.showMessageDialog(null, ex.getMessage() + "\n" + speed);
245                    return;
246                }
247                if (!speedList.contains(speed) && !speed.contains("Global")) { // NOI18N
248                    speedList.add(speed);
249                }
250                break;
251            case REPORTERCOL:
252                @SuppressWarnings("unchecked")
253                String strReporter = (String) ((JComboBox<String>) value).getSelectedItem();
254                Reporter r = InstanceManager.getDefault(ReporterManager.class).getReporter(strReporter);
255                b.setReporter(r);
256                break;
257            case SENSORCOL:
258                @SuppressWarnings("unchecked")
259                String strSensor = (String) ((JComboBox<String>) value).getSelectedItem();
260                b.setSensor(strSensor);
261                break;
262            case CURRENTREPCOL:
263                b.setReportingCurrent((Boolean) value);
264                break;
265            case EDITCOL:
266                javax.swing.SwingUtilities.invokeLater(() -> {
267                    editButton(b);
268                });
269                break;
270            default:
271                super.setValueAt(value, row, col);
272                break;
273        }
274    }
275
276    @Override
277    public String getColumnName(int col) {
278        switch (col) {
279            case DIRECTIONCOL:
280                return Bundle.getMessage("BlockDirection");
281            case VALUECOL:
282                return Bundle.getMessage("BlockValue");
283            case CURVECOL:
284                return Bundle.getMessage("BlockCurveColName");
285            case LENGTHCOL:
286                return Bundle.getMessage("BlockLengthColName");
287            case PERMISCOL:
288                return Bundle.getMessage("BlockPermColName");
289            case SPEEDCOL:
290                return Bundle.getMessage("BlockSpeedColName");
291            case STATECOL:
292                return Bundle.getMessage("BlockState");
293            case REPORTERCOL:
294                return Bundle.getMessage("BlockReporter");
295            case SENSORCOL:
296                return Bundle.getMessage("BlockSensor");
297            case CURRENTREPCOL:
298                return Bundle.getMessage("BlockReporterCurrent");
299            case EDITCOL:
300                return Bundle.getMessage("ButtonEdit");
301            default:
302                return super.getColumnName(col);
303        }
304    }
305
306    @Override
307    public Class<?> getColumnClass(int col) {
308        switch (col) {
309            case DIRECTIONCOL:
310            case VALUECOL: // not a button
311            case LENGTHCOL:
312                return String.class;
313            case STATECOL: // may use an image to show block state
314                if (_graphicState) {
315                    return JLabel.class;
316                } else {
317                    return String.class;
318                }
319            case SPEEDCOL:
320            case CURVECOL:
321            case REPORTERCOL:
322            case SENSORCOL:
323                return JComboBox.class;
324            case CURRENTREPCOL:
325            case PERMISCOL:
326                return Boolean.class;
327            case EDITCOL:
328                return JButton.class;
329            default:
330                return super.getColumnClass(col);
331        }
332    }
333
334    @Override
335    public int getPreferredWidth(int col) {
336        switch (col) {
337            case DIRECTIONCOL:
338            case LENGTHCOL:
339            case PERMISCOL:
340            case SPEEDCOL:
341            case CURRENTREPCOL:
342            case EDITCOL:
343                return new JTextField(7).getPreferredSize().width;
344            case CURVECOL:
345            case STATECOL:
346            case REPORTERCOL:
347            case SENSORCOL:
348                return new JTextField(8).getPreferredSize().width;
349            default:
350                return super.getPreferredWidth(col);
351        }
352    }
353
354    @Override
355    public void configValueColumn(JTable table) {
356        // value column isn't button, so config is null
357    }
358
359    @Override
360    public boolean isCellEditable(int row, int col) {
361        switch (col) {
362            case CURVECOL:
363            case LENGTHCOL:
364            case PERMISCOL:
365            case SPEEDCOL:
366            case REPORTERCOL:
367            case SENSORCOL:
368            case CURRENTREPCOL:
369            case EDITCOL:
370                return true;
371            case STATECOL:
372                return false;
373            default:
374                return super.isCellEditable(row, col);
375        }
376    }
377
378    @Override
379    public void configureTable(JTable table) {
380        InstanceManager.sensorManagerInstance().addPropertyChangeListener(this);
381        InstanceManager.getDefault(ReporterManager.class).addPropertyChangeListener(this);
382        configStateColumn(table);
383        super.configureTable(table);
384    }
385
386    void editButton(Block b) {
387        BlockEditAction beanEdit = new BlockEditAction();
388        beanEdit.setBean(b);
389        beanEdit.actionPerformed(null);
390    }
391
392    /**
393     * returns true for all Block properties.
394     * @param e property event that has changed.
395     * @return true as all matched.
396     */
397    @Override
398    protected boolean matchPropertyName(PropertyChangeEvent e) {
399        return true;
400    }
401
402    @Override
403    public JButton configureButton() {
404        log.error("configureButton should not have been called");
405        return null;
406    }
407
408    @Override
409    public void propertyChange(PropertyChangeEvent e) {
410        if (e.getSource() instanceof SensorManager) {
411            if (e.getPropertyName().equals("length") || e.getPropertyName().equals("DisplayListName")) { // NOI18N
412                updateSensorList();
413            }
414        }
415        if (e.getSource() instanceof ReporterManager) {
416            if (e.getPropertyName().equals("length") || e.getPropertyName().equals("DisplayListName")) { // NOI18N
417                updateReporterList();
418            }
419        }
420        if (e.getPropertyName().equals("DefaultBlockSpeedChange")) { // NOI18N
421            updateSpeedList();
422        } else {
423            super.propertyChange(e);
424        }
425    }
426
427    private boolean metricUi = InstanceManager.getDefault(UserPreferencesManager.class)
428        .getSimplePreferenceState(BlockTableAction.BLOCK_METRIC_PREF);
429
430    /**
431     * Set and refresh the UI to use Metric or Imperial values.
432     * @param boo true if metric, false for Imperial.
433     */
434    public void setMetric(boolean boo){
435        metricUi = boo;
436        fireTableDataChanged();
437    }
438
439    private void updateSensorList() {
440        Set<Sensor> nameSet = InstanceManager.sensorManagerInstance().getNamedBeanSet();
441        String[] displayList = new String[nameSet.size()];
442        int i = 0;
443        for (Sensor nBean : nameSet) {
444            if (nBean != null) {
445                displayList[i++] = nBean.getDisplayName();
446            }
447        }
448        Arrays.sort(displayList);
449        sensorList = new String[displayList.length + 1];
450        sensorList[0] = "";
451        i = 1;
452        for (String name : displayList) {
453            sensorList[i] = name;
454            i++;
455        }
456    }
457
458    private void updateReporterList() {
459        Set<Reporter> nameSet = InstanceManager.getDefault(ReporterManager.class).getNamedBeanSet();
460        String[] displayList = new String[nameSet.size()];
461        int i = 0;
462        for (Reporter nBean : nameSet) {
463            if (nBean != null) {
464                displayList[i++] = nBean.getDisplayName();
465            }
466        }
467        Arrays.sort(displayList);
468        reporterList = new String[displayList.length + 1];
469        reporterList[0] = "";
470        i = 1;
471        for (String name : displayList) {
472            reporterList[i] = name;
473            i++;
474        }
475    }
476
477    private void updateSpeedList() {
478        speedList.remove(defaultBlockSpeedText);
479        defaultBlockSpeedText = (Bundle.getMessage("UseGlobal", "Global") + " " + InstanceManager.getDefault(BlockManager.class).getDefaultSpeed());
480        speedList.add(0, defaultBlockSpeedText);
481        fireTableDataChanged();
482    }
483
484    public void setDefaultSpeeds(JFrame _who) {
485        JComboBox<String> blockSpeedCombo = new JComboBox<>(speedList);
486        JComboBoxUtil.setupComboBoxMaxRows(blockSpeedCombo);
487
488        blockSpeedCombo.setEditable(true);
489
490        JPanel block = new JPanel();
491        block.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("BlockSpeedLabel"))));
492        block.add(blockSpeedCombo);
493
494        blockSpeedCombo.removeItem(defaultBlockSpeedText);
495
496        blockSpeedCombo.setSelectedItem(InstanceManager.getDefault(BlockManager.class).getDefaultSpeed());
497
498        // block of options above row of buttons; gleaned from Maintenance.makeDialog()
499        // can be accessed by Jemmy in GUI test
500        String title = Bundle.getMessage("BlockSpeedLabel");
501        // build JPanel for comboboxes
502        JPanel speedspanel = new JPanel();
503        speedspanel.setLayout(new BoxLayout(speedspanel, BoxLayout.PAGE_AXIS));
504        speedspanel.add(new JLabel(Bundle.getMessage("BlockSpeedSelectDialog")));
505        //default LEFT_ALIGNMENT
506        block.setAlignmentX(Component.LEFT_ALIGNMENT);
507        speedspanel.add(block);
508
509        int retval = JmriJOptionPane.showConfirmDialog(
510                _who,
511                speedspanel,
512                title,
513                JmriJOptionPane.OK_CANCEL_OPTION,
514                JmriJOptionPane.INFORMATION_MESSAGE);
515        log.debug("Retval = {}", retval);
516        if (retval != JmriJOptionPane.OK_OPTION) { // OK button not clicked
517            return;
518        }
519
520        String speedValue = (String) blockSpeedCombo.getSelectedItem();
521        //We will allow the turnout manager to handle checking if the values have changed
522        try {
523            InstanceManager.getDefault(BlockManager.class).setDefaultSpeed(speedValue);
524        } catch (IllegalArgumentException ex) {
525            JmriJOptionPane.showMessageDialog(_who, ex.getMessage() + "\n" + speedValue);
526        }
527    }
528
529    @Override
530    synchronized public void dispose() {
531        InstanceManager.getDefault(SensorManager.class).removePropertyChangeListener(this);
532        InstanceManager.getDefault(ReporterManager.class).removePropertyChangeListener(this);
533        super.dispose();
534    }
535
536    /**
537     * Customize the block table State column to show an appropriate
538     * graphic for the block occupancy state if _graphicState = true, or
539     * (default) just show the localized state text when the
540     * TableDataModel is being called from ListedTableAction.
541     *
542     * @param table a JTable of Blocks
543     */
544    protected void configStateColumn(JTable table) {
545        // have the state column hold a JPanel (icon)
546        //setColumnToHoldButton(table, VALUECOL, new JLabel("1234")); // for small round icon, but cannot be converted to JButton
547        // add extras, override BeanTableDataModel
548        log.debug("Block configStateColumn (I am {})", super.toString());
549        if (_graphicState) { // load icons, only once
550            //table.setDefaultEditor(JLabel.class, new ImageIconRenderer()); // there's no editor for state column in BlockTable
551            table.setDefaultRenderer(JLabel.class, new ImageIconRenderer()); // item class copied from SwitchboardEditor panel
552            // else, classic text style state indication, do nothing extra
553        }
554    }
555
556    // state column may be image so have the tooltip as text version of Block state.
557    // length column tooltip confirms inches or cm.
558    @Override
559    public String getCellToolTip(JTable table, int row, int col) {
560        switch (col) {
561            case BlockTableDataModel.STATECOL:
562                Block b = (Block) getValueAt(row, 0);
563                return blockDescribeState(b.getState());
564            case BlockTableDataModel.LENGTHCOL:
565                return ( metricUi ? Bundle.getMessage("LengthCentimeters"): Bundle.getMessage("LengthInches"));
566            default:
567                return super.getCellToolTip(table, row, col);
568        }
569    }
570
571    /**
572     * Visualize state in table as a graphic, customized for Blocks (2
573     * states). Renderer and Editor are identical, as the cell contents
574     * are not actually edited.
575     *
576     */
577    static class ImageIconRenderer extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
578
579        protected JLabel label;
580        protected String rootPath = "resources/icons/misc/switchboard/"; // also used in display.switchboardEditor
581        protected char beanTypeChar = 'S'; // reuse Sensor icon for block state
582        protected String onIconPath = rootPath + beanTypeChar + "-on-s.png";
583        protected String offIconPath = rootPath + beanTypeChar + "-off-s.png";
584        protected BufferedImage onImage;
585        protected BufferedImage offImage;
586        protected ImageIcon onIcon;
587        protected ImageIcon offIcon;
588        protected int iconHeight = -1;
589
590        @Override
591        public Component getTableCellRendererComponent(
592                JTable table, Object value, boolean isSelected,
593                boolean hasFocus, int row, int column) {
594            log.debug("Renderer Item = {}, State = {}", row, value);
595            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
596                loadIcons();
597                log.debug("icons loaded");
598            }
599            Block b = (Block) table.getModel().getValueAt(row, 0);
600            return updateLabel(b);
601        }
602
603        @Override
604        public Component getTableCellEditorComponent(
605                JTable table, Object value, boolean isSelected,
606                int row, int column) {
607            log.debug("Renderer Item = {}, State = {}", row, value);
608            if (iconHeight < 0) { // load resources only first time, either for renderer or editor
609                loadIcons();
610                log.debug("icons loaded");
611            }
612            Block b = (Block) table.getModel().getValueAt(row, 0);
613            return updateLabel(b);
614        }
615
616        public JLabel updateLabel(Block b) {
617            //  if (iconHeight > 0) { // if necessary, increase row height;
618            //table.setRowHeight(row, Math.max(table.getRowHeight(), iconHeight - 5)); // TODO adjust table row height for Block icons
619            //                     }
620            if (b.getState()==Block.UNOCCUPIED && offIcon != null) {
621                label = new JLabel(offIcon);
622                label.setVerticalAlignment(JLabel.BOTTOM);
623            } else if (b.getState()==Block.OCCUPIED && onIcon != null) {
624                label = new JLabel(onIcon);
625                label.setVerticalAlignment(JLabel.BOTTOM);
626            } else if (b.getState()==Block.INCONSISTENT) {
627                label = new JLabel("X", JLabel.CENTER); // centered text alignment
628                label.setForeground(Color.red);
629                iconHeight = 0;
630            } else { // Unknown Undetected Other
631                label = new JLabel("?", JLabel.CENTER); // centered text alignment
632                iconHeight = 0;
633            }
634            label.addMouseListener(new MouseAdapter() {
635                @Override
636                public final void mousePressed(MouseEvent evt) {
637                    log.debug("Clicked on icon for block {}",b);
638                    stopCellEditing();
639                }
640            });
641            return label;
642        }
643
644        @Override
645        public Object getCellEditorValue() {
646            log.debug("getCellEditorValue, me = {})", this.toString());
647            return this.toString();
648        }
649
650        /**
651         * Read and buffer graphics. Only called once for this table.
652         *
653         * @see #getTableCellEditorComponent(JTable, Object, boolean,
654         * int, int)
655         */
656        protected void loadIcons() {
657            try {
658                onImage = ImageIO.read(new File(onIconPath));
659                offImage = ImageIO.read(new File(offIconPath));
660            } catch (IOException ex) {
661                log.error("error reading image from {} or {}", onIconPath, offIconPath, ex);
662            }
663            log.debug("Success reading images");
664            int imageWidth = onImage.getWidth();
665            int imageHeight = onImage.getHeight();
666            // scale icons 50% to fit in table rows
667            Image smallOnImage = onImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT);
668            Image smallOffImage = offImage.getScaledInstance(imageWidth / 2, imageHeight / 2, Image.SCALE_DEFAULT);
669            onIcon = new ImageIcon(smallOnImage);
670            offIcon = new ImageIcon(smallOffImage);
671            iconHeight = onIcon.getIconHeight();
672        }
673
674    } // end of ImageIconRenderer class
675
676    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockTableDataModel.class);
677
678}