001package jmri.jmrit.operations.routes;
002
003import java.awt.BorderLayout;
004import java.awt.FlowLayout;
005import java.awt.event.ActionEvent;
006import java.awt.event.ActionListener;
007import java.beans.PropertyChangeEvent;
008import java.beans.PropertyChangeListener;
009import java.util.ArrayList;
010import java.util.List;
011
012import javax.swing.*;
013import javax.swing.colorchooser.AbstractColorChooserPanel;
014import javax.swing.table.TableCellEditor;
015
016import jmri.jmrit.operations.setup.Control;
017import jmri.jmrit.operations.setup.Setup;
018import jmri.util.swing.*;
019import jmri.util.table.ButtonEditor;
020import jmri.util.table.ButtonRenderer;
021
022/**
023 * Table Model for edit of route locations used by operations
024 *
025 * @author Daniel Boudreau Copyright (C) 2008, 2013
026 */
027public class RouteEditTableModel extends javax.swing.table.AbstractTableModel implements PropertyChangeListener {
028
029    // Defines the columns
030    private static final int ID_COLUMN = 0;
031    private static final int NAME_COLUMN = ID_COLUMN + 1;
032    private static final int TRAIN_DIRECTION_COLUMN = NAME_COLUMN + 1;
033    private static final int MAXMOVES_COLUMN = TRAIN_DIRECTION_COLUMN + 1;
034    private static final int RANDOM_CONTROL_COLUMN = MAXMOVES_COLUMN + 1;
035    private static final int PICKUP_COLUMN = RANDOM_CONTROL_COLUMN + 1;
036    private static final int DROP_COLUMN = PICKUP_COLUMN + 1;
037    private static final int TRAVEL_COLUMN = DROP_COLUMN + 1;
038    private static final int TIME_COLUMN = TRAVEL_COLUMN + 1;
039    private static final int MAXLENGTH_COLUMN = TIME_COLUMN + 1;
040    private static final int GRADE = MAXLENGTH_COLUMN + 1;
041    private static final int TRAINICONX = GRADE + 1;
042    private static final int TRAINICONY = TRAINICONX + 1;
043    private static final int COMMENT_COLUMN = TRAINICONY + 1;
044    private static final int UP_COLUMN = COMMENT_COLUMN + 1;
045    private static final int DOWN_COLUMN = UP_COLUMN + 1;
046    private static final int DELETE_COLUMN = DOWN_COLUMN + 1;
047
048    private static final int HIGHEST_COLUMN = DELETE_COLUMN + 1;
049
050    private JTable _table;
051    private Route _route;
052    private RouteEditFrame _frame;
053    List<RouteLocation> _routeList = new ArrayList<>();
054
055    public RouteEditTableModel() {
056        super();
057    }
058
059    public void setWait(boolean showWait) {
060        XTableColumnModel tcm = (XTableColumnModel) _table.getColumnModel();
061        tcm.setColumnVisible(tcm.getColumnByModelIndex(TRAVEL_COLUMN), showWait);
062        tcm.setColumnVisible(tcm.getColumnByModelIndex(TIME_COLUMN), !showWait);
063    }
064
065    private void updateList() {
066        if (_route == null) {
067            return;
068        }
069        // first, remove listeners from the individual objects
070        removePropertyChangeRouteLocations();
071        _routeList = _route.getLocationsBySequenceList();
072        // and add them back in
073        for (RouteLocation rl : _routeList) {
074            rl.addPropertyChangeListener(this);
075        }
076    }
077
078    protected void initTable(RouteEditFrame frame, JTable table, Route route) {
079        _frame = frame;
080        _table = table;
081        _route = route;
082        if (_route != null) {
083            _route.addPropertyChangeListener(this);
084        }
085        Setup.getDefault().addPropertyChangeListener(this);
086        initTable(table);
087    }
088
089    private void initTable(JTable table) {
090        // Use XTableColumnModel so we can control which columns are visible
091        XTableColumnModel tcm = new XTableColumnModel();
092        _table.setColumnModel(tcm);
093        _table.createDefaultColumnsFromModel();
094        // Install the button handlers
095        ButtonRenderer buttonRenderer = new ButtonRenderer();
096        TableCellEditor buttonEditor = new ButtonEditor(new javax.swing.JButton());
097        tcm.getColumn(COMMENT_COLUMN).setCellRenderer(buttonRenderer);
098        tcm.getColumn(COMMENT_COLUMN).setCellEditor(buttonEditor);
099        tcm.getColumn(UP_COLUMN).setCellRenderer(buttonRenderer);
100        tcm.getColumn(UP_COLUMN).setCellEditor(buttonEditor);
101        tcm.getColumn(DOWN_COLUMN).setCellRenderer(buttonRenderer);
102        tcm.getColumn(DOWN_COLUMN).setCellEditor(buttonEditor);
103        tcm.getColumn(DELETE_COLUMN).setCellRenderer(buttonRenderer);
104        tcm.getColumn(DELETE_COLUMN).setCellEditor(buttonEditor);
105        table.setDefaultRenderer(JComboBox.class, new jmri.jmrit.symbolicprog.ValueRenderer());
106        table.setDefaultEditor(JComboBox.class, new jmri.jmrit.symbolicprog.ValueEditor());
107
108        // set column preferred widths
109        table.getColumnModel().getColumn(ID_COLUMN).setPreferredWidth(40);
110        table.getColumnModel().getColumn(NAME_COLUMN).setPreferredWidth(150);
111        table.getColumnModel().getColumn(TRAIN_DIRECTION_COLUMN).setPreferredWidth(95);
112        table.getColumnModel().getColumn(MAXMOVES_COLUMN).setPreferredWidth(50);
113        table.getColumnModel().getColumn(RANDOM_CONTROL_COLUMN).setPreferredWidth(65);
114        table.getColumnModel().getColumn(PICKUP_COLUMN).setPreferredWidth(65);
115        table.getColumnModel().getColumn(DROP_COLUMN).setPreferredWidth(65);
116        table.getColumnModel().getColumn(TRAVEL_COLUMN).setPreferredWidth(65);
117        table.getColumnModel().getColumn(TIME_COLUMN).setPreferredWidth(65);
118        table.getColumnModel().getColumn(MAXLENGTH_COLUMN).setPreferredWidth(75);
119        table.getColumnModel().getColumn(GRADE).setPreferredWidth(50);
120        table.getColumnModel().getColumn(TRAINICONX).setPreferredWidth(35);
121        table.getColumnModel().getColumn(TRAINICONY).setPreferredWidth(35);
122        table.getColumnModel().getColumn(COMMENT_COLUMN).setPreferredWidth(70);
123        table.getColumnModel().getColumn(UP_COLUMN).setPreferredWidth(60);
124        table.getColumnModel().getColumn(DOWN_COLUMN).setPreferredWidth(70);
125        table.getColumnModel().getColumn(DELETE_COLUMN).setPreferredWidth(80);
126
127        _frame.loadTableDetails(table);
128        // does not use a table sorter
129        table.setRowSorter(null);
130
131        // turn off columns
132        tcm.setColumnVisible(tcm.getColumnByModelIndex(TIME_COLUMN), false);
133
134        updateList();
135    }
136
137    @Override
138    public int getRowCount() {
139        return _routeList.size();
140    }
141
142    @Override
143    public int getColumnCount() {
144        return HIGHEST_COLUMN;
145    }
146
147    @Override
148    public String getColumnName(int col) {
149        switch (col) {
150            case ID_COLUMN:
151                return Bundle.getMessage("Id");
152            case NAME_COLUMN:
153                return Bundle.getMessage("Location");
154            case TRAIN_DIRECTION_COLUMN:
155                return Bundle.getMessage("TrainDirection");
156            case MAXMOVES_COLUMN:
157                return Bundle.getMessage("MaxMoves");
158            case RANDOM_CONTROL_COLUMN:
159                return Bundle.getMessage("Random");
160            case PICKUP_COLUMN:
161                return Bundle.getMessage("Pickups");
162            case DROP_COLUMN:
163                return Bundle.getMessage("Drops");
164            case TRAVEL_COLUMN:
165                return Bundle.getMessage("Travel");
166            case TIME_COLUMN:
167                return Bundle.getMessage("Time");
168            case MAXLENGTH_COLUMN:
169                return Bundle.getMessage("MaxLength");
170            case GRADE:
171                return Bundle.getMessage("Grade");
172            case TRAINICONX:
173                return Bundle.getMessage("X");
174            case TRAINICONY:
175                return Bundle.getMessage("Y");
176            case COMMENT_COLUMN:
177                return Bundle.getMessage("Comment");
178            case UP_COLUMN:
179                return Bundle.getMessage("Up");
180            case DOWN_COLUMN:
181                return Bundle.getMessage("Down");
182            case DELETE_COLUMN:
183                return Bundle.getMessage("ButtonDelete"); // titles above all columns
184            default:
185                return "unknown"; // NOI18N
186        }
187    }
188
189    @Override
190    public Class<?> getColumnClass(int col) {
191        switch (col) {
192            case ID_COLUMN:
193            case NAME_COLUMN:
194                return String.class;
195            case TRAVEL_COLUMN:
196            case MAXLENGTH_COLUMN:
197            case MAXMOVES_COLUMN:
198            case TRAINICONX:
199            case TRAINICONY:
200                return Integer.class;
201            case GRADE:
202                return Double.class;
203            case TRAIN_DIRECTION_COLUMN:
204            case RANDOM_CONTROL_COLUMN:
205            case PICKUP_COLUMN:
206            case DROP_COLUMN:
207            case TIME_COLUMN:
208                return JComboBox.class;
209            case COMMENT_COLUMN:
210            case UP_COLUMN:
211            case DOWN_COLUMN:
212            case DELETE_COLUMN:
213                return JButton.class;
214            default:
215                return null;
216        }
217    }
218
219    @Override
220    public boolean isCellEditable(int row, int col) {
221        switch (col) {
222            case DELETE_COLUMN:
223            case TRAIN_DIRECTION_COLUMN:
224            case MAXMOVES_COLUMN:
225            case RANDOM_CONTROL_COLUMN:
226            case PICKUP_COLUMN:
227            case DROP_COLUMN:
228            case TRAVEL_COLUMN:
229            case TIME_COLUMN:
230            case MAXLENGTH_COLUMN:
231            case GRADE:
232            case TRAINICONX:
233            case TRAINICONY:
234            case COMMENT_COLUMN:
235            case UP_COLUMN:
236            case DOWN_COLUMN:
237                return true;
238            default:
239                return false;
240        }
241    }
242
243    @Override
244    public Object getValueAt(int row, int col) {
245        if (row >= getRowCount()) {
246            return "ERROR unknown " + row; // NOI18N
247        }
248        RouteLocation rl = _routeList.get(row);
249        if (rl == null) {
250            return "ERROR unknown route location " + row; // NOI18N
251        }
252        switch (col) {
253            case ID_COLUMN:
254                return rl.getId();
255            case NAME_COLUMN:
256                return rl.getName();
257            case TRAIN_DIRECTION_COLUMN: {
258                JComboBox<String> cb = Setup.getTrainDirectionComboBox();
259                cb.setSelectedItem(rl.getTrainDirectionString());
260                return cb;
261            }
262            case MAXMOVES_COLUMN:
263                return rl.getMaxCarMoves();
264            case RANDOM_CONTROL_COLUMN: {
265                JComboBox<String> cb = getRandomControlComboBox();
266                cb.setSelectedItem(rl.getRandomControl());
267                return cb;
268            }
269            case PICKUP_COLUMN: {
270                JComboBox<String> cb = getYesNoComboBox();
271                cb.setSelectedItem(rl.isPickUpAllowed() ? Bundle.getMessage("yes") : Bundle.getMessage("no"));
272                return cb;
273            }
274            case DROP_COLUMN: {
275                JComboBox<String> cb = getYesNoComboBox();
276                cb.setSelectedItem(rl.isDropAllowed() ? Bundle.getMessage("yes") : Bundle.getMessage("no"));
277                return cb;
278            }
279            case TRAVEL_COLUMN: {
280                return rl.getWait() + Setup.getTravelTime();
281            }
282            case TIME_COLUMN: {
283                JComboBox<String> cb = getTimeComboBox();
284                cb.setSelectedItem(rl.getDepartureTime());
285                return cb;
286            }
287            case MAXLENGTH_COLUMN:
288                return rl.getMaxTrainLength();
289            case GRADE:
290                return rl.getGrade();
291            case TRAINICONX:
292                return rl.getTrainIconX();
293            case TRAINICONY:
294                return rl.getTrainIconY();
295            case COMMENT_COLUMN: {
296                if (rl.getComment().equals(RouteLocation.NONE)) {
297                    return Bundle.getMessage("Add");
298                } else {
299                    return Bundle.getMessage("ButtonEdit");
300                }
301            }
302            case UP_COLUMN:
303                return Bundle.getMessage("Up");
304            case DOWN_COLUMN:
305                return Bundle.getMessage("Down");
306            case DELETE_COLUMN:
307                return Bundle.getMessage("ButtonDelete");
308            default:
309                return "unknown " + col; // NOI18N
310        }
311    }
312
313    @Override
314    public void setValueAt(Object value, int row, int col) {
315        if (value == null) {
316            log.debug("Warning route table row {} still in edit", row);
317            return;
318        }
319        RouteLocation rl = _routeList.get(row);
320        if (rl == null) {
321            log.error("ERROR unknown route location for row: {}", row); // NOI18N
322        }
323        switch (col) {
324            case COMMENT_COLUMN:
325                setComment(rl);
326                break;
327            case UP_COLUMN:
328                moveUpRouteLocation(rl);
329                break;
330            case DOWN_COLUMN:
331                moveDownRouteLocation(rl);
332                break;
333            case DELETE_COLUMN:
334                deleteRouteLocation(rl);
335                break;
336            case TRAIN_DIRECTION_COLUMN:
337                setTrainDirection(value, rl);
338                break;
339            case MAXMOVES_COLUMN:
340                setMaxTrainMoves(value, rl);
341                break;
342            case RANDOM_CONTROL_COLUMN:
343                setRandomControlValue(value, rl);
344                break;
345            case PICKUP_COLUMN:
346                setPickup(value, rl);
347                break;
348            case DROP_COLUMN:
349                setDrop(value, rl);
350                break;
351            case TRAVEL_COLUMN:
352                setTravel(value, rl);
353                break;
354            case TIME_COLUMN:
355                setDepartureTime(value, rl);
356                break;
357            case MAXLENGTH_COLUMN:
358                setMaxTrainLength(value, rl);
359                break;
360            case GRADE:
361                setGrade(value, rl);
362                break;
363            case TRAINICONX:
364                setTrainIconX(value, rl);
365                break;
366            case TRAINICONY:
367                setTrainIconY(value, rl);
368                break;
369            default:
370                break;
371        }
372    }
373
374    private void moveUpRouteLocation(RouteLocation rl) {
375        log.debug("move location up");
376        _route.moveLocationUp(rl);
377    }
378
379    private void moveDownRouteLocation(RouteLocation rl) {
380        log.debug("move location down");
381        _route.moveLocationDown(rl);
382    }
383
384    private void deleteRouteLocation(RouteLocation rl) {
385        log.debug("Delete location");
386        _route.deleteLocation(rl);
387    }
388
389    private int _trainDirection = Setup.getDirectionInt(Setup.getTrainDirectionList().get(0));
390
391    public int getLastTrainDirection() {
392        return _trainDirection;
393    }
394
395    private void setTrainDirection(Object value, RouteLocation rl) {
396        _trainDirection = Setup.getDirectionInt((String) ((JComboBox<?>) value).getSelectedItem());
397        rl.setTrainDirection(_trainDirection);
398        // update train icon
399        rl.setTrainIconCoordinates();
400    }
401
402    private int _maxTrainMoves = Setup.getCarMoves();
403
404    public int getLastMaxTrainMoves() {
405        return _maxTrainMoves;
406    }
407
408    private void setMaxTrainMoves(Object value, RouteLocation rl) {
409        int moves = (int) value;
410        if (moves <= 500) {
411            rl.setMaxCarMoves(moves);
412            _maxTrainMoves = moves;
413        } else {
414            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MaximumLocationMoves"), Bundle
415                    .getMessage("CanNotChangeMoves"), JmriJOptionPane.ERROR_MESSAGE);
416        }
417    }
418
419    private void setRandomControlValue(Object value, RouteLocation rl) {
420        rl.setRandomControl((String) ((JComboBox<?>) value).getSelectedItem());
421    }
422
423    private void setDrop(Object value, RouteLocation rl) {
424        rl.setDropAllowed(
425                ((String) ((JComboBox<?>) value).getSelectedItem()).equals(Bundle.getMessage("yes")));
426    }
427
428    private void setPickup(Object value, RouteLocation rl) {
429        rl.setPickUpAllowed(
430                ((String) ((JComboBox<?>) value).getSelectedItem()).equals(Bundle.getMessage("yes")));
431    }
432
433    private int _maxTrainLength = Setup.getMaxTrainLength();
434
435    public int getLastMaxTrainLength() {
436        return _maxTrainLength;
437    }
438
439    private void setTravel(Object value, RouteLocation rl) {
440        int wait = (int) value;
441        rl.setWait(wait - Setup.getTravelTime());
442    }
443
444    private void setDepartureTime(Object value, RouteLocation rl) {
445        rl.setDepartureTime(((String) ((JComboBox<?>) value).getSelectedItem()));
446    }
447
448    private void setMaxTrainLength(Object value, RouteLocation rl) {
449        int length = (int) value;
450        if (length < 0) {
451            log.error("Maximum departure length must be a postive number");
452            return;
453        }
454        if (length > Setup.getMaxTrainLength()) {
455            log.error("Maximum departure length can not exceed maximum train length");
456            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("DepartureLengthNotExceed",
457                    length, Setup.getMaxTrainLength()), Bundle.getMessage("CanNotChangeMaxLength"),
458                    JmriJOptionPane.ERROR_MESSAGE);
459            return;
460        }
461        if (rl != _route.getTerminatesRouteLocation() &&
462                (length < 500 && Setup.getLengthUnit().equals(Setup.FEET) ||
463                        length < 160 && Setup.getLengthUnit().equals(Setup.METER))) {
464            // warn that train length might be too short
465            if (JmriJOptionPane.showConfirmDialog(null, Bundle.getMessage("LimitTrainLength",
466                    length, Setup.getLengthUnit().toLowerCase(), rl.getName()),
467                    Bundle.getMessage("WarningTooShort"),
468                    JmriJOptionPane.OK_CANCEL_OPTION) != JmriJOptionPane.OK_OPTION) {
469                return;
470            }
471        }
472        rl.setMaxTrainLength(length);
473        _maxTrainLength = length;
474    }
475
476    private void setGrade(Object value, RouteLocation rl) {
477        double grade = (Double) value;
478        if (grade <= 6 && grade >= -6) {
479            rl.setGrade(grade);
480        } else {
481            log.error("Maximum grade is 6 percent");
482            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MaxGrade"),
483                    Bundle.getMessage("CanNotChangeGrade"),
484                    JmriJOptionPane.ERROR_MESSAGE);
485        }
486    }
487
488    private void setTrainIconX(Object value, RouteLocation rl) {
489        int x = (int) value;
490        rl.setTrainIconX(x);
491    }
492
493    private void setTrainIconY(Object value, RouteLocation rl) {
494        int y = (int) value;
495        rl.setTrainIconY(y);
496    }
497
498    private void setComment(RouteLocation rl) {
499        // Create comment panel
500        final JDialog dialog = new JDialog();
501        dialog.setLayout(new BorderLayout());
502        dialog.setTitle(Bundle.getMessage("Comment") + " " + rl.getName());
503        final JTextArea commentTextArea = new JTextArea(5, 100);
504        JScrollPane commentScroller = new JScrollPane(commentTextArea);
505        dialog.add(commentScroller, BorderLayout.CENTER);
506        commentTextArea.setText(rl.getComment());
507
508        JPanel buttonPane = new JPanel();
509        buttonPane.setLayout(new FlowLayout(FlowLayout.CENTER));
510        dialog.add(buttonPane, BorderLayout.SOUTH);
511
512        // text color chooser
513        JPanel pTextColor = new JPanel();
514        pTextColor.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("TextColor")));
515        JColorChooser commentColorChooser = new JColorChooser(rl.getCommentColor());
516        AbstractColorChooserPanel commentColorPanels[] = {new SplitButtonColorChooserPanel()};
517        commentColorChooser.setChooserPanels(commentColorPanels);
518        commentColorChooser.setPreviewPanel(new JPanel());
519        pTextColor.add(commentColorChooser);
520        buttonPane.add(pTextColor);
521
522        JButton okayButton = new JButton(Bundle.getMessage("ButtonOK"));
523        okayButton.addActionListener(new ActionListener() {
524            @Override
525            public void actionPerformed(ActionEvent arg0) {
526                rl.setComment(commentTextArea.getText());
527                rl.setCommentColor(commentColorChooser.getColor());
528                dialog.dispose();
529                return;
530            }
531        });
532        buttonPane.add(okayButton);
533
534        JButton cancelButton = new JButton(Bundle.getMessage("ButtonCancel"));
535        cancelButton.addActionListener(new ActionListener() {
536            @Override
537            public void actionPerformed(ActionEvent arg0) {
538                dialog.dispose();
539                return;
540            }
541        });
542        buttonPane.add(cancelButton);
543
544        dialog.setModal(true);
545        dialog.pack();
546        dialog.setVisible(true);
547    }
548
549    private JComboBox<String> getYesNoComboBox() {
550        JComboBox<String> cb = new JComboBox<>();
551        cb.addItem(Bundle.getMessage("yes"));
552        cb.addItem(Bundle.getMessage("no"));
553        return cb;
554    }
555
556    private JComboBox<String> getRandomControlComboBox() {
557        JComboBox<String> cb = new JComboBox<>();
558        cb.addItem(RouteLocation.DISABLED);
559        // 10 to 100 by 10
560        for (int i = 10; i < 101; i = i + 10) {
561            cb.addItem(Integer.toString(i));
562        }
563        return cb;
564    }
565
566    protected JComboBox<String> getTimeComboBox() {
567        JComboBox<String> timeBox = new JComboBox<>();
568        String hour;
569        String minute;
570        timeBox.addItem("");
571        for (int i = 0; i < 24; i++) {
572            if (i < 10) {
573                hour = "0" + Integer.toString(i);
574            } else {
575                hour = Integer.toString(i);
576            }
577            for (int j = 0; j < 60; j += 1) {
578                if (j < 10) {
579                    minute = "0" + Integer.toString(j);
580                } else {
581                    minute = Integer.toString(j);
582                }
583
584                timeBox.addItem(hour + ":" + minute);
585            }
586        }
587        return timeBox;
588    }
589
590    // this table listens for changes to a route and it's locations
591    @Override
592    public void propertyChange(PropertyChangeEvent e) {
593        if (Control.SHOW_PROPERTY) {
594            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e
595                    .getNewValue());
596        }
597        if (e.getPropertyName().equals(Route.LISTCHANGE_CHANGED_PROPERTY)) {
598            updateList();
599            fireTableDataChanged();
600        }
601        if (e.getPropertyName().equals(Setup.TRAIN_DIRECTION_PROPERTY_CHANGE) ||
602                e.getPropertyName().equals(Setup.TRAVEL_TIME_PROPERTY_CHANGE)) {
603            fireTableDataChanged();
604        }
605        if (e.getSource().getClass().equals(RouteLocation.class)) {
606            RouteLocation rl = (RouteLocation) e.getSource();
607            int row = _routeList.indexOf(rl);
608            if (Control.SHOW_PROPERTY) {
609                log.debug("Update route table row: {} id: {}", row, rl.getId());
610            }
611            if (row >= 0) {
612                fireTableRowsUpdated(row, row);
613            }
614        }
615    }
616
617    private void removePropertyChangeRouteLocations() {
618        for (RouteLocation rl : _routeList) {
619            rl.removePropertyChangeListener(this);
620        }
621    }
622
623    public void dispose() {
624        removePropertyChangeRouteLocations();
625        if (_route != null) {
626            _route.removePropertyChangeListener(this);
627        }
628        Setup.getDefault().removePropertyChangeListener(this);
629        _routeList.clear();
630        fireTableDataChanged();
631    }
632
633    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RouteEditTableModel.class);
634}