001package jmri.jmrix.rps.swing.debugger;
002
003import java.awt.FlowLayout;
004import java.io.FileReader;
005import java.io.IOException;
006import java.io.Reader;
007import java.text.ParseException;
008
009import javax.swing.BoxLayout;
010import javax.swing.JButton;
011import javax.swing.JComboBox;
012import javax.swing.JFileChooser;
013import javax.swing.JLabel;
014import javax.swing.JMenu;
015import javax.swing.JMenuBar;
016import javax.swing.JPanel;
017import javax.swing.JScrollPane;
018import javax.swing.JSeparator;
019import javax.swing.JTextField;
020
021import jmri.jmrix.rps.Distributor;
022import jmri.jmrix.rps.Engine;
023import jmri.jmrix.rps.Measurement;
024import jmri.jmrix.rps.MeasurementListener;
025import jmri.jmrix.rps.Reading;
026import jmri.jmrix.rps.ReadingListener;
027import jmri.jmrix.rps.RpsSystemConnectionMemo;
028import jmri.util.IntlUtilities;
029
030import org.apache.commons.csv.CSVFormat;
031import org.apache.commons.csv.CSVParser;
032import org.apache.commons.csv.CSVRecord;
033
034/**
035 * Frame for manual operation and debugging of the RPS system.
036 *
037 * @author Bob Jacobsen Copyright (C) 2008
038 */
039public class DebuggerFrame extends jmri.util.JmriJFrame
040        implements ReadingListener, MeasurementListener {
041
042    private RpsSystemConnectionMemo memo = null;
043
044    public DebuggerFrame(RpsSystemConnectionMemo rpsMemo) {
045        super();
046        memo = rpsMemo;
047
048        NUMSENSORS = Engine.instance().getMaxReceiverNumber();
049
050        setTitle(title());
051    }
052
053    protected final String title() {
054        return "RPS Debugger";
055    }  // product name, not translated
056
057    @Override
058    public void dispose() {
059        // separate from data source
060        Distributor.instance().removeReadingListener(this);
061        Distributor.instance().removeMeasurementListener(this);
062        // and unwind swing
063        super.dispose();
064    }
065
066    private java.text.NumberFormat nf;
067
068    private JComboBox<String> mode;
069
070    private final JTextField x = new JTextField(18);
071    private final JTextField y = new JTextField(18);
072    private final JTextField z = new JTextField(18);
073    private final JLabel code = new JLabel();
074
075    private final JTextField id = new JTextField(5);
076
077    private final DebuggerTimePane timep = new DebuggerTimePane();
078
079    private final int NUMSENSORS;
080
081    @Override
082    public void initComponents() {
083
084        // I18N format
085        nf = java.text.NumberFormat.getInstance();
086        nf.setMinimumFractionDigits(1);
087        nf.setMaximumFractionDigits(1);
088        nf.setGroupingUsed(false);
089
090        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
091
092        // add panes in the middle
093        JPanel p;
094        JPanel p1;
095
096        // Time inputs
097        p = new JPanel();
098        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
099
100        p.add(new JLabel("Time measurements: "));
101
102        timep.initComponents();
103        JScrollPane sc = new JScrollPane(timep);
104        p.add(sc);
105
106        // add id field at bottom
107        JPanel p5 = new JPanel();
108        p5.setLayout(new FlowLayout());
109        p5.add(new JLabel("Id: "));
110        p5.add(id);
111        p.add(p5);
112
113        getContentPane().add(p);
114
115        getContentPane().add(new JSeparator());
116
117        // x, y, z results
118        p = new JPanel();
119        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
120        p.add(new JLabel("Results:"));
121        p1 = new JPanel();
122        p1.setLayout(new FlowLayout());
123        p1.add(new JLabel("X:"));
124        p1.add(x);
125        p.add(p1);
126        p1 = new JPanel();
127        p1.setLayout(new FlowLayout());
128        p1.add(new JLabel("Y:"));
129        p1.add(y);
130        p.add(p1);
131        p1 = new JPanel();
132        p1.setLayout(new FlowLayout());
133        p1.add(new JLabel("Z:"));
134        p1.add(z);
135        p.add(p1);
136        p1 = new JPanel();
137        p1.setLayout(new FlowLayout());
138        p1.add(new JLabel("Code:"));
139        p1.add(code);
140        p.add(p1);
141        getContentPane().add(p);
142
143        getContentPane().add(new JSeparator());
144
145        // add controls at bottom
146        p = new JPanel();
147
148        mode = new JComboBox<>(new String[]{"From time fields", "from X,Y,Z fields", "from time file", "from X,Y,Z file"});
149        p.add(mode);
150        p.setLayout(new FlowLayout());
151
152        JButton doButton = new JButton("Do Once");
153        doButton.addActionListener(e -> doOnce());
154        p.add(doButton);
155        getContentPane().add(p);
156
157        // start working
158        Distributor.instance().addReadingListener(this);
159        Distributor.instance().addMeasurementListener(this);
160
161        // add file menu
162        JMenuBar menuBar = new JMenuBar();
163        JMenu fileMenu = new JMenu("File");
164        menuBar.add(fileMenu);
165        fileMenu.add(new jmri.jmrix.rps.swing.CsvExportAction("Export Readings as CSV...", memo));
166        fileMenu.add(new jmri.jmrix.rps.swing.CsvExportMeasurementAction("Export Measurements as CSV...", memo));
167        setJMenuBar(menuBar);
168
169        // add help
170        addHelpMenu("package.jmri.jmrix.rps.swing.debugger.DebuggerFrame", true);
171
172        // prepare for display
173        pack();
174    }
175
176    /**
177     * Invoked by button to do one cycle
178     */
179    private void doOnce() {
180        try {
181            switch (mode.getSelectedIndex()) {
182                case 0: // From time fields
183                    doReadingFromTimeFields();
184                    break;
185                case 1: // From X,Y,Z fields
186                    doMeasurementFromPositionFields();
187                    break;
188                case 2: // From time file
189                    doLoadReadingFromFile();
190                    doReadingFromTimeFields();
191                    break;
192                case 3: // From X,Y,Z file
193                    doLoadMeasurementFromFile();
194                    break;
195                default: // should not happen
196                    log.error("Did not expect selected mode {}", mode.getSelectedIndex());
197            }
198        } catch (IOException e) {
199            log.error("exception ", e);
200        }
201    }
202
203    private void doLoadReadingFromFile() throws IOException {
204        if (readingInput == null) {
205            readingInput = getParser(readingFileChooser);
206        }
207
208        // get and load a line
209        if (readingInput.getRecords().isEmpty()) {
210            // read failed, try once to get another file
211            readingInput = getParser(readingFileChooser);
212            if (readingInput.getRecords().isEmpty()) {
213                throw new IOException("no valid file");
214            }
215        }
216        CSVRecord readingRecord = readingInput.getRecords().get(0);
217
218        // item 0 is the ID, not used right now
219        for (int i = 0; i < Math.min(NUMSENSORS, readingRecord.size() + 1); i++) {
220            timep.times[i].setText(readingRecord.get(i + 1));
221        }
222    }
223
224    private CSVParser getParser(JFileChooser chooser) throws IOException {
225        // get file
226        CSVParser parser = null;
227
228        chooser.rescanCurrentDirectory();
229        int retVal = chooser.showOpenDialog(this);
230
231        // handle selection or cancel
232        if (retVal == JFileChooser.APPROVE_OPTION) {
233            // create and keep reader
234            Reader reader = new FileReader(chooser.getSelectedFile());
235            parser = new CSVParser(reader,
236                    CSVFormat.Builder.create(CSVFormat.DEFAULT).setSkipHeaderRecord(true).build());
237        }
238        return parser;
239    }
240
241    private void doLoadMeasurementFromFile() throws IOException {
242        if (measurementInput == null) {
243            measurementInput = getParser(measurementFileChooser);
244        }
245
246        // get and load a line
247        if (measurementInput.getRecords().isEmpty()) {
248            // read failed, try once to get another file
249            measurementInput = getParser(measurementFileChooser);
250            if (measurementInput.getRecords().isEmpty()) {
251                throw new IOException("no valid file");
252            }
253        }
254        CSVRecord measurementRecord = measurementInput.getRecords().get(0);
255
256        // item 0 is the ID, not used right now
257        Measurement m = new Measurement(null,
258                Double.parseDouble(measurementRecord.get(1)),
259                Double.parseDouble(measurementRecord.get(2)),
260                Double.parseDouble(measurementRecord.get(3)),
261                Engine.instance().getVSound(),
262                0,
263                "Data File"
264        );
265
266        Distributor.instance().submitMeasurement(m);
267    }
268
269    private Reading getReadingFromTimeFields() {
270
271        double[] values = new double[NUMSENSORS + 1];
272
273        JTextField tmp;
274        // parse input
275        for (int i = 0; i <= NUMSENSORS; i++) {
276            values[i] = 0.;
277            tmp = timep.times[i];
278            if ((tmp != null) && !tmp.getText().isEmpty()) {
279                try {
280                    values[i] = IntlUtilities.doubleValue(tmp.getText());
281                } catch (ParseException ex) {
282                    log.error("Could not create number from {}, {}",tmp.getText(),ex.getMessage());
283                }
284            }
285        }
286
287        // get the id number and make reading
288        return new Reading(id.getText(), values);
289    }
290
291    private void doReadingFromTimeFields() {
292        // get the reading
293        Reading r = getReadingFromTimeFields();
294
295        // and forward
296        Distributor.instance().submitReading(r);
297    }
298
299    @Override
300    public void notify(Reading r) {
301        // This implementation creates a new Calculator
302        // each time to ensure that the most recent
303        // receiver positions are used; this should be
304        // replaced with some notification system
305        // to reduce the work used.
306
307        id.setText("" + r.getId());
308        timep.notify(r);
309    }
310
311    private void doMeasurementFromPositionFields() {
312        // contain dummy Reading
313        Reading r = new Reading(id.getText(), new double[]{0., 0., 0., 0.});
314
315        try {
316            Measurement m = new Measurement(r,
317                IntlUtilities.doubleValue(x.getText()),
318                IntlUtilities.doubleValue(y.getText()),
319                IntlUtilities.doubleValue(z.getText()),
320                Engine.instance().getVSound(),
321                0,
322                "Position Data"
323            );
324            Distributor.instance().submitMeasurement(m);
325        } catch (ParseException ex) {
326            log.error("Could not translate a field to Number. {}",ex.getMessage());
327        }
328    }
329
330    @Override
331    public void notify(Measurement m) {
332        // show result
333        x.setText(nf.format(m.getX()));
334        y.setText(nf.format(m.getY()));
335        z.setText(nf.format(m.getZ()));
336        code.setText(m.textCode());
337
338        timep.notify(m);
339    }
340
341    // to find and remember the input files
342    private CSVParser readingInput = null;
343    final JFileChooser readingFileChooser = new jmri.util.swing.JmriJFileChooser("rps/readings.csv");
344
345    private CSVParser measurementInput = null;
346    final JFileChooser measurementFileChooser = new jmri.util.swing.JmriJFileChooser("rps/positions.csv");
347
348    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DebuggerFrame.class);
349
350}