From: Keith Packard Date: Sun, 21 Nov 2010 00:35:48 +0000 (-0800) Subject: Merge remote branch 'aj/sitemap' into buttonbox X-Git-Tag: debian/0.7.1+117+ge7954c8~8 X-Git-Url: https://git.gag.com/?p=fw%2Faltos;a=commitdiff_plain;h=8df185cd95cfecbed8272dd1275d077c5b45535b;hp=-c Merge remote branch 'aj/sitemap' into buttonbox Conflicts: ao-tools/altosui/AltosFlightUI.java Signed-off-by: Keith Packard --- 8df185cd95cfecbed8272dd1275d077c5b45535b diff --combined ao-tools/altosui/AltosFlightUI.java index d5bcdb10,c85fc977..a3a28782 --- a/ao-tools/altosui/AltosFlightUI.java +++ b/ao-tools/altosui/AltosFlightUI.java @@@ -42,8 -42,10 +42,9 @@@ public class AltosFlightUI extends JFra AltosAscent ascent; AltosDescent descent; AltosLanded landed; - AltosSiteMap sitemap; ++ AltosSiteMap sitemap; private AltosFlightStatus flightStatus; - private JScrollPane flightInfoPane; private AltosInfoTable flightInfo; static final int tab_pad = 1; @@@ -65,6 -67,14 +66,6 @@@ return tab_landed; } - public int width() { - return flightInfo.width(); - } - - public int height() { - return flightStatus.height() + flightInfo.height(); - } - void stop_display() { if (thread != null && thread.isAlive()) { thread.interrupt(); @@@ -85,6 -95,7 +86,7 @@@ descent.reset(); landed.reset(); flightInfo.clear(); + sitemap.reset(); } public void show(AltosState state, int crc_errors) { @@@ -111,6 -122,7 +113,7 @@@ } flightStatus.show(state, crc_errors); flightInfo.show(state, crc_errors); + sitemap.show(state, crc_errors); } public void set_exit_on_close() { @@@ -137,7 -149,6 +140,7 @@@ 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)); @@@ -154,7 -165,6 +157,7 @@@ bag.add (channels, c); } + /* Flight status is always visible */ flightStatus = new AltosFlightStatus(); c.gridx = 0; c.gridy = 1; @@@ -162,9 -172,6 +165,9 @@@ 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(); @@@ -180,9 -187,12 +183,12 @@@ pane.add("Landed", landed); flightInfo = new AltosInfoTable(); - flightInfoPane = new JScrollPane(flightInfo.box()); - pane.add("Table", flightInfoPane); + pane.add("Table", new JScrollPane(flightInfo)); - sitemap = new AltosSiteMap(); - pane.add("Site Map", sitemap); ++ 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; @@@ -190,20 -200,22 +196,20 @@@ c.weighty = 1; bag.add(pane, c); - this.setSize(this.getPreferredSize()); - this.validate(); - 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); -- } -- }); - - this.setVisible(true); ++ @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); diff --combined ao-tools/altosui/AltosInfoTable.java index c571d5c9,9f2bef5b..723f8301 --- a/ao-tools/altosui/AltosInfoTable.java +++ b/ao-tools/altosui/AltosInfoTable.java @@@ -28,44 -28,70 +28,44 @@@ import java.text.* import java.util.prefs.*; import java.util.concurrent.LinkedBlockingQueue; -public class AltosInfoTable { - private Box box; - private JTable table[]; - private AltosFlightInfoTableModel model[]; - private Box ibox[]; +public class AltosInfoTable extends JTable { + private AltosFlightInfoTableModel model; - private Font infoLabelFont = new Font("SansSerif", Font.PLAIN, 12); - private Font infoValueFont = new Font("Monospaced", Font.PLAIN, 12); + private Font infoLabelFont = new Font("SansSerif", Font.PLAIN, 14); + private Font infoValueFont = new Font("Monospaced", Font.PLAIN, 14); static final int info_columns = 3; static final int info_rows = 17; - public AltosInfoTable() { - box = Box.createHorizontalBox(); - model = new AltosFlightInfoTableModel[info_columns]; - table = new JTable[info_columns]; - ibox = new Box[info_columns]; - for (int i = 0; i < info_columns; i++) { - model[i] = new AltosFlightInfoTableModel(); - table[i] = new JTable(model[i]); - ibox[i] = box.createVerticalBox(); - - table[i].setFont(infoValueFont); - table[i].setRowHeight(rowHeight()); - table[i].setShowGrid(true); - ibox[i].add(table[i].getTableHeader()); - ibox[i].add(table[i]); - box.add(ibox[i]); - } - } - - public int rowHeight() { - FontMetrics infoValueMetrics = table[0].getFontMetrics(infoValueFont); - return (infoValueMetrics.getHeight() + infoValueMetrics.getLeading()) * 20 / 10; - } - - public int columnWidth() { - FontMetrics infoValueMetrics = table[0].getFontMetrics(infoValueFont); - return infoValueMetrics.charWidth('0') * 20 * 2; - } - - public int height() { - return rowHeight() * info_rows; + int desired_row_height() { + FontMetrics infoValueMetrics = getFontMetrics(infoValueFont); + return (infoValueMetrics.getHeight() + infoValueMetrics.getLeading()) * 18 / 10; } - public int width() { - return columnWidth() * info_columns; + public AltosInfoTable() { + super(new AltosFlightInfoTableModel(info_rows, info_columns)); + model = (AltosFlightInfoTableModel) getModel(); + setFont(infoValueFont); + setAutoResizeMode(AUTO_RESIZE_ALL_COLUMNS); + setShowGrid(true); + setRowHeight(desired_row_height()); + doLayout(); } - public Box box() { - return box; + public Dimension getPreferredScrollableViewportSize() { + return getPreferredSize(); } void info_reset() { - for (int i = 0; i < info_columns; i++) - model[i].resetRow(); + model.reset(); } void info_add_row(int col, String name, String value) { - model[col].addRow(name, value); + model.addRow(col, name, value); } void info_add_row(int col, String name, String format, Object... parameters) { - model[col].addRow(name, String.format(format, parameters)); + info_add_row (col, name, String.format(format, parameters)); } void info_add_deg(int col, String name, double v, int pos, int neg) { @@@ -77,15 -103,17 +77,15 @@@ double deg = Math.floor(v); double min = (v - deg) * 60; - model[col].addRow(name, String.format("%3.0f°%08.5f'", deg, min)); + info_add_row(col, name, String.format("%3.0f°%08.5f'", deg, min)); } void info_finish() { - for (int i = 0; i < info_columns; i++) - model[i].finish(); + model.finish(); } public void clear() { - info_reset(); - info_finish(); + model.clear(); } public void show(AltosState state, int crc_errors) { diff --combined ao-tools/altosui/AltosSiteMap.java index 00000000,5f5e30f0..50177d4e mode 000000,100644..100644 --- a/ao-tools/altosui/AltosSiteMap.java +++ b/ao-tools/altosui/AltosSiteMap.java @@@ -1,0 -1,269 +1,268 @@@ + /* + * Copyright © 2010 Anthony Towns + * + * 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 { + // max vertical step in a tile in naut. miles + static final double tile_size_nmi = 1.0; + + static final int px_size = 512; + + 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); + } + + AltosSiteMapTile [] mapTiles = new AltosSiteMapTile[9]; + Point2D.Double [] tileOffset = new Point2D.Double[9]; + + 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/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 int i, + final File pngfile, final String pngurl) + { + Thread thread = new Thread() { + public void run() { + ImageIcon res; + res = AltosSiteMapCache.fetchAndLoadMap(pngfile, pngurl); + if (res != null) { + mapTiles[i].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); + Point2D.Double c = 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( + -c.x + x*px_size + px_size/2, + -c.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 initMaps(double lat, double lng) { + Point2D.Double c = getBaseLocation(lat, lng); + Point2D.Double p = new Point2D.Double(); + + for (int i = 0; i < 9; i++) { + int x = i%3 - 1, y = i/3 - 1; + + tileOffset[i] = new Point2D.Double( + c.x - x*px_size, p.y = c.y - y*px_size); + LatLng map_latlng = latlng( + -tileOffset[i].x+px_size/2, + -tileOffset[i].y+px_size/2); + + File pngfile = MapFile(map_latlng.lat, map_latlng.lng); + String pngurl = MapURL(map_latlng.lat, map_latlng.lng); + bgLoadMap(i, pngfile, pngurl); + } + } + + 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; + public void show(AltosState state, int crc_errors) { + // if insufficient gps data, nothing to update - if (!state.gps_ready) { ++ if (state.gps == null || !state.gps.locked) { + if (state.pad_lat == 0 && state.pad_lon == 0) + return; + if (state.ngps < 3) + return; + } + + if (!initialised) { + initMaps(state.pad_lat, state.pad_lon); + initialised = true; + } + + Point2D.Double pt = pt(state.gps.lat, state.gps.lon); + for (int x = 0; x < mapTiles.length; x++) { + mapTiles[x].show(state, crc_errors, + translatePoint(pt, tileOffset[x])); + } + } + + private AltosSiteMap(boolean knowWhatYouAreDoing) { + if (!knowWhatYouAreDoing) { + throw new RuntimeException("Arggh."); + } + } + + public AltosSiteMap() { + JComponent comp = new JComponent() { + GrabNDrag scroller = new GrabNDrag(this); + { + addMouseMotionListener(scroller); + addMouseListener(scroller); + setAutoscrolls(true); + } + }; + + GridBagLayout layout = new GridBagLayout(); + comp.setLayout(layout); + + 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); + for (int x = 0; x < 9; x++) { + c.gridx = x % 3; + c.gridy = x / 3; + mapTiles[x] = new AltosSiteMapTile(px_size); + layout.setConstraints(mapTiles[x], c); + comp.add(mapTiles[x]); + } + setViewportView(comp); + setPreferredSize(new Dimension(500,200)); + } + } - diff --combined ao-tools/altosui/AltosSiteMapCache.java index 00000000,e9dbf8e6..2e62cc45 mode 000000,100644..100644 --- a/ao-tools/altosui/AltosSiteMapCache.java +++ b/ao-tools/altosui/AltosSiteMapCache.java @@@ -1,0 -1,103 +1,103 @@@ + /* + * Copyright © 2010 Anthony Towns + * + * 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.imageio.ImageIO; + import javax.swing.table.*; + import java.io.*; + import java.util.*; + import java.text.*; + import java.util.prefs.*; + import java.net.URL; + import java.net.URLConnection; + + public class AltosSiteMapCache extends JLabel { + public static boolean fetchMap(File file, String url) { + URL u; ++ + try { + u = new URL(url); + } catch (java.net.MalformedURLException e) { + return false; + } + + byte[] data; + try { + URLConnection uc = u.openConnection(); + int contentLength = uc.getContentLength(); + InputStream in = new BufferedInputStream(uc.getInputStream()); + int bytesRead = 0; + int offset = 0; + data = new byte[contentLength]; + while (offset < contentLength) { + bytesRead = in.read(data, offset, data.length - offset); + if (bytesRead == -1) + break; + offset += bytesRead; + } + in.close(); + + if (offset != contentLength) { + return false; + } + } catch (IOException e) { + return false; + } + + try { + FileOutputStream out = new FileOutputStream(file); + out.write(data); + out.flush(); + out.close(); + } catch (FileNotFoundException e) { + return false; + } catch (IOException e) { + if (file.exists()) { + file.delete(); + } + return false; + } + return true; + } + + public static ImageIcon fetchAndLoadMap(File pngfile, String url) { + if (!pngfile.exists()) { + if (!fetchMap(pngfile, url)) { + return null; + } + } + return loadMap(pngfile, url); + } + + public static ImageIcon loadMap(File pngfile, String url) { + if (!pngfile.exists()) { + return null; + } + + try { + return new ImageIcon(ImageIO.read(pngfile)); + } catch (IOException e) { + System.out.printf("# IO error trying to load %s\n", pngfile); + return null; + } + } + } - diff --combined ao-tools/altosui/AltosSiteMapTile.java index 00000000,8aee86c1..9d6f855d mode 000000,100644..100644 --- a/ao-tools/altosui/AltosSiteMapTile.java +++ b/ao-tools/altosui/AltosSiteMapTile.java @@@ -1,0 -1,134 +1,133 @@@ + /* + * Copyright © 2010 Anthony Towns + * + * 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.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 AltosSiteMapTile extends JLayeredPane { + Point2D.Double coord_pt; + Point2D.Double last_pt; + + JLabel mapLabel; + JLabel draw; + Graphics2D g2d; + + public void loadMap(ImageIcon icn) { + mapLabel.setIcon(icn); + } + + static Color stateColors[] = { + Color.WHITE, // startup + Color.WHITE, // idle + Color.WHITE, // pad + Color.RED, // boost + Color.PINK, // fast + Color.YELLOW, // coast + Color.CYAN, // drogue + Color.BLUE, // main + Color.BLACK // landed + }; + + boolean drawn_landed_circle = false; + boolean drawn_boost_circle = false; + public void show(AltosState state, int crc_errors, Point2D.Double pt) { + if (last_pt == null) { + // setLocation(state.pad_lat, state.pad_lon); + // loadMap(); + last_pt = pt; + } + + if (pt != last_pt) { + if (0 <= state.state && state.state < stateColors.length) { + g2d.setColor(stateColors[state.state]); + } + g2d.draw(new Line2D.Double(last_pt, pt)); + } + + int px_size = getWidth(); + if (0 <= pt.x && pt.x < px_size) { + if (0 <= pt.y && pt.y < px_size) { + int dx = 500, dy = 250; + if (state.state > 2) { + dx = Math.min(200, 20 + (int) Math.abs(last_pt.x - pt.x)); + dy = Math.min(100, 10 + (int) Math.abs(last_pt.y - pt.y)); + } + Rectangle r = new Rectangle((int)pt.x-dx, (int)pt.y-dy, + dx*2, dy*2); + scrollRectToVisible(r); + } + } + + if (state.state == 3 && !drawn_boost_circle) { + drawn_boost_circle = true; + g2d.setColor(Color.RED); + g2d.drawOval((int)last_pt.x-5, (int)last_pt.y-5, 10, 10); + g2d.drawOval((int)last_pt.x-20, (int)last_pt.y-20, 40, 40); + g2d.drawOval((int)last_pt.x-35, (int)last_pt.y-35, 70, 70); + } + if (state.state == 8 && !drawn_landed_circle) { + drawn_landed_circle = true; + g2d.setColor(Color.BLACK); + g2d.drawOval((int)pt.x-5, (int)pt.y-5, 10, 10); + g2d.drawOval((int)pt.x-20, (int)pt.y-20, 40, 40); + g2d.drawOval((int)pt.x-35, (int)pt.y-35, 70, 70); + } + + last_pt = pt; + repaint(); + } + + public static Graphics2D fillLabel(JLabel l, Color c, int px_size) { + BufferedImage img = new BufferedImage(px_size, px_size, + BufferedImage.TYPE_INT_ARGB); + Graphics2D g = img.createGraphics(); + g.setColor(c); + g.fillRect(0, 0, px_size, px_size); + l.setIcon(new ImageIcon(img)); + return g; + } + + public AltosSiteMapTile(int px_size) { + setPreferredSize(new Dimension(px_size, px_size)); + + mapLabel = new JLabel(); + fillLabel(mapLabel, Color.GRAY, px_size); + mapLabel.setOpaque(true); + mapLabel.setBounds(0, 0, px_size, px_size); + add(mapLabel, new Integer(0)); + + draw = new JLabel(); + g2d = fillLabel(draw, new Color(127, 127, 127, 0), px_size); + draw.setBounds(0, 0, px_size, px_size); + draw.setOpaque(false); + + add(draw, new Integer(1)); + } + } -