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