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