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