]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/load/FileLoader.java
Version 1, September 2006
[GpsPrune.git] / tim / prune / load / FileLoader.java
1 package tim.prune.load;
2
3 import java.awt.BorderLayout;
4 import java.awt.CardLayout;
5 import java.awt.Component;
6 import java.awt.Dimension;
7 import java.awt.FlowLayout;
8 import java.awt.GridLayout;
9 import java.awt.event.ActionEvent;
10 import java.awt.event.ActionListener;
11 import javax.swing.*;
12 import javax.swing.event.DocumentEvent;
13 import javax.swing.event.DocumentListener;
14 import javax.swing.event.ListSelectionEvent;
15 import javax.swing.event.ListSelectionListener;
16 import javax.swing.table.TableCellEditor;
17
18 import java.io.File;
19
20 import tim.prune.App;
21 import tim.prune.I18nManager;
22 import tim.prune.data.Altitude;
23 import tim.prune.data.Field;
24
25
26 /**
27  * Special class to handle file loading including GUI options,
28  * and conversion to a Track object
29  */
30 public class FileLoader
31 {
32         private File _file = null;
33         private App _app = null;
34         private JFrame _parentFrame = null;
35         private JDialog _dialog = null;
36         private JPanel _cardPanel = null;
37         private CardLayout _layout = null;
38         private JButton _backButton = null, _nextButton = null;
39         private JButton _finishButton = null;
40         private JButton _moveUpButton = null, _moveDownButton = null;
41         private JRadioButton[] _delimiterRadios = null;
42         private JTextField _otherDelimiterText = null;
43         private JLabel _statusLabel = null;
44         private DelimiterInfo[] _delimiterInfos = null;
45         private JFileChooser _fileChooser = null;
46         private FileCacher _fileCacher = null;
47         private JList _snippetBox = null;
48         private FileExtractTableModel _fileExtractTableModel = null;
49         private JTable _fieldTable;
50         private FieldSelectionTableModel _fieldTableModel = null;
51         private JComboBox _unitsDropDown = null;
52         private int _selectedField = -1;
53         private char _currentDelimiter = ',';
54
55         // previously selected values
56         private char _lastUsedDelimiter = ',';
57         private int _lastNumFields = -1;
58         private Field[] _lastSelectedFields = null;
59         private int _lastAltitudeFormat = Altitude.FORMAT_NONE;
60
61         // constants
62         private static final int SNIPPET_SIZE = 6;
63         private static final int MAX_SNIPPET_WIDTH = 80;
64         private static final char[] DELIMITERS = {',', '\t', ';', ' '};
65
66
67         /**
68          * Inner class to listen for delimiter change operations
69          */
70         private class DelimListener implements ActionListener, DocumentListener
71         {
72                 public void actionPerformed(ActionEvent e)
73                 {
74                         informDelimiterSelected();
75                 }
76                 public void changedUpdate(DocumentEvent e)
77                 {
78                         informDelimiterSelected();
79                 }
80                 public void insertUpdate(DocumentEvent e)
81                 {
82                         informDelimiterSelected();
83                 }
84                 public void removeUpdate(DocumentEvent e)
85                 {
86                         informDelimiterSelected();
87                 }
88         }
89
90
91         /**
92          * Constructor
93          * @param inApp Application object to inform of track load
94          * @param inParentFrame parent frame to reference for dialogs
95          */
96         public FileLoader(App inApp, JFrame inParentFrame)
97         {
98                 _app = inApp;
99                 _parentFrame = inParentFrame;
100         }
101
102
103         /**
104          * Select an input file and open the GUI frame
105          * to select load options
106          */
107         public void openFile()
108         {
109                 if (_fileChooser == null)
110                         _fileChooser = new JFileChooser();
111                 if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
112                 {
113                         _file = _fileChooser.getSelectedFile();
114                         if (preCheckFile(_file))
115                         {
116                                 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.openoptions.title"), true);
117                                 _dialog.setLocationRelativeTo(_parentFrame);
118                                 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
119                                 _dialog.getContentPane().add(makeDialogComponents());
120
121                                 // select best separator according to row counts (more is better)
122                                 int bestDelim = getBestOption(_delimiterInfos[0].getNumWinningRecords(),
123                                         _delimiterInfos[1].getNumWinningRecords(), _delimiterInfos[2].getNumWinningRecords(),
124                                         _delimiterInfos[3].getNumWinningRecords());
125                                 if (bestDelim >= 0)
126                                         _delimiterRadios[bestDelim].setSelected(true);
127                                 else
128                                         _delimiterRadios[_delimiterRadios.length-1].setSelected(true);
129                                 informDelimiterSelected();
130                                 _dialog.pack();
131                                 _dialog.show();
132                         }
133                         else
134                         {
135                                 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.load.noread"),
136                                         I18nManager.getText("error.load.dialogtitle"), JOptionPane.ERROR_MESSAGE);
137                         }
138                 }
139         }
140
141
142         /**
143          * Check the given file for readability and funny characters,
144          * and count the fields for the various separators
145          * @param inFile file to check
146          */
147         private boolean preCheckFile(File inFile)
148         {
149                 // Check file exists and is readable
150                 if (inFile == null || !inFile.exists() || !inFile.canRead())
151                 {
152                         return false;
153                 }
154                 // Use a FileCacher to read the file into an array
155                 _fileCacher = new FileCacher(inFile);
156
157                 // Check each line of the file
158                 String[] fileContents = _fileCacher.getContents();
159                 boolean fileOK = true;
160                 _delimiterInfos = new DelimiterInfo[5];
161                 for (int i=0; i<4; i++) _delimiterInfos[i] = new DelimiterInfo(DELIMITERS[i]);
162
163                 String currLine = null;
164                 String[] splitFields = null;
165                 int commaFields = 0, semicolonFields = 0, tabFields = 0, spaceFields = 0;
166                 for (int lineNum=0; lineNum<fileContents.length && fileOK; lineNum++)
167                 {
168                         currLine = fileContents[lineNum];
169                         // check for invalid characters
170                         if (currLine.indexOf('\0') >= 0) {fileOK = false;}
171                         // check for commas
172                         splitFields = currLine.split(",");
173                         commaFields = splitFields.length;
174                         if (commaFields > 1) _delimiterInfos[0].incrementNumRecords();
175                         _delimiterInfos[0].updateMaxFields(commaFields);
176                         // check for tabs
177                         splitFields = currLine.split("\t");
178                         tabFields = splitFields.length;
179                         if (tabFields > 1) _delimiterInfos[1].incrementNumRecords();
180                         _delimiterInfos[1].updateMaxFields(tabFields);
181                         // check for semicolons
182                         splitFields = currLine.split(";");
183                         semicolonFields = splitFields.length;
184                         if (semicolonFields > 1) _delimiterInfos[2].incrementNumRecords();
185                         _delimiterInfos[2].updateMaxFields(semicolonFields);
186                         // check for spaces
187                         splitFields = currLine.split(" ");
188                         spaceFields = splitFields.length;
189                         if (spaceFields > 1) _delimiterInfos[3].incrementNumRecords();
190                         _delimiterInfos[3].updateMaxFields(spaceFields);
191                         // increment counters
192                         int bestScorer = getBestOption(commaFields, tabFields, semicolonFields, spaceFields);
193                         if (bestScorer >= 0)
194                                 _delimiterInfos[bestScorer].incrementNumWinningRecords();
195                 }
196                 return fileOK;
197         }
198
199
200         /**
201          * Get the index of the best one in the list
202          * @return the index of the maximum of the four given values
203          */
204         private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3)
205         {
206                 int bestIndex = -1;
207                 int maxScore = 1;
208                 if (inOpt0 > maxScore) {bestIndex = 0; maxScore = inOpt0;}
209                 if (inOpt1 > maxScore) {bestIndex = 1; maxScore = inOpt1;}
210                 if (inOpt2 > maxScore) {bestIndex = 2; maxScore = inOpt2;}
211                 if (inOpt3 > maxScore) {bestIndex = 3; maxScore = inOpt3;}
212                 return bestIndex;
213         }
214
215
216         /**
217          * Make the components for the open options dialog
218          * @return Component for all options
219          */
220         private Component makeDialogComponents()
221         {
222                 JPanel wholePanel = new JPanel();
223                 wholePanel.setLayout(new BorderLayout());
224
225                 // add buttons to south
226                 JPanel buttonPanel = new JPanel();
227                 buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER));
228                 _backButton = new JButton(I18nManager.getText("button.back"));
229                 _backButton.addActionListener(new ActionListener() {
230                         public void actionPerformed(ActionEvent e)
231                         {
232                                 _layout.previous(_cardPanel);
233                                 _backButton.setEnabled(false);
234                                 _nextButton.setEnabled(true);
235                                 _finishButton.setEnabled(false);
236                         }
237                 });
238                 _backButton.setEnabled(false);
239                 buttonPanel.add(_backButton);
240                 _nextButton = new JButton(I18nManager.getText("button.next"));
241                 _nextButton.addActionListener(new ActionListener() {
242                         public void actionPerformed(ActionEvent e)
243                         {
244                                 prepareSecondPanel();
245                                 _layout.next(_cardPanel);
246                                 _nextButton.setEnabled(false);
247                                 _backButton.setEnabled(true);
248                                 _finishButton.setEnabled(true);
249                         }
250                 });
251                 buttonPanel.add(_nextButton);
252                 _finishButton = new JButton(I18nManager.getText("button.finish"));
253                 _finishButton.addActionListener(new ActionListener() {
254                         public void actionPerformed(ActionEvent e)
255                         {
256                                 finished();
257                         }
258                 });
259                 _finishButton.setEnabled(false);
260                 buttonPanel.add(_finishButton);
261                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
262                 cancelButton.addActionListener(new ActionListener() {
263                         public void actionPerformed(ActionEvent e)
264                         {
265                                 _dialog.dispose();
266                         }
267                 });
268                 buttonPanel.add(cancelButton);
269                 wholePanel.add(buttonPanel, BorderLayout.SOUTH);
270
271                 // Make the two cards, for delimiter and fields
272                 _cardPanel = new JPanel();
273                 _layout = new CardLayout();
274                 _cardPanel.setLayout(_layout);
275                 JPanel firstCard = new JPanel();
276                 firstCard.setLayout(new BorderLayout());
277
278                 JPanel delimsPanel = new JPanel();
279                 delimsPanel.setLayout(new GridLayout(0, 2));
280                 delimsPanel.add(new JLabel(I18nManager.getText("dialog.delimiter.label")));
281                 delimsPanel.add(new JLabel("")); // blank label to go to next grid row
282                 // radio buttons
283                 _delimiterRadios = new JRadioButton[5];
284                 _delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma"));
285                 delimsPanel.add(_delimiterRadios[0]);
286                 _delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab"));
287                 delimsPanel.add(_delimiterRadios[1]);
288                 _delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon"));
289                 delimsPanel.add(_delimiterRadios[2]);
290                 _delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space"));
291                 delimsPanel.add(_delimiterRadios[3]);
292                 JPanel otherPanel = new JPanel();
293                 otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
294                 _delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other"));
295                 otherPanel.add(_delimiterRadios[4]);
296                 _otherDelimiterText = new JTextField(new OneCharDocument(), null, 2);
297                 otherPanel.add(_otherDelimiterText);
298                 // Group radio buttons
299                 ButtonGroup delimGroup = new ButtonGroup();
300                 DelimListener delimListener = new DelimListener();
301                 for (int i=0; i<_delimiterRadios.length; i++)
302                 {
303                         delimGroup.add(_delimiterRadios[i]);
304                         _delimiterRadios[i].addActionListener(delimListener);
305                 }
306                 _otherDelimiterText.getDocument().addDocumentListener(delimListener);
307                 delimsPanel.add(new JLabel(""));
308                 delimsPanel.add(otherPanel);
309                 _statusLabel = new JLabel("");
310                 delimsPanel.add(_statusLabel);
311                 firstCard.add(delimsPanel, BorderLayout.SOUTH);
312                 // load snippet to show first few lines
313                 _snippetBox = new JList(_fileCacher.getSnippet(SNIPPET_SIZE, MAX_SNIPPET_WIDTH));
314                 _snippetBox.setEnabled(false);
315                 firstCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", _snippetBox), BorderLayout.CENTER);
316
317                 // Second screen, for field order selection
318                 JPanel secondCard = new JPanel();
319                 secondCard.setLayout(new BorderLayout());
320                 // table for file contents
321                 _fileExtractTableModel = new FileExtractTableModel();
322                 JTable extractTable = new JTable(_fileExtractTableModel);
323                 JScrollPane tableScrollPane = new JScrollPane(extractTable);
324                 extractTable.setPreferredScrollableViewportSize(new Dimension(350, 80));
325                 extractTable.getTableHeader().setReorderingAllowed(false);
326                 secondCard.add(makeLabelledPanel("dialog.openoptions.tabledesc", tableScrollPane), BorderLayout.NORTH);
327                 JPanel innerPanel2 = new JPanel();
328                 innerPanel2.setLayout(new BorderLayout());
329                 _fieldTable = new JTable(new FieldSelectionTableModel());
330                 _fieldTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
331                 // add listener for selected table row
332                 _fieldTable.getSelectionModel().addListSelectionListener(
333                         new ListSelectionListener() {
334                                 public void valueChanged(ListSelectionEvent e) {
335                                         ListSelectionModel lsm = (ListSelectionModel) e.getSource();
336                                         if (lsm.isSelectionEmpty()) {
337                                                 //no rows are selected
338                                                 selectField(-1);
339                                         } else {
340                                                 selectField(lsm.getMinSelectionIndex());
341                                         }
342                                 }
343                         });
344                 JPanel tablePanel = new JPanel();
345                 tablePanel.setLayout(new BorderLayout());
346                 tablePanel.add(_fieldTable.getTableHeader(), BorderLayout.NORTH);
347                 tablePanel.add(_fieldTable, BorderLayout.CENTER);
348                 innerPanel2.add(tablePanel, BorderLayout.CENTER);
349
350                 JPanel innerPanel3 = new JPanel();
351                 innerPanel3.setLayout(new BoxLayout(innerPanel3, BoxLayout.Y_AXIS));
352                 _moveUpButton = new JButton(I18nManager.getText("button.moveup"));
353                 _moveUpButton.addActionListener(new ActionListener() {
354                         public void actionPerformed(ActionEvent e)
355                         {
356                                 int currRow = _fieldTable.getSelectedRow();
357                                 closeTableComboBox(currRow);
358                                 _fieldTableModel.moveUp(currRow);
359                                 _fieldTable.setRowSelectionInterval(currRow-1, currRow-1);
360                         }
361                 });
362                 innerPanel3.add(_moveUpButton);
363                 _moveDownButton = new JButton(I18nManager.getText("button.movedown"));
364                 _moveDownButton.addActionListener(new ActionListener() {
365                         public void actionPerformed(ActionEvent e)
366                         {
367                                 int currRow = _fieldTable.getSelectedRow();
368                                 closeTableComboBox(currRow);
369                                 _fieldTableModel.moveDown(currRow);
370                                 _fieldTable.setRowSelectionInterval(currRow+1, currRow+1);
371                         }
372                 });
373                 innerPanel3.add(_moveDownButton);
374                 innerPanel3.add(Box.createVerticalStrut(70));
375
376                 innerPanel2.add(innerPanel3, BorderLayout.EAST);
377                 secondCard.add(innerPanel2, BorderLayout.CENTER);
378                 JPanel altUnitsPanel = new JPanel();
379                 altUnitsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
380                 altUnitsPanel.add(new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits")));
381                 String[] units = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")};
382                 _unitsDropDown = new JComboBox(units);
383                 altUnitsPanel.add(_unitsDropDown);
384                 secondCard.add(altUnitsPanel, BorderLayout.SOUTH);
385                 _cardPanel.add(firstCard, "card1");
386                 _cardPanel.add(secondCard, "card2");
387
388                 wholePanel.add(_cardPanel, BorderLayout.CENTER);
389                 return wholePanel;
390         }
391
392
393         /**
394          * Close the combo box on the selected row of the field table
395          * @param inRow currently selected row number
396          */
397         private void closeTableComboBox(int inRow)
398         {
399                 TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1);
400                 if (editor != null)
401                 {
402                         editor.stopCellEditing();
403                 }
404         }
405
406
407         /**
408          * change the status based on selection of a delimiter
409          */
410         protected void informDelimiterSelected()
411         {
412                 for (int i=0; i<(_delimiterRadios.length-1); i++)
413                 {
414                         if (_delimiterRadios[i].isSelected())
415                         {
416                                 int numRecords = _delimiterInfos[i].getNumRecords();
417                                 if (numRecords == 0)
418                                 {
419                                         _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords"));
420                                 }
421                                 else
422                                 {
423                                         _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records")
424                                                 + _delimiterInfos[i].getMaxFields() + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields"));
425                                 }
426                         }
427                 }
428                 if (_delimiterRadios[_delimiterRadios.length-1].isSelected())
429                 {
430                         _statusLabel.setText("");
431                 }
432                 // enable/disable next button
433                 _nextButton.setEnabled(_delimiterRadios[4].isSelected() == false
434                         || _otherDelimiterText.getText().length() == 1);
435         }
436
437
438         /**
439          * Get the delimiter info from the first step
440          * @return delimiter information object for the selected delimiter
441          */
442         public DelimiterInfo getSelectedDelimiterInfo()
443         {
444                 for (int i=0; i<4; i++)
445                         if (_delimiterRadios[i].isSelected()) return _delimiterInfos[i];
446                 // must be "other" - build info if necessary
447                 if (_delimiterInfos[4] == null)
448                         _delimiterInfos[4] = new DelimiterInfo(_otherDelimiterText.getText().charAt(0));
449                 return _delimiterInfos[4];
450         }
451
452
453         /**
454          * Use the delimiter selected to determine the fields in the file
455          * and prepare the second panel accordingly
456          */
457         private void prepareSecondPanel()
458         {
459                 DelimiterInfo info = getSelectedDelimiterInfo();
460                 FileSplitter splitter = new FileSplitter(_fileCacher);
461                 // Check info makes sense - num fields > 0, num records > 0
462                 // set "Finished" button to disabled if not ok
463                 // TODO: Work out if there are header rows or not, save?
464                 // Try to match header rows with fields
465                 // Try to match data with fields
466                 // Add data to GUI elements
467                 Object[][] tableData = splitter.splitFieldData(info.getDelimiter());
468                 // possible to ignore blank columns here
469                 _currentDelimiter = info.getDelimiter();
470                 _fileExtractTableModel.updateData(tableData);
471                 _fieldTableModel = new FieldSelectionTableModel();
472
473                 // Check number of fields and use last ones if count matches
474                 Field[] startFieldArray = null;
475                 if (_lastSelectedFields != null && splitter.getNumColumns() == _lastSelectedFields.length)
476                         startFieldArray = _lastSelectedFields;
477                 else
478                         startFieldArray = splitter.makeDefaultFields();
479                 _fieldTableModel.updateData(startFieldArray);
480                 _fieldTable.setModel(_fieldTableModel);
481                 // add dropdowns to second column
482                 JComboBox fieldTypesBox = new JComboBox();
483                 for (int i=0; i<Field.ALL_AVAILABLE_FIELDS.length; i++)
484                 {
485                         fieldTypesBox.addItem(Field.ALL_AVAILABLE_FIELDS[i].getName());
486                 }
487                 _fieldTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(fieldTypesBox));
488
489                 // Set altitude format to same as last time if available
490                 if (_lastAltitudeFormat == Altitude.FORMAT_METRES)
491                         _unitsDropDown.setSelectedIndex(0);
492                 else if (_lastAltitudeFormat == Altitude.FORMAT_FEET)
493                         _unitsDropDown.setSelectedIndex(1);
494                 // no selection on field list
495                 selectField(-1);
496         }
497
498
499         /**
500          * All options have been selected, so load file
501          */
502         private void finished()
503         {
504                 // Save delimiter, field array and altitude format for later use
505                 _lastUsedDelimiter = _currentDelimiter;
506                 _lastSelectedFields = _fieldTableModel.getFieldArray();
507                 int altitudeFormat = Altitude.FORMAT_METRES;
508                 if (_unitsDropDown.getSelectedIndex() == 1)
509                 {
510                         altitudeFormat = Altitude.FORMAT_FEET;
511                 }
512                 _lastAltitudeFormat = altitudeFormat;
513                 // give data to App
514                 _app.informDataLoaded(_fieldTableModel.getFieldArray(),
515                         _fileExtractTableModel.getData(), altitudeFormat,
516                         _file.getName());
517                 // clear up file cacher
518                 _fileCacher.clear();
519                 // dispose of dialog
520                 _dialog.dispose();
521         }
522
523
524         /**
525          * Make a panel with a label and a component
526          * @param inLabelKey label key to use
527          * @param inComponent component for main area of panel
528          * @return labelled Panel
529          */
530         private static JPanel makeLabelledPanel(String inLabelKey, JComponent inComponent)
531         {
532                 JPanel panel = new JPanel();
533                 panel.setLayout(new BorderLayout());
534                 panel.add(new JLabel(I18nManager.getText(inLabelKey)), BorderLayout.NORTH);
535                 panel.add(inComponent, BorderLayout.CENTER);
536                 return panel;
537         }
538
539
540         /**
541          * An entry in the field list has been selected
542          * @param inFieldNum index of field, starting with 0
543          */
544         private void selectField(int inFieldNum)
545         {
546                 if (inFieldNum == -1 || inFieldNum != _selectedField)
547                 {
548                         _selectedField = inFieldNum;
549                         _moveUpButton.setEnabled(inFieldNum > 0);
550                         _moveDownButton.setEnabled(inFieldNum >= 0
551                                 && inFieldNum < (_fieldTableModel.getRowCount()-1));
552                 }
553         }
554
555
556         /**
557          * @return the last delimiter character used for a load
558          */
559         public char getLastUsedDelimiter()
560         {
561                 return _lastUsedDelimiter;
562         }
563 }