Merge branch 'buttonbox' of git://git.gag.com/fw/altos into buttonbox
authorAnthony Towns <aj@erisian.com.au>
Wed, 24 Nov 2010 23:10:50 +0000 (09:10 +1000)
committerAnthony Towns <aj@erisian.com.au>
Wed, 24 Nov 2010 23:10:50 +0000 (09:10 +1000)
1  2 
altosui/AltosFlightUI.java
altosui/AltosSiteMap.java

index 0000000,24d25bd..7fcfb8b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,221 +1,205 @@@
 -      static final int tab_pad = 1;
 -      static final int tab_ascent = 2;
 -      static final int tab_descent = 3;
 -      static final int tab_landed = 4;
 -
 -      int cur_tab = 0;
 -
+ /*
+  * Copyright © 2010 Keith Packard <keithp@keithp.com>
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; version 2 of the License.
+  *
+  * This program is distributed in the hope that it will be useful, but
+  * WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  * General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+  */
+ package altosui;
+ import java.awt.*;
+ import java.awt.event.*;
+ import javax.swing.*;
+ import javax.swing.filechooser.FileNameExtensionFilter;
+ import javax.swing.table.*;
+ import java.io.*;
+ import java.util.*;
+ import java.text.*;
+ import java.util.prefs.*;
+ import java.util.concurrent.LinkedBlockingQueue;
+ public class AltosFlightUI extends JFrame implements AltosFlightDisplay {
+       String[] statusNames = { "Height (m)", "State", "RSSI (dBm)", "Speed (m/s)" };
+       Object[][] statusData = { { "0", "pad", "-50", "0" } };
+       AltosVoice              voice;
+       AltosFlightReader       reader;
+       AltosDisplayThread      thread;
+       JTabbedPane     pane;
+       AltosPad        pad;
+       AltosAscent     ascent;
+       AltosDescent    descent;
+       AltosLanded     landed;
+       AltosSiteMap    sitemap;
+       private AltosFlightStatus flightStatus;
+       private AltosInfoTable flightInfo;
 -      int which_tab(AltosState state) {
+       boolean exit_on_close = false;
 -                      return tab_pad;
++      JComponent cur_tab = null;
++      JComponent which_tab(AltosState state) {
+               if (state.state < Altos.ao_flight_boost)
 -                      return tab_ascent;
++                      return pad;
+               if (state.state <= Altos.ao_flight_coast)
 -                      return tab_descent;
 -              return tab_landed;
++                      return ascent;
+               if (state.state <= Altos.ao_flight_main)
 -              int     tab = which_tab(state);
++                      return descent;
++              return landed;
+       }
+       void stop_display() {
+               if (thread != null && thread.isAlive()) {
+                       thread.interrupt();
+                       try {
+                               thread.join();
+                       } catch (InterruptedException ie) {}
+               }
+               thread = null;
+       }
+       void disconnect() {
+               stop_display();
+       }
+       public void reset() {
+               pad.reset();
+               ascent.reset();
+               descent.reset();
+               landed.reset();
+               flightInfo.clear();
+               sitemap.reset();
+       }
+       public void show(AltosState state, int crc_errors) {
 -                      switch (tab) {
 -                      case tab_pad:
 -                              pane.setSelectedComponent(pad);
 -                              break;
 -                      case tab_ascent:
 -                              pane.setSelectedComponent(ascent);
 -                              break;
 -                      case tab_descent:
 -                              pane.setSelectedComponent(descent);
 -                              break;
 -                      case tab_landed:
 -                              pane.setSelectedComponent(landed);
++              JComponent tab = which_tab(state);
+               pad.show(state, crc_errors);
+               ascent.show(state, crc_errors);
+               descent.show(state, crc_errors);
+               landed.show(state, crc_errors);
+               if (tab != cur_tab) {
++                      if (cur_tab == pane.getSelectedComponent()) {
++                              pane.setSelectedComponent(tab);
+                       }
+                       cur_tab = tab;
+               }
+               flightStatus.show(state, crc_errors);
+               flightInfo.show(state, crc_errors);
+               sitemap.show(state, crc_errors);
+       }
+       public void set_exit_on_close() {
+               exit_on_close = true;
+       }
+       Container       bag;
+       JComboBox       channels;
+       public AltosFlightUI(AltosVoice in_voice, AltosFlightReader in_reader, final int serial) {
+               AltosPreferences.init(this);
+               voice = in_voice;
+               reader = in_reader;
+               bag = getContentPane();
+               bag.setLayout(new GridBagLayout());
+               GridBagConstraints c = new GridBagConstraints();
+               java.net.URL imgURL = AltosUI.class.getResource("/altus-metrum-16x16.jpg");
+               if (imgURL != null)
+                       setIconImage(new ImageIcon(imgURL).getImage());
+               setTitle(String.format("AltOS %s", reader.name));
+               /* Stick channel selector at top of table for telemetry monitoring */
+               if (serial >= 0) {
+                       // Channel menu
+                       channels = new AltosChannelMenu(AltosPreferences.channel(serial));
+                       channels.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       int channel = channels.getSelectedIndex();
+                                       reader.set_channel(channel);
+                               }
+                       });
+                       c.gridx = 0;
+                       c.gridy = 0;
+                       c.anchor = GridBagConstraints.WEST;
+                       bag.add (channels, c);
+               }
+               /* Flight status is always visible */
+               flightStatus = new AltosFlightStatus();
+               c.gridx = 0;
+               c.gridy = 1;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.weightx = 1;
+               bag.add(flightStatus, c);
+               /* The rest of the window uses a tabbed pane to
+                * show one of the alternate data views
+                */
+               pane = new JTabbedPane();
+               pad = new AltosPad();
+               pane.add("Launch Pad", pad);
+               ascent = new AltosAscent();
+               pane.add("Ascent", ascent);
+               descent = new AltosDescent();
+               pane.add("Descent", descent);
+               landed = new AltosLanded();
+               pane.add("Landed", landed);
+               flightInfo = new AltosInfoTable();
+               pane.add("Table", new JScrollPane(flightInfo));
+               sitemap = new AltosSiteMap();
+               pane.add("Site Map", sitemap);
+               /* Make the tabbed pane use the rest of the window space */
+               c.gridx = 0;
+               c.gridy = 2;
+               c.fill = GridBagConstraints.BOTH;
+               c.weightx = 1;
+               c.weighty = 1;
+               bag.add(pane, c);
+               setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+               addWindowListener(new WindowAdapter() {
+                               @Override
+                               public void windowClosing(WindowEvent e) {
+                                       disconnect();
+                                       setVisible(false);
+                                       dispose();
+                                       if (exit_on_close)
+                                               System.exit(0);
+                               }
+                       });
+               pack();
+               setVisible(true);
+               thread = new AltosDisplayThread(this, voice, this, reader);
+               thread.start();
+       }
+       public AltosFlightUI (AltosVoice in_voice, AltosFlightReader in_reader) {
+               this(in_voice, in_reader, -1);
+       }
+ }
index 0000000,8097060..d4a4cbf
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,388 +1,391 @@@
 -              if (state.pad_lat == 0 && state.pad_lon == 0)
+ /*
+  * Copyright © 2010 Anthony Towns <aj@erisian.com.au>
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; version 2 of the License.
+  *
+  * This program is distributed in the hope that it will be useful, but
+  * WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  * General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+  */
+ package altosui;
+ import java.awt.*;
+ import java.awt.image.*;
+ import java.awt.event.*;
+ import javax.swing.*;
+ import javax.swing.event.MouseInputAdapter;
+ import javax.imageio.ImageIO;
+ import javax.swing.table.*;
+ import java.io.*;
+ import java.util.*;
+ import java.text.*;
+ import java.util.prefs.*;
+ import java.lang.Math;
+ import java.awt.geom.Point2D;
+ import java.awt.geom.Line2D;
+ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
+       // preferred vertical step in a tile in naut. miles
+       // will actually choose a step size between x and 2x, where this
+       // is 1.5x
+       static final double tile_size_nmi = 0.75;
+       static final int px_size = 512;
+       static final int MAX_TILE_DELTA = 100;
+       private static Point2D.Double translatePoint(Point2D.Double p,
+                       Point2D.Double d)
+       {
+               return new Point2D.Double(p.x + d.x, p.y + d.y);
+       }
+       static class LatLng {
+               public double lat, lng;
+               public LatLng(double lat, double lng) {
+                       this.lat = lat;
+                       this.lng = lng;
+               }
+       }
+       // based on google js
+       //  http://maps.gstatic.com/intl/en_us/mapfiles/api-3/2/10/main.js
+       // search for fromLatLngToPoint and fromPointToLatLng
+       private static Point2D.Double pt(LatLng latlng, int zoom) {
+               double scale_x = 256/360.0 * Math.pow(2, zoom);
+               double scale_y = 256/(2.0*Math.PI) * Math.pow(2, zoom);
+               return pt(latlng, scale_x, scale_y);
+       }
+       private static Point2D.Double pt(LatLng latlng,
+                                        double scale_x, double scale_y)
+       {
+               Point2D.Double res = new Point2D.Double();
+               double e;
+               res.x = latlng.lng * scale_x;
+               e = Math.sin(Math.toRadians(latlng.lat));
+               e = Math.max(e,-(1-1.0E-15));
+               e = Math.min(e,  1-1.0E-15 );
+               res.y = 0.5*Math.log((1+e)/(1-e))*-scale_y;
+               return res;
+       }
+       static private LatLng latlng(Point2D.Double pt,
+                                    double scale_x, double scale_y)
+       {
+               double lat, lng;
+               double rads;
+               lng = pt.x/scale_x;
+               rads = 2 * Math.atan(Math.exp(-pt.y/scale_y));
+               lat = Math.toDegrees(rads - Math.PI/2);
+               return new LatLng(lat,lng);
+       }
+       int zoom;
+       double scale_x, scale_y;
+       private Point2D.Double pt(double lat, double lng) {
+               return pt(new LatLng(lat, lng), scale_x, scale_y);
+       }
+       private LatLng latlng(double x, double y) {
+               return latlng(new Point2D.Double(x,y), scale_x, scale_y);
+       }
+       private LatLng latlng(Point2D.Double pt) {
+               return latlng(pt, scale_x, scale_y);
+       }
+       HashMap<Point,AltosSiteMapTile> mapTiles = new HashMap<Point,AltosSiteMapTile>();
+       Point2D.Double centre;
+       private Point2D.Double tileCoordOffset(Point p) {
+               return new Point2D.Double(centre.x - p.x*px_size,
+                                         centre.y - p.y * px_size);
+       }
+       private Point tileOffset(Point2D.Double p) {
+               return new Point((int)Math.floor((centre.x+p.x)/px_size),
+                                (int)Math.floor((centre.y+p.y)/px_size));
+       }
+       private Point2D.Double getBaseLocation(double lat, double lng) {
+               Point2D.Double locn, north_step;
+               zoom = 2;
+               // stupid loop structure to please Java's control flow analysis
+               do {
+                       zoom++;
+                       scale_x = 256/360.0 * Math.pow(2, zoom);
+                       scale_y = 256/(2.0*Math.PI) * Math.pow(2, zoom);
+                       locn = pt(lat, lng);
+                       north_step = pt(lat+tile_size_nmi*4/3/60.0, lng);
+                       if (locn.y - north_step.y > px_size)
+                               break;
+               } while (zoom < 22);
+               locn.x = -px_size * Math.floor(locn.x/px_size);
+               locn.y = -px_size * Math.floor(locn.y/px_size);
+               return locn;
+       }
+       public void reset() {
+               // nothing
+       }
+       private void bgLoadMap(final AltosSiteMapTile tile,
+                              final File pngfile, final String pngurl)
+       {
+               //System.out.printf("Loading/fetching map %s\n", pngfile);
+               Thread thread = new Thread() {
+                       public void run() {
+                               ImageIcon res;
+                               res = AltosSiteMapCache.fetchAndLoadMap(pngfile, pngurl);
+                               if (res != null) {
+                                       tile.loadMap(res);
+                               } else {
+                                       System.out.printf("# Failed to fetch file %s\n", pngfile);
+                                       System.out.printf(" wget -O '%s' ''\n", pngfile, pngurl);
+                               }
+                       }
+               };
+               thread.start();
+       }
+       public static void prefetchMaps(double lat, double lng, int w, int h) {
+               AltosPreferences.init(null);
+               AltosSiteMap asm = new AltosSiteMap(true);
+               asm.centre = asm.getBaseLocation(lat, lng);
+               Point2D.Double p = new Point2D.Double();
+               Point2D.Double p2;
+               int dx = -w/2, dy = -h/2;
+               for (int y = dy; y < h+dy; y++) {
+                       for (int x = dx; x < w+dx; x++) {
+                               LatLng map_latlng = asm.latlng(
+                                                           -asm.centre.x + x*px_size + px_size/2,
+                                                           -asm.centre.y + y*px_size + px_size/2);
+                               File pngfile = asm.MapFile(map_latlng.lat, map_latlng.lng);
+                               String pngurl = asm.MapURL(map_latlng.lat, map_latlng.lng);
+                               if (pngfile.exists()) {
+                                       System.out.printf("Already have %s\n", pngfile);
+                               } else if (AltosSiteMapCache.fetchMap(pngfile, pngurl)) {
+                                       System.out.printf("Fetched map %s\n", pngfile);
+                               } else {
+                                       System.out.printf("# Failed to fetch file %s\n", pngfile);
+                                       System.out.printf(" wget -O '%s' ''\n", pngfile, pngurl);
+                               }
+                       }
+               }
+       }
+       private void initMap(AltosSiteMapTile tile, Point offset) {
+               Point2D.Double coord = tileCoordOffset(offset);
+               LatLng map_latlng = latlng(px_size/2-coord.x, px_size/2-coord.y);
+               File pngfile = MapFile(map_latlng.lat, map_latlng.lng);
+               String pngurl = MapURL(map_latlng.lat, map_latlng.lng);
+               bgLoadMap(tile, pngfile, pngurl);
+       }
+       private void initMaps(double lat, double lng) {
+               centre = getBaseLocation(lat, lng);
+               for (Point k : mapTiles.keySet()) {
+                       initMap(mapTiles.get(k), k);
+               }
+       }
+       private File MapFile(double lat, double lng) {
+               char chlat = lat < 0 ? 'S' : 'N';
+               char chlng = lng < 0 ? 'E' : 'W';
+               if (lat < 0) lat = -lat;
+               if (lng < 0) lng = -lng;
+               return new File(AltosPreferences.logdir(),
+                               String.format("map-%c%.6f,%c%.6f-%d.png",
+                                             chlat, lat, chlng, lng, zoom));
+       }
+       private String MapURL(double lat, double lng) {
+               return String.format("http://maps.google.com/maps/api/staticmap?center=%.6f,%.6f&zoom=%d&size=%dx%d&sensor=false&maptype=hybrid&format=png32", lat, lng, zoom, px_size, px_size);
+       }
+       boolean initialised = false;
+       Point2D.Double last_pt = null;
+       int last_state = -1;
+       public void show(final AltosState state, final int crc_errors) {
+               // if insufficient gps data, nothing to update
+               if (state.gps == null)
+                       return;
 -              if (!state.gps.locked) {
 -                      if (state.gps.nsat < 4)
 -                              return;
 -              }
++              if (!state.gps.locked && state.gps.nsat < 4)
+                       return;
 -                      initMaps(state.pad_lat, state.pad_lon);
 -                      initialised = true;
+               if (!initialised) {
++                      if (state.pad_lat != 0 || state.pad_lon != 0) {
++                              initMaps(state.pad_lat, state.pad_lon);
++                              initialised = true;
++                      } else if (state.gps.lat != 0 || state.gps.lon != 0) {
++                              initMaps(state.gps.lat, state.gps.lon);
++                              initialised = true;
++                      } else {
++                              return;
++                      }
+               }
+               final Point2D.Double pt = pt(state.gps.lat, state.gps.lon);
+               if (last_pt == pt && last_state == state.state)
+                       return;
+               if (last_pt == null) {
+                       last_pt = pt;
+               }
+               boolean in_any = false;
+               for (Point offset : mapTiles.keySet()) {
+                       AltosSiteMapTile tile = mapTiles.get(offset);
+                       Point2D.Double ref, lref;
+                       ref = translatePoint(pt, tileCoordOffset(offset));
+                       lref = translatePoint(last_pt, tileCoordOffset(offset));
+                       tile.show(state, crc_errors, lref, ref);
+                       if (0 <= ref.x && ref.x < px_size)
+                               if (0 <= ref.y && ref.y < px_size)
+                                       in_any = true;
+               }
+               Point offset = tileOffset(pt);
+               if (!in_any) {
+                       Point2D.Double ref, lref;
+                       ref = translatePoint(pt, tileCoordOffset(offset));
+                       lref = translatePoint(last_pt, tileCoordOffset(offset));
+                       AltosSiteMapTile tile = createTile(offset);
+                       tile.show(state, crc_errors, lref, ref);
+                       initMap(tile, offset);
+                       finishTileLater(tile, offset);
+               }
+               scrollRocketToVisible(pt);
+               if (offset != tileOffset(last_pt)) {
+                       ensureTilesAround(offset);
+               }
+               last_pt = pt;
+               last_state = state.state;
+       }
+       private AltosSiteMapTile createTile(Point offset) {
+               AltosSiteMapTile tile = new AltosSiteMapTile(px_size);
+               mapTiles.put(offset, tile);
+               return tile;
+       }
+       private void finishTileLater(final AltosSiteMapTile tile,
+                                    final Point offset)
+       {
+               SwingUtilities.invokeLater( new Runnable() {
+                       public void run() {
+                               addTileAt(tile, offset);
+                       }
+               } );
+       }
+       private void ensureTilesAround(Point base_offset) {
+               for (int x = -1; x <= 1; x++) {
+                       for (int y = -1; y <= 1; y++) {
+                               Point offset = new Point(base_offset.x + x, base_offset.y + y);
+                               if (mapTiles.containsKey(offset))
+                                       continue;
+                               AltosSiteMapTile tile = createTile(offset);
+                               initMap(tile, offset);
+                               finishTileLater(tile, offset);
+                       }
+               }
+       }
+       private Point topleft = new Point(0,0);
+       private void scrollRocketToVisible(Point2D.Double pt) {
+               Rectangle r = comp.getVisibleRect();
+               Point2D.Double copt = translatePoint(pt, tileCoordOffset(topleft));
+               int dx = (int)copt.x - r.width/2 - r.x;
+               int dy = (int)copt.y - r.height/2 - r.y;
+               if (Math.abs(dx) > r.width/4 || Math.abs(dy) > r.height/4) {
+                       r.x += dx;
+                       r.y += dy;
+                       comp.scrollRectToVisible(r);
+               }
+       }
+       private void addTileAt(AltosSiteMapTile tile, Point offset) {
+               if (Math.abs(offset.x) >= MAX_TILE_DELTA ||
+                               Math.abs(offset.y) >= MAX_TILE_DELTA)
+               {
+                       System.out.printf("Rocket too far away from pad (tile %d,%d)\n",
+                                         offset.x, offset.y);
+                       return;
+               }
+               boolean review = false;
+               Rectangle r = comp.getVisibleRect();
+               if (offset.x < topleft.x) {
+                       r.x += (topleft.x - offset.x) * px_size;
+                       topleft.x = offset.x;
+                       review = true;
+               }
+               if (offset.y < topleft.y) {
+                       r.y += (topleft.y - offset.y) * px_size;
+                       topleft.y = offset.y;
+                       review = true;
+               }
+               GridBagConstraints c = new GridBagConstraints();
+               c.anchor = GridBagConstraints.CENTER;
+               c.fill = GridBagConstraints.BOTH;
+               // put some space between the map tiles, debugging only
+               // c.insets = new Insets(5, 5, 5, 5);
+               c.gridx = offset.x + MAX_TILE_DELTA;
+               c.gridy = offset.y + MAX_TILE_DELTA;
+               layout.setConstraints(tile, c);
+               comp.add(tile);
+               if (review) {
+                       comp.scrollRectToVisible(r);
+               }
+       }
+       private AltosSiteMap(boolean knowWhatYouAreDoing) {
+               if (!knowWhatYouAreDoing) {
+                       throw new RuntimeException("Arggh.");
+               }
+       }
+       JComponent comp = new JComponent() { };
+       private GridBagLayout layout = new GridBagLayout();
+       public AltosSiteMap() {
+               GrabNDrag scroller = new GrabNDrag(comp);
+               comp.setLayout(layout);
+               for (int x = -1; x <= 1; x++) {
+                       for (int y = -1; y <= 1; y++) {
+                               Point offset = new Point(x, y);
+                               AltosSiteMapTile t = createTile(offset);
+                               addTileAt(t, offset);
+                       }
+               }
+               setViewportView(comp);
+               setPreferredSize(new Dimension(500,200));
+       }
+ }