altoslib: Mark listener as 'not running' on EOF.
[fw/altos] / telegps / TeleGPS.java
1 /*
2  * Copyright © 2014 Keith Packard <keithp@keithp.com>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
16  */
17
18 package org.altusmetrum.telegps;
19
20 import java.awt.*;
21 import java.awt.event.*;
22 import javax.swing.*;
23 import java.io.*;
24 import java.util.concurrent.*;
25 import java.util.*;
26 import org.altusmetrum.altoslib_5.*;
27 import org.altusmetrum.altosuilib_3.*;
28
29 public class TeleGPS
30         extends AltosUIFrame
31         implements AltosFlightDisplay, AltosFontListener, AltosUnitsListener, ActionListener
32 {
33
34         static String[] telegps_icon_names = {
35                 "/altusmetrum-telegps-16.png",
36                 "/altusmetrum-telegps-32.png",
37                 "/altusmetrum-telegps-48.png",
38                 "/altusmetrum-telegps-64.png",
39                 "/altusmetrum-telegps-128.png",
40                 "/altusmetrum-telegps-256.png"
41         };
42
43         static { set_icon_names(telegps_icon_names); }
44
45         static AltosVoice       voice;
46
47         static AltosVoice voice() {
48                 if (voice == null)
49                         voice = new AltosVoice();
50                 return voice;
51         }
52
53         AltosFlightReader       reader;
54         TeleGPSDisplayThread    thread;
55
56         JMenuBar                menu_bar;
57
58         JMenu                   file_menu;
59         JMenu                   monitor_menu;
60         JMenu                   device_menu;
61         AltosUIFreqList         frequencies;
62         ActionListener          frequency_listener;
63         AltosUIRateList         rates;
64         ActionListener          rate_listener;
65
66         Container               bag;
67
68         TeleGPSStatus           telegps_status;
69         TeleGPSStatusUpdate     status_update;
70
71         JTabbedPane             pane;
72
73         AltosUIMap              map;
74         TeleGPSInfo             gps_info;
75         TeleGPSState            gps_state;
76         AltosInfoTable          info_table;
77
78         LinkedList<AltosFlightDisplay>  displays;
79
80         /* File menu */
81         final static String     new_command = "new";
82         final static String     graph_command = "graph";
83         final static String     export_command = "export";
84         final static String     load_maps_command = "loadmaps";
85         final static String     preferences_command = "preferences";
86         final static String     close_command = "close";
87         final static String     exit_command = "exit";
88
89         static final String[][] file_menu_entries = new String[][] {
90                 { "New Window",         new_command },
91                 { "Graph Data",         graph_command },
92                 { "Export Data",        export_command },
93                 { "Load Maps",          load_maps_command },
94                 { "Preferences",        preferences_command },
95                 { "Close",              close_command },
96                 { "Exit",               exit_command },
97         };
98
99         /* Monitor menu */
100         final static String     connect_command = "connect";
101         final static String     disconnect_command = "disconnect";
102         final static String     scan_command = "scan";
103
104         static final String[][] monitor_menu_entries = new String[][] {
105                 { "Connect Device",     connect_command },
106                 { "Disconnect",         disconnect_command },
107                 { "Scan Channels",      scan_command },
108         };
109
110         /* Device menu */
111         final static String     download_command = "download";
112         final static String     configure_command = "configure";
113         final static String     flash_command = "flash";
114
115         static final String[][] device_menu_entries = new String[][] {
116                 { "Download Data",      download_command },
117                 { "Configure Device",   configure_command },
118                 { "Flash Device",       flash_command },
119         };
120
121         void stop_display() {
122                 if (thread != null && thread.isAlive()) {
123                         thread.interrupt();
124                         try {
125                                 thread.join();
126                         } catch (InterruptedException ie) {}
127                 }
128                 thread = null;
129         }
130
131         public void reset() {
132                 for (AltosFlightDisplay display : displays)
133                         display.reset();
134         }
135
136         public void font_size_changed(int font_size) {
137                 for (AltosFlightDisplay display : displays)
138                         display.font_size_changed(font_size);
139         }
140
141         public void units_changed(boolean imperial_units) {
142                 for (AltosFlightDisplay display : displays)
143                         display.units_changed(imperial_units);
144         }
145
146         public void show(AltosState state, AltosListenerState listener_state) {
147                 try {
148                         status_update.saved_state = state;
149                         status_update.saved_listener_state = listener_state;
150
151                         if (state == null)
152                                 state = new AltosState();
153
154                         int i = 0;
155                         for (AltosFlightDisplay display : displays) {
156                                 display.show(state, listener_state);
157                                 i++;
158                         }
159                 } catch (Exception ex) {
160                         System.out.printf("Exception %s\n", ex.toString());
161                         for (StackTraceElement e : ex.getStackTrace())
162                                 System.out.printf("%s\n", e.toString());
163                 }
164         }
165
166         void new_window() {
167                 new TeleGPS();
168         }
169
170         void preferences() {
171                 new TeleGPSPreferences(this, voice());
172         }
173
174         void load_maps() {
175                 new AltosUIMapPreload(this);
176         }
177
178         void disconnect() {
179                 setTitle("TeleGPS");
180                 stop_display();
181                 telegps_status.stop();
182
183                 telegps_status.disable_receive();
184                 disable_frequency_menu();
185                 disable_rate_menu();
186         }
187
188         void connect(AltosDevice device) {
189                 if (reader != null)
190                         disconnect();
191                 try {
192                         AltosFlightReader       reader = new AltosTelemetryReader(new AltosSerial(device));
193                         set_reader(reader, device);
194                 } catch (FileNotFoundException ee) {
195                         JOptionPane.showMessageDialog(this,
196                                                       ee.getMessage(),
197                                                       String.format ("Cannot open %s", device.toShortString()),
198                                                       JOptionPane.ERROR_MESSAGE);
199                 } catch (AltosSerialInUseException si) {
200                         JOptionPane.showMessageDialog(this,
201                                                       String.format("Device \"%s\" already in use",
202                                                                     device.toShortString()),
203                                                       "Device in use",
204                                                       JOptionPane.ERROR_MESSAGE);
205                 } catch (IOException ee) {
206                         JOptionPane.showMessageDialog(this,
207                                                       String.format ("Unknown I/O error on %s", device.toShortString()),
208                                                       "Unknown I/O error",
209                                                       JOptionPane.ERROR_MESSAGE);
210                 } catch (TimeoutException te) {
211                         JOptionPane.showMessageDialog(this,
212                                                       String.format ("Timeout on %s", device.toShortString()),
213                                                       "Timeout error",
214                                                       JOptionPane.ERROR_MESSAGE);
215                 } catch (InterruptedException ie) {
216                         JOptionPane.showMessageDialog(this,
217                                                       String.format("Interrupted %s", device.toShortString()),
218                                                       "Interrupted exception",
219                                                       JOptionPane.ERROR_MESSAGE);
220                 }
221         }
222
223         void connect() {
224                 AltosDevice     device = AltosDeviceUIDialog.show(this,
225                                                                   AltosLib.product_basestation);
226                 if (device == null)
227                         return;
228                 connect(device);
229         }
230
231         public void scan_device_selected(AltosDevice device) {
232                 connect(device);
233         }
234
235         void scan() {
236                 new AltosScanUI(this, false);
237         }
238
239         void download(){
240                 new AltosEepromManage(this, AltosLib.product_telegps);
241         }
242
243         void configure() {
244                 new TeleGPSConfig(this);
245         }
246
247         void export() {
248                 AltosDataChooser chooser;
249                 chooser = new AltosDataChooser(this);
250                 AltosStateIterable states = chooser.runDialog();
251                 if (states == null)
252                         return;
253                 new AltosCSVUI(this, states, chooser.file());
254         }
255
256         void graph() {
257                 AltosDataChooser chooser;
258                 chooser = new AltosDataChooser(this);
259                 AltosStateIterable states = chooser.runDialog();
260                 if (states == null)
261                         return;
262                 try {
263                         new TeleGPSGraphUI(states, chooser.file());
264                 } catch (InterruptedException ie) {
265                 } catch (IOException ie) {
266                 }
267         }
268
269         void flash() {
270                 AltosFlashUI.show(this);
271         }
272
273         public void actionPerformed(ActionEvent ev) {
274
275                 /* File menu */
276                 if (new_command.equals(ev.getActionCommand())) {
277                         new_window();
278                         return;
279                 }
280                 if (preferences_command.equals(ev.getActionCommand())) {
281                         preferences();
282                         return;
283                 }
284                 if (load_maps_command.equals(ev.getActionCommand())) {
285                         load_maps();
286                         return;
287                 }
288                 if (close_command.equals(ev.getActionCommand())) {
289                         close();
290                         return;
291                 }
292                 if (exit_command.equals(ev.getActionCommand()))
293                         System.exit(0);
294
295                 /* Monitor menu */
296                 if (connect_command.equals(ev.getActionCommand())) {
297                         connect();
298                         return;
299                 }
300                 if (disconnect_command.equals(ev.getActionCommand())) {
301                         disconnect();
302                         return;
303                 }
304                 if (scan_command.equals(ev.getActionCommand())) {
305                         scan();
306                         return;
307                 }
308
309                 /* Device menu */
310                 if (download_command.equals(ev.getActionCommand())) {
311                         download();
312                         return;
313                 }
314                 if (configure_command.equals(ev.getActionCommand())) {
315                         configure();
316                         return;
317                 }
318                 if (export_command.equals(ev.getActionCommand())) {
319                         export();
320                         return;
321                 }
322                 if (graph_command.equals(ev.getActionCommand())) {
323                         graph();
324                         return;
325                 }
326                 if (flash_command.equals(ev.getActionCommand())) {
327                         flash();
328                         return;
329                 }
330         }
331
332         void enable_frequency_menu(int serial, final AltosFlightReader reader) {
333
334                 if (frequency_listener != null)
335                         disable_frequency_menu();
336
337                 frequency_listener = new ActionListener() {
338                                 public void actionPerformed(ActionEvent e) {
339                                         double frequency = frequencies.frequency();
340                                         try {
341                                                 reader.set_frequency(frequency);
342                                         } catch (TimeoutException te) {
343                                         } catch (InterruptedException ie) {
344                                         }
345                                         reader.save_frequency();
346                                 }
347                         };
348
349                 frequencies.addActionListener(frequency_listener);
350                 frequencies.set_product("Monitor");
351                 frequencies.set_serial(serial);
352                 frequencies.set_frequency(AltosUIPreferences.frequency(serial));
353                 frequencies.setEnabled(true);
354
355         }
356
357         void disable_frequency_menu() {
358                 if (frequency_listener != null) {
359                         frequencies.removeActionListener(frequency_listener);
360                         frequencies.setEnabled(false);
361                         frequency_listener = null;
362                 }
363
364         }
365
366         void enable_rate_menu(int serial, final AltosFlightReader reader) {
367
368                 if (rate_listener != null)
369                         disable_rate_menu();
370
371                 rate_listener = new ActionListener() {
372                                 public void actionPerformed(ActionEvent e) {
373                                         int rate = rates.rate();
374                                         try {
375                                                 System.out.printf("set rate %d\n", rate);
376                                                 reader.set_telemetry_rate(rate);
377                                         } catch (TimeoutException te) {
378                                         } catch (InterruptedException ie) {
379                                         }
380                                         reader.save_telemetry_rate();
381                                 }
382                         };
383
384                 rates.addActionListener(rate_listener);
385                 rates.set_product("Monitor");
386                 rates.set_serial(serial);
387                 rates.set_rate(AltosUIPreferences.telemetry_rate(serial));
388                 rates.setEnabled(reader.supports_telemetry_rate(AltosLib.ao_telemetry_rate_2400));
389         }
390
391         void disable_rate_menu() {
392                 if (rate_listener != null) {
393                         rates.removeActionListener(rate_listener);
394                         rates.setEnabled(false);
395                         rate_listener = null;
396                 }
397
398         }
399
400         public void set_reader(AltosFlightReader reader, AltosDevice device) {
401                 status_update = new TeleGPSStatusUpdate(telegps_status);
402
403                 telegps_status.start(status_update);
404
405                 setTitle(String.format("TeleGPS %s", reader.name));
406                 thread = new TeleGPSDisplayThread(this, voice(), this, reader);
407                 thread.start();
408
409                 if (device != null) {
410                         enable_frequency_menu(device.getSerial(), reader);
411                         enable_rate_menu(device.getSerial(), reader);
412                 }
413         }
414
415         static int      number_of_windows;
416
417         static public void add_window() {
418                 ++number_of_windows;
419         }
420
421         static public void subtract_window() {
422                 --number_of_windows;
423                 if (number_of_windows == 0)
424                         System.exit(0);
425         }
426
427         private void close() {
428                 disconnect();
429                 AltosUIPreferences.unregister_font_listener(this);
430                 AltosPreferences.unregister_units_listener(this);
431                 setVisible(false);
432                 dispose();
433                 subtract_window();
434         }
435
436         private void add_menu(JMenu menu, String label, String action) {
437                 JMenuItem       item = new JMenuItem(label);
438                 menu.add(item);
439                 item.addActionListener(this);
440                 item.setActionCommand(action);
441         }
442
443
444         private JMenu make_menu(String label, String[][] items) {
445                 JMenu   menu = new JMenu(label);
446                 for (int i = 0; i < items.length; i++) {
447                         if (MAC_OS_X) {
448                                 if (items[i][1].equals("exit"))
449                                         continue;
450                                 if (items[i][1].equals("preferences"))
451                                         continue;
452                         }
453                         add_menu(menu, items[i][0], items[i][1]);
454                 }
455                 menu_bar.add(menu);
456                 return menu;
457         }
458
459         /* OSXAdapter interfaces */
460         public void macosx_file_handler(String path) {
461                 process_graph(new File(path));
462         }
463
464         public void macosx_quit_handler() {
465                 System.exit(0);
466         }
467
468         public void macosx_preferences_handler() {
469                 preferences();
470         }
471
472         public TeleGPS() {
473
474                 AltosUIPreferences.set_component(this);
475
476                 register_for_macosx_events();
477
478                 reader = null;
479
480                 bag = getContentPane();
481                 bag.setLayout(new GridBagLayout());
482
483                 GridBagConstraints c = new GridBagConstraints();
484
485                 setTitle("TeleGPS");
486
487                 menu_bar = new JMenuBar();
488                 setJMenuBar(menu_bar);
489
490                 file_menu = make_menu("File", file_menu_entries);
491                 monitor_menu = make_menu("Monitor", monitor_menu_entries);
492                 device_menu = make_menu("Device", device_menu_entries);
493
494                 frequencies = new AltosUIFreqList();
495                 frequencies.setEnabled(false);
496                 c.gridx = 0;
497                 c.gridy = 0;
498                 c.fill = GridBagConstraints.NONE;
499                 c.anchor = GridBagConstraints.WEST;
500                 c.weightx = 0;
501                 c.gridwidth = 1;
502                 bag.add(frequencies, c);
503
504                 rates = new AltosUIRateList();
505                 rates.setEnabled(false);
506                 c.gridx = 1;
507                 c.gridy = 0;
508                 c.fill = GridBagConstraints.NONE;
509                 c.anchor = GridBagConstraints.WEST;
510                 c.weightx = 0;
511                 c.gridwidth = 1;
512                 bag.add(rates, c);
513
514                 displays = new LinkedList<AltosFlightDisplay>();
515
516                 int serial = -1;
517
518                 /* TeleGPS status is always visible */
519                 telegps_status = new TeleGPSStatus();
520                 c.gridx = 0;
521                 c.gridy = 1;
522                 c.fill = GridBagConstraints.HORIZONTAL;
523                 c.weightx = 1;
524                 c.gridwidth = 2;
525                 bag.add(telegps_status, c);
526                 c.gridwidth = 1;
527                 displays.add(telegps_status);
528
529
530                 /* The rest of the window uses a tabbed pane to
531                  * show one of the alternate data views
532                  */
533                 pane = new JTabbedPane();
534
535                 /* Make the tabbed pane use the rest of the window space */
536                 c.gridx = 0;
537                 c.gridy = 2;
538                 c.fill = GridBagConstraints.BOTH;
539                 c.weightx = 1;
540                 c.weighty = 1;
541                 c.gridwidth = 2;
542                 bag.add(pane, c);
543
544                 map = new AltosUIMap();
545                 pane.add(map.getName(), map);
546                 displays.add(map);
547
548                 gps_info = new TeleGPSInfo();
549                 pane.add(gps_info.getName(), gps_info);
550                 displays.add(gps_info);
551
552                 gps_state = new TeleGPSState();
553                 pane.add(gps_state.getName(), gps_state);
554                 displays.add(gps_state);
555
556                 info_table = new AltosInfoTable();
557                 pane.add("Table", info_table);
558                 displays.add(info_table);
559
560                 setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
561
562                 AltosUIPreferences.register_font_listener(this);
563                 AltosPreferences.register_units_listener(this);
564
565                 addWindowListener(new WindowAdapter() {
566                                 @Override
567                                 public void windowClosing(WindowEvent e) {
568                                         close();
569                                 }
570                         });
571
572                 pack();
573                 setVisible(true);
574
575                 add_window();
576         }
577
578         public TeleGPS(AltosFlightReader reader) {
579                 this();
580                 set_reader(reader, null);
581         }
582
583         public TeleGPS(AltosDevice device) {
584                 this();
585                 connect(device);
586         }
587
588         static AltosStateIterable record_iterable(File file) {
589                 FileInputStream in;
590                 try {
591                         in = new FileInputStream(file);
592                 } catch (Exception e) {
593                         System.out.printf("Failed to open file '%s'\n", file);
594                         return null;
595                 }
596                 if (file.getName().endsWith("telem"))
597                         return new AltosTelemetryFile(in);
598                 else
599                         return new AltosEepromFile(in);
600         }
601
602         static AltosReplayReader replay_file(File file) {
603                 AltosStateIterable states = record_iterable(file);
604                 if (states == null)
605                         return null;
606                 return new AltosReplayReader(states.iterator(), file);
607         }
608
609         static boolean process_graph(File file) {
610                 AltosStateIterable states = record_iterable(file);
611                 if (states == null)
612                         return false;
613                 try {
614                         new TeleGPSGraphUI(states, file);
615                 } catch (Exception e) {
616                         return false;
617                 }
618                 return true;
619         }
620
621         static boolean process_replay(File file) {
622                 AltosReplayReader new_reader = replay_file(file);
623                 if (new_reader == null)
624                         return false;
625
626                 new TeleGPS(new_reader);
627                 return true;
628         }
629
630         static final int process_none = 0;
631         static final int process_csv = 1;
632         static final int process_kml = 2;
633         static final int process_graph = 3;
634         static final int process_replay = 4;
635         static final int process_summary = 5;
636         static final int process_cat = 6;
637
638         public static boolean load_library(Frame frame) {
639                 if (!AltosUILib.load_library()) {
640                         JOptionPane.showMessageDialog(frame,
641                                                       String.format("No AltOS library in \"%s\"",
642                                                                     System.getProperty("java.library.path","<undefined>")),
643                                                       "Cannot load device access library",
644                                                       JOptionPane.ERROR_MESSAGE);
645                         return false;
646                 }
647                 return true;
648         }
649
650         public static void help(int code) {
651                 System.out.printf("Usage: altosui [OPTION]... [FILE]...\n");
652                 System.out.printf("  Options:\n");
653                 System.out.printf("    --fetchmaps <lat> <lon>\tpre-fetch maps for site map view\n");
654                 System.out.printf("    --replay <filename>\t\trelive the glory of past flights \n");
655                 System.out.printf("    --graph <filename>\t\tgraph a flight\n");
656                 System.out.printf("    --csv\tgenerate comma separated output for spreadsheets, etc\n");
657                 System.out.printf("    --kml\tgenerate KML output for use with Google Earth\n");
658                 System.exit(code);
659         }
660
661         public static void main(String[] args) {
662                 int     errors = 0;
663
664                 load_library(null);
665                 try {
666                         UIManager.setLookAndFeel(AltosUIPreferences.look_and_feel());
667                 } catch (Exception e) {
668                 }
669
670                 boolean any_created = false;
671
672
673                 /* Handle batch-mode */
674                 int process = process_none;
675                 for (int i = 0; i < args.length; i++) {
676                         if (args[i].equals("--help"))
677                                 help(0);
678                         else if (args[i].equals("--fetchmaps")) {
679                                 if (args.length < i + 3) {
680                                         help(1);
681                                 } else {
682                                         double lat = Double.parseDouble(args[i+1]);
683                                         double lon = Double.parseDouble(args[i+2]);
684                                         AltosUIMap.prefetch_maps(lat, lon);
685                                         i += 2;
686                                 }
687                         } else if (args[i].equals("--replay"))
688                                 process = process_replay;
689                         else if (args[i].equals("--kml"))
690                                 process = process_kml;
691                         else if (args[i].equals("--csv"))
692                                 process = process_csv;
693                         else if (args[i].equals("--graph"))
694                                 process = process_graph;
695                         else if (args[i].equals("--summary"))
696                                 process = process_summary;
697                         else if (args[i].equals("--cat"))
698                                 process = process_cat;
699                         else if (args[i].startsWith("--"))
700                                 help(1);
701                         else {
702                                 File file = new File(args[i]);
703                                 switch (process) {
704                                 case process_none:
705                                 case process_graph:
706                                         if (!process_graph(file))
707                                                 ++errors;
708                                         break;
709                                 case process_replay:
710                                         if (!process_replay(file))
711                                                 ++errors;
712                                         any_created = true;
713                                         break;
714                                 case process_kml:
715                                         ++errors;
716                                         break;
717                                 case process_csv:
718                                         ++errors;
719                                         break;
720                                 case process_summary:
721                                         ++errors;
722                                         break;
723                                 case process_cat:
724                                         ++errors;
725                                 }
726                         }
727                 }
728                 if (errors != 0)
729                         System.exit(errors);
730                 if (number_of_windows == 0) {
731                         java.util.List<AltosDevice> devices = AltosUSBDevice.list(AltosLib.product_basestation);
732                         if (devices != null)
733                                 for (AltosDevice device : devices) {
734                                         new TeleGPS(device);
735                                         any_created = true;
736                                 }
737                         if (number_of_windows == 0)
738                                 new TeleGPS();
739                 }
740         }
741 }