summaryrefslogtreecommitdiff
path: root/altosuilib
diff options
context:
space:
mode:
authorKeith Packard <keithp@keithp.com>2014-05-25 20:55:11 -0700
committerKeith Packard <keithp@keithp.com>2014-05-25 20:56:48 -0700
commit0a6c76fc0525d6588a1d88127f0085f13a02f1af (patch)
treed7cf76748c86522708b0826237eb031f371e349d /altosuilib
parent4ac7797d3efb9cc2d9fae88519f55e40b1050224 (diff)
altosui/altosuilib/altoslib: Move more stuff out of autosui. Reduce site map memory
Prepare to share with TeleGPS application. This also has the changes to the site map tile which cache only a few images and regenerate the flight path on the fly, saving piles of memory Signed-off-by: Keith Packard <keithp@keithp.com>
Diffstat (limited to 'altosuilib')
-rw-r--r--altosuilib/AltosDisplayThread.java259
-rw-r--r--altosuilib/AltosFlightDisplay.java28
-rw-r--r--altosuilib/AltosFreqList.java85
-rw-r--r--altosuilib/AltosSiteMap.java484
-rw-r--r--altosuilib/AltosSiteMapCache.java194
-rw-r--r--altosuilib/AltosSiteMapPreload.java467
-rw-r--r--altosuilib/AltosSiteMapTile.java142
-rw-r--r--altosuilib/AltosVoice.java94
-rw-r--r--altosuilib/GrabNDrag.java48
-rw-r--r--altosuilib/Makefile.am13
10 files changed, 1812 insertions, 2 deletions
diff --git a/altosuilib/AltosDisplayThread.java b/altosuilib/AltosDisplayThread.java
new file mode 100644
index 00000000..e88a891e
--- /dev/null
+++ b/altosuilib/AltosDisplayThread.java
@@ -0,0 +1,259 @@
+/*
+ * 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 org.altusmetrum.altosuilib_2;
+
+import java.awt.*;
+import javax.swing.*;
+import java.io.*;
+import java.text.*;
+import org.altusmetrum.altoslib_4.*;
+
+public class AltosDisplayThread extends Thread {
+
+ Frame parent;
+ IdleThread idle_thread;
+ AltosVoice voice;
+ AltosFlightReader reader;
+ AltosState old_state, state;
+ AltosListenerState listener_state;
+ AltosFlightDisplay display;
+
+ synchronized void show_safely() {
+ final AltosState my_state = state;
+ final AltosListenerState my_listener_state = listener_state;
+ Runnable r = new Runnable() {
+ public void run() {
+ try {
+ display.show(my_state, my_listener_state);
+ } catch (Exception ex) {
+ }
+ }
+ };
+ SwingUtilities.invokeLater(r);
+ }
+
+ void reading_error_internal() {
+ JOptionPane.showMessageDialog(parent,
+ String.format("Error reading from \"%s\"", reader.name),
+ "Telemetry Read Error",
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ void reading_error_safely() {
+ Runnable r = new Runnable() {
+ public void run() {
+ try {
+ reading_error_internal();
+ } catch (Exception ex) {
+ }
+ }
+ };
+ SwingUtilities.invokeLater(r);
+ }
+
+ class IdleThread extends Thread {
+
+ boolean started;
+ int reported_landing;
+ int report_interval;
+ long report_time;
+
+ public synchronized void report(boolean last) {
+ if (state == null)
+ return;
+
+ /* reset the landing count once we hear about a new flight */
+ if (state.state < AltosLib.ao_flight_drogue)
+ reported_landing = 0;
+
+ /* Shut up once the rocket is on the ground */
+ if (reported_landing > 2) {
+ return;
+ }
+
+ /* If the rocket isn't on the pad, then report height */
+ if (AltosLib.ao_flight_drogue <= state.state &&
+ state.state < AltosLib.ao_flight_landed &&
+ state.from_pad != null &&
+ state.range >= 0)
+ {
+ voice.speak("Height %s, bearing %s %d, elevation %d, range %s.\n",
+ AltosConvert.height.say(state.height()),
+ state.from_pad.bearing_words(
+ AltosGreatCircle.BEARING_VOICE),
+ (int) (state.from_pad.bearing + 0.5),
+ (int) (state.elevation + 0.5),
+ AltosConvert.distance.say(state.range));
+ } else if (state.state > AltosLib.ao_flight_pad) {
+ voice.speak(AltosConvert.height.say_units(state.height()));
+ } else {
+ reported_landing = 0;
+ }
+
+ /* If the rocket is coming down, check to see if it has landed;
+ * either we've got a landed report or we haven't heard from it in
+ * a long time
+ */
+ if (state.state >= AltosLib.ao_flight_drogue &&
+ (last ||
+ System.currentTimeMillis() - state.received_time >= 15000 ||
+ state.state == AltosLib.ao_flight_landed))
+ {
+ if (Math.abs(state.speed()) < 20 && state.height() < 100)
+ voice.speak("rocket landed safely");
+ else
+ voice.speak("rocket may have crashed");
+ if (state.from_pad != null)
+ voice.speak("Bearing %d degrees, range %s.",
+ (int) (state.from_pad.bearing + 0.5),
+ AltosConvert.distance.say_units(state.from_pad.distance));
+ ++reported_landing;
+ if (state.state != AltosLib.ao_flight_landed) {
+ state.state = AltosLib.ao_flight_landed;
+ show_safely();
+ }
+ }
+ }
+
+ long now () {
+ return System.currentTimeMillis();
+ }
+
+ void set_report_time() {
+ report_time = now() + report_interval;
+ }
+
+ public void run () {
+ try {
+ for (;;) {
+ if (reader.has_monitor_battery()) {
+ listener_state.battery = reader.monitor_battery();
+ show_safely();
+ }
+ set_report_time();
+ for (;;) {
+ voice.drain();
+ synchronized (this) {
+ long sleep_time = report_time - now();
+ if (sleep_time <= 0)
+ break;
+ wait(sleep_time);
+ }
+ }
+
+ report(false);
+ }
+ } catch (InterruptedException ie) {
+ try {
+ voice.drain();
+ } catch (InterruptedException iie) { }
+ }
+ }
+
+ public synchronized void notice(boolean spoken) {
+ if (old_state != null && old_state.state != state.state) {
+ report_time = now();
+ this.notify();
+ } else if (spoken)
+ set_report_time();
+ }
+
+ public IdleThread() {
+ reported_landing = 0;
+ report_interval = 10000;
+ }
+ }
+
+ synchronized boolean tell() {
+ boolean ret = false;
+ if (old_state == null || old_state.state != state.state) {
+ voice.speak(state.state_name());
+ if ((old_state == null || old_state.state <= AltosLib.ao_flight_boost) &&
+ state.state > AltosLib.ao_flight_boost) {
+ voice.speak("max speed: %s.",
+ AltosConvert.speed.say_units(state.max_speed() + 0.5));
+ ret = true;
+ } else if ((old_state == null || old_state.state < AltosLib.ao_flight_drogue) &&
+ state.state >= AltosLib.ao_flight_drogue) {
+ voice.speak("max height: %s.",
+ AltosConvert.height.say_units(state.max_height() + 0.5));
+ ret = true;
+ }
+ }
+ if (old_state == null || old_state.gps_ready != state.gps_ready) {
+ if (state.gps_ready) {
+ voice.speak("GPS ready");
+ ret = true;
+ }
+ else if (old_state != null) {
+ voice.speak("GPS lost");
+ ret = true;
+ }
+ }
+ old_state = state;
+ return ret;
+ }
+
+ public void run() {
+ boolean interrupted = false;
+ boolean told;
+
+ idle_thread = new IdleThread();
+ idle_thread.start();
+
+ try {
+ for (;;) {
+ try {
+ state = reader.read();
+ if (state == null)
+ break;
+ reader.update(state);
+ show_safely();
+ told = tell();
+ idle_thread.notice(told);
+ } catch (ParseException pp) {
+ System.out.printf("Parse error: %d \"%s\"\n", pp.getErrorOffset(), pp.getMessage());
+ } catch (AltosCRCException ce) {
+ ++listener_state.crc_errors;
+ show_safely();
+ }
+ }
+ } catch (InterruptedException ee) {
+ interrupted = true;
+ } catch (IOException ie) {
+ reading_error_safely();
+ } finally {
+ if (!interrupted)
+ idle_thread.report(true);
+ reader.close(interrupted);
+ idle_thread.interrupt();
+ try {
+ idle_thread.join();
+ } catch (InterruptedException ie) {}
+ }
+ }
+
+ public AltosDisplayThread(Frame in_parent, AltosVoice in_voice, AltosFlightDisplay in_display, AltosFlightReader in_reader) {
+ listener_state = new AltosListenerState();
+ parent = in_parent;
+ voice = in_voice;
+ display = in_display;
+ reader = in_reader;
+ display.reset();
+ }
+}
diff --git a/altosuilib/AltosFlightDisplay.java b/altosuilib/AltosFlightDisplay.java
new file mode 100644
index 00000000..5695a015
--- /dev/null
+++ b/altosuilib/AltosFlightDisplay.java
@@ -0,0 +1,28 @@
+/*
+ * 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 org.altusmetrum.altosuilib_2;
+
+import org.altusmetrum.altoslib_4.*;
+
+public interface AltosFlightDisplay {
+ void reset();
+
+ void show(AltosState state, AltosListenerState listener_state);
+
+ void set_font();
+}
diff --git a/altosuilib/AltosFreqList.java b/altosuilib/AltosFreqList.java
new file mode 100644
index 00000000..e1299aae
--- /dev/null
+++ b/altosuilib/AltosFreqList.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2011 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 org.altusmetrum.altosuilib_2;
+
+import javax.swing.*;
+import org.altusmetrum.altoslib_4.*;
+
+public class AltosFreqList extends JComboBox<AltosFrequency> {
+
+ String product;
+ int serial;
+ int calibrate;
+
+ public void set_frequency(double new_frequency) {
+ int i;
+
+ if (new_frequency < 0) {
+ setVisible(false);
+ return;
+ }
+
+ for (i = 0; i < getItemCount(); i++) {
+ AltosFrequency f = (AltosFrequency) getItemAt(i);
+
+ if (f.close(new_frequency)) {
+ setSelectedIndex(i);
+ return;
+ }
+ }
+ for (i = 0; i < getItemCount(); i++) {
+ AltosFrequency f = (AltosFrequency) getItemAt(i);
+
+ if (new_frequency < f.frequency)
+ break;
+ }
+ String description = String.format("%s serial %d", product, serial);
+ AltosFrequency frequency = new AltosFrequency(new_frequency, description);
+ AltosUIPreferences.add_common_frequency(frequency);
+ insertItemAt(frequency, i);
+ setMaximumRowCount(getItemCount());
+ }
+
+ public void set_product(String new_product) {
+ product = new_product;
+ }
+
+ public void set_serial(int new_serial) {
+ serial = new_serial;
+ }
+
+ public double frequency() {
+ AltosFrequency f = (AltosFrequency) getSelectedItem();
+ if (f != null)
+ return f.frequency;
+ return 434.550;
+ }
+
+ public AltosFreqList () {
+ super(AltosUIPreferences.common_frequencies());
+ setMaximumRowCount(getItemCount());
+ setEditable(false);
+ product = "Unknown";
+ serial = 0;
+ }
+
+ public AltosFreqList(double in_frequency) {
+ this();
+ set_frequency(in_frequency);
+ }
+}
diff --git a/altosuilib/AltosSiteMap.java b/altosuilib/AltosSiteMap.java
new file mode 100644
index 00000000..1cfbc8b5
--- /dev/null
+++ b/altosuilib/AltosSiteMap.java
@@ -0,0 +1,484 @@
+/*
+ * 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 org.altusmetrum.altosuilib_2;
+
+import java.awt.*;
+import javax.swing.*;
+import java.io.*;
+import java.lang.Math;
+import java.awt.geom.Point2D;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_4.*;
+
+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;
+
+ int radius; /* half width/height of tiles to load */
+
+ 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);
+ }
+ */
+
+ ConcurrentHashMap<Point,AltosSiteMapTile> mapTiles = new ConcurrentHashMap<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
+ }
+
+ public void set_font() {
+ // nothing
+ }
+
+ private void loadMap(final AltosSiteMapTile tile,
+ final File pngfile, String pngurl)
+ {
+ boolean loaded = AltosSiteMapCache.fetchMap(pngfile, pngurl);
+ if (loaded) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ tile.loadMap(pngfile);
+ }
+ });
+ } else {
+ System.out.printf("# Failed to fetch file %s\n", pngfile);
+ System.out.printf(" wget -O '%s' '%s'\n", pngfile, pngurl);
+ }
+ }
+
+
+ class AltosSiteMapPrefetch {
+ int x;
+ int y;
+ int result;
+ File pngfile;
+ String pngurl;
+ }
+
+ public AltosSiteMapPrefetch prefetchMap(int x, int y) {
+ AltosSiteMapPrefetch prefetch = new AltosSiteMapPrefetch();
+ LatLng map_latlng = latlng(
+ -centre.x + x*px_size + px_size/2,
+ -centre.y + y*px_size + px_size/2);
+ prefetch.pngfile = MapFile(map_latlng.lat, map_latlng.lng, zoom);
+ prefetch.pngurl = MapURL(map_latlng.lat, map_latlng.lng, zoom);
+ if (prefetch.pngfile.exists()) {
+ prefetch.result = 1;
+ } else if (AltosSiteMapCache.fetchMap(prefetch.pngfile, prefetch.pngurl)) {
+ prefetch.result = 0;
+ } else {
+ prefetch.result = -1;
+ }
+ return prefetch;
+ }
+
+ public static void prefetchMaps(double lat, double lng) {
+ int w = AltosSiteMapPreload.width;
+ int h = AltosSiteMapPreload.height;
+ 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++) {
+ AltosSiteMapPrefetch prefetch = asm.prefetchMap(x, y);
+ switch (prefetch.result) {
+ case 1:
+ System.out.printf("Already have %s\n", prefetch.pngfile);
+ break;
+ case 0:
+ System.out.printf("Fetched map %s\n", prefetch.pngfile);
+ break;
+ case -1:
+ System.out.printf("# Failed to fetch file %s\n", prefetch.pngfile);
+ System.out.printf(" wget -O '%s' ''\n",
+ prefetch.pngfile, prefetch.pngurl);
+ break;
+ }
+ }
+ }
+ }
+
+ public String initMap(Point offset) {
+ AltosSiteMapTile tile = mapTiles.get(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, zoom);
+ String pngurl = MapURL(map_latlng.lat, map_latlng.lng, zoom);
+ loadMap(tile, pngfile, pngurl);
+ return pngfile.toString();
+ }
+
+ public void initAndFinishMapAsync (final AltosSiteMapTile tile, final Point offset) {
+ Thread thread = new Thread() {
+ public void run() {
+ initMap(offset);
+ finishTileLater(tile, offset);
+ }
+ };
+ thread.start();
+ }
+
+ public void setBaseLocation(double lat, double lng) {
+ for (Point k : mapTiles.keySet()) {
+ AltosSiteMapTile tile = mapTiles.get(k);
+ tile.clearMap();
+ }
+
+ centre = getBaseLocation(lat, lng);
+ scrollRocketToVisible(pt(lat,lng));
+ }
+
+ private void initMaps(double lat, double lng) {
+ setBaseLocation(lat, lng);
+
+ Thread thread = new Thread() {
+ public void run() {
+ for (Point k : mapTiles.keySet())
+ initMap(k);
+ }
+ };
+ thread.start();
+ }
+
+ private static File MapFile(double lat, double lng, int zoom) {
+ char chlat = lat < 0 ? 'S' : 'N';
+ char chlng = lng < 0 ? 'W' : 'E';
+ if (lat < 0) lat = -lat;
+ if (lng < 0) lng = -lng;
+ return new File(AltosUIPreferences.mapdir(),
+ String.format("map-%c%.6f,%c%.6f-%d.png",
+ chlat, lat, chlng, lng, zoom));
+ }
+
+ private static String MapURL(double lat, double lng, int zoom) {
+ 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(double lat, double lon) {
+ System.out.printf ("show %g %g\n", lat, lon);
+ return;
+// initMaps(lat, lon);
+// scrollRocketToVisible(pt(lat, lon));
+ }
+ public void show(final AltosState state, final AltosListenerState listener_state) {
+ // if insufficient gps data, nothing to update
+ AltosGPS gps = state.gps;
+
+ if (gps == null)
+ return;
+
+ if (!gps.locked && gps.nsat < 4)
+ return;
+
+ if (!initialised) {
+ if (state.pad_lat != AltosLib.MISSING && state.pad_lon != AltosLib.MISSING) {
+ initMaps(state.pad_lat, state.pad_lon);
+ initialised = true;
+ } else if (gps.lat != AltosLib.MISSING && gps.lon != AltosLib.MISSING) {
+ initMaps(gps.lat, gps.lon);
+ initialised = true;
+ } else {
+ return;
+ }
+ }
+
+ final Point2D.Double pt = pt(gps.lat, 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, listener_state, 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, listener_state, lref, ref);
+ initAndFinishMapAsync(tile, offset);
+ }
+
+ scrollRocketToVisible(pt);
+
+ if (offset != tileOffset(last_pt)) {
+ ensureTilesAround(offset);
+ }
+
+ last_pt = pt;
+ last_state = state.state;
+ }
+
+ public void centre(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;
+ r.x += dx;
+ r.y += dy;
+ comp.scrollRectToVisible(r);
+ }
+
+ public void centre(AltosState state) {
+ if (!state.gps.locked && state.gps.nsat < 4)
+ return;
+ centre(pt(state.gps.lat, state.gps.lon));
+ }
+
+ public void draw_circle(double lat, double lon) {
+ final Point2D.Double pt = pt(lat, lon);
+
+ for (Point offset : mapTiles.keySet()) {
+ AltosSiteMapTile tile = mapTiles.get(offset);
+ Point2D.Double ref = translatePoint(pt, tileCoordOffset(offset));
+ tile.set_boost(ref);
+ }
+ }
+
+ 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 = -radius; x <= radius; x++) {
+ for (int y = -radius; y <= radius; y++) {
+ Point offset = new Point(base_offset.x + x, base_offset.y + y);
+ if (mapTiles.containsKey(offset))
+ continue;
+ AltosSiteMapTile tile = createTile(offset);
+ initAndFinishMapAsync(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(int in_radius) {
+ radius = in_radius;
+
+ GrabNDrag scroller = new GrabNDrag(comp);
+
+ comp.setLayout(layout);
+
+ for (int x = -radius; x <= radius; x++) {
+ for (int y = -radius; y <= radius; y++) {
+ Point offset = new Point(x, y);
+ AltosSiteMapTile t = createTile(offset);
+ addTileAt(t, offset);
+ }
+ }
+ setViewportView(comp);
+ setPreferredSize(new Dimension(500,500));
+ }
+
+ public AltosSiteMap() {
+ this(1);
+ }
+}
diff --git a/altosuilib/AltosSiteMapCache.java b/altosuilib/AltosSiteMapCache.java
new file mode 100644
index 00000000..cf93016a
--- /dev/null
+++ b/altosuilib/AltosSiteMapCache.java
@@ -0,0 +1,194 @@
+/*
+ * 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 org.altusmetrum.altosuilib_2;
+
+import javax.swing.*;
+import javax.imageio.ImageIO;
+import java.awt.image.*;
+import java.awt.*;
+import java.io.*;
+import java.net.URL;
+import java.net.URLConnection;
+
+
+class AltosCacheImage {
+ Component component;
+ File file;
+ VolatileImage image;
+ int width;
+ int height;
+ long used;
+
+ public void load_image() throws IOException {
+ BufferedImage bimg = ImageIO.read(file);
+ Graphics2D g = image.createGraphics();
+ g.drawImage(bimg, 0, 0, null);
+ bimg.flush();
+ }
+
+ public Image validate() {
+ int returnCode;
+
+ if (image != null)
+ returnCode = image.validate(component.getGraphicsConfiguration());
+ else
+ returnCode = VolatileImage.IMAGE_INCOMPATIBLE;
+ if (returnCode == VolatileImage.IMAGE_RESTORED) {
+ try {
+ load_image();
+ } catch (IOException e) {
+ return null;
+ }
+ } else if (returnCode == VolatileImage.IMAGE_INCOMPATIBLE) {
+ image = component.createVolatileImage(width, height);
+ try {
+ load_image();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+ return image;
+ }
+
+ public void flush() {
+ image.flush();
+ }
+
+ public AltosCacheImage(Component component, File file, int w, int h) throws IOException {
+ this.component = component;
+ this.file = file;
+ width = w;
+ height = h;
+ image = component.createVolatileImage(w, h);
+ used = 0;
+ }
+}
+
+public class AltosSiteMapCache extends JLabel {
+ static final long google_maps_ratelimit_ms = 1200;
+ // Google limits static map queries to 50 per minute per IP, so
+ // each query should take at least 1.2 seconds.
+
+ public static boolean fetchMap(File file, String url) {
+ if (file.exists())
+ return true;
+
+ URL u;
+ long startTime = System.nanoTime();
+
+ 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;
+ }
+
+ long duration_ms = (System.nanoTime() - startTime) / 1000000;
+ if (duration_ms < google_maps_ratelimit_ms) {
+ try {
+ Thread.sleep(google_maps_ratelimit_ms - duration_ms);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ return true;
+ }
+
+ static int cache_size = 9;
+
+ static AltosCacheImage[] images;
+
+ static long used;
+
+ public static void set_cache_size(int cache_size) {
+ AltosSiteMapCache.cache_size = cache_size;
+ images = null;
+ }
+
+ public static Image get_image(Component component, File file, int width, int height) {
+ int oldest = -1;
+ long age = used;
+ AltosCacheImage image;
+ if (images == null)
+ images = new AltosCacheImage[cache_size];
+ for (int i = 0; i < cache_size; i++) {
+ image = images[i];
+
+ if (image == null) {
+ oldest = i;
+ break;
+ }
+ if (image.component == component && file.equals(image.file)) {
+ image.used = used++;
+ return image.validate();
+ }
+ if (image.used < age) {
+ oldest = i;
+ age = image.used;
+ }
+ }
+
+ try {
+ image = new AltosCacheImage(component, file, width, height);
+ image.used = used++;
+ if (images[oldest] != null)
+ images[oldest].flush();
+ images[oldest] = image;
+ return image.validate();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/altosuilib/AltosSiteMapPreload.java b/altosuilib/AltosSiteMapPreload.java
new file mode 100644
index 00000000..baa7fc37
--- /dev/null
+++ b/altosuilib/AltosSiteMapPreload.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright © 2011 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 org.altusmetrum.altosuilib_2;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import java.io.*;
+import java.util.*;
+import java.text.*;
+import java.lang.Math;
+import java.net.URL;
+import java.net.URLConnection;
+import org.altusmetrum.altoslib_4.*;
+
+class AltosMapPos extends Box {
+ AltosUIFrame owner;
+ JLabel label;
+ JComboBox hemi;
+ JTextField deg;
+ JLabel deg_label;
+ JTextField min;
+ JLabel min_label;
+
+ public void set_value(double new_value) {
+ double d, m;
+ int h;
+
+ h = 0;
+ if (new_value < 0) {
+ h = 1;
+ new_value = -new_value;
+ }
+ d = Math.floor(new_value);
+ deg.setText(String.format("%3.0f", d));
+ m = (new_value - d) * 60.0;
+ min.setText(String.format("%7.4f", m));
+ hemi.setSelectedIndex(h);
+ }
+
+ public double get_value() throws NumberFormatException {
+ int h = hemi.getSelectedIndex();
+ String d_t = deg.getText();
+ String m_t = min.getText();
+ double d, m, v;
+ try {
+ d = Double.parseDouble(d_t);
+ } catch (NumberFormatException ne) {
+ JOptionPane.showMessageDialog(owner,
+ String.format("Invalid degrees \"%s\"",
+ d_t),
+ "Invalid number",
+ JOptionPane.ERROR_MESSAGE);
+ throw ne;
+ }
+ try {
+ if (m_t.equals(""))
+ m = 0;
+ else
+ m = Double.parseDouble(m_t);
+ } catch (NumberFormatException ne) {
+ JOptionPane.showMessageDialog(owner,
+ String.format("Invalid minutes \"%s\"",
+ m_t),
+ "Invalid number",
+ JOptionPane.ERROR_MESSAGE);
+ throw ne;
+ }
+ v = d + m/60.0;
+ if (h == 1)
+ v = -v;
+ return v;
+ }
+
+ public AltosMapPos(AltosUIFrame in_owner,
+ String label_value,
+ String[] hemi_names,
+ double default_value) {
+ super(BoxLayout.X_AXIS);
+ owner = in_owner;
+ label = new JLabel(label_value);
+ hemi = new JComboBox<String>(hemi_names);
+ hemi.setEditable(false);
+ deg = new JTextField(5);
+ deg.setMinimumSize(deg.getPreferredSize());
+ deg.setHorizontalAlignment(JTextField.RIGHT);
+ deg_label = new JLabel("°");
+ min = new JTextField(9);
+ min.setMinimumSize(min.getPreferredSize());
+ min_label = new JLabel("'");
+ set_value(default_value);
+ add(label);
+ add(Box.createRigidArea(new Dimension(5, 0)));
+ add(hemi);
+ add(Box.createRigidArea(new Dimension(5, 0)));
+ add(deg);
+ add(Box.createRigidArea(new Dimension(5, 0)));
+ add(deg_label);
+ add(Box.createRigidArea(new Dimension(5, 0)));
+ add(min);
+ add(Box.createRigidArea(new Dimension(5, 0)));
+ add(min_label);
+ }
+}
+
+class AltosSite {
+ String name;
+ double latitude;
+ double longitude;
+
+ public String toString() {
+ return name;
+ }
+
+ public AltosSite(String in_name, double in_latitude, double in_longitude) {
+ name = in_name;
+ latitude = in_latitude;
+ longitude = in_longitude;
+ }
+
+ public AltosSite(String line) throws ParseException {
+ String[] elements = line.split(":");
+
+ if (elements.length < 3)
+ throw new ParseException(String.format("Invalid site line %s", line), 0);
+
+ name = elements[0];
+
+ try {
+ latitude = Double.parseDouble(elements[1]);
+ longitude = Double.parseDouble(elements[2]);
+ } catch (NumberFormatException ne) {
+ throw new ParseException(String.format("Invalid site line %s", line), 0);
+ }
+ }
+}
+
+class AltosSites extends Thread {
+ AltosSiteMapPreload preload;
+ URL url;
+ LinkedList<AltosSite> sites;
+
+ void notify_complete() {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ preload.set_sites();
+ }
+ });
+ }
+
+ void add(AltosSite site) {
+ sites.add(site);
+ }
+
+ void add(String line) {
+ try {
+ add(new AltosSite(line));
+ } catch (ParseException pe) {
+ }
+ }
+
+ public void run() {
+ try {
+ URLConnection uc = url.openConnection();
+ //int length = uc.getContentLength();
+
+ InputStreamReader in_stream = new InputStreamReader(uc.getInputStream(), AltosLib.unicode_set);
+ BufferedReader in = new BufferedReader(in_stream);
+
+ for (;;) {
+ String line = in.readLine();
+ if (line == null)
+ break;
+ add(line);
+ }
+ } catch (IOException e) {
+ } finally {
+ notify_complete();
+ }
+ }
+
+ public AltosSites(AltosSiteMapPreload in_preload) {
+ sites = new LinkedList<AltosSite>();
+ preload = in_preload;
+ try {
+ url = new URL(AltosLib.launch_sites_url);
+ } catch (java.net.MalformedURLException e) {
+ notify_complete();
+ }
+ start();
+ }
+}
+
+public class AltosSiteMapPreload extends AltosUIDialog implements ActionListener, ItemListener {
+ AltosUIFrame owner;
+ AltosSiteMap map;
+
+ AltosMapPos lat;
+ AltosMapPos lon;
+
+ final static int radius = 5;
+ final static int width = (radius * 2 + 1);
+ final static int height = (radius * 2 + 1);
+
+ JProgressBar pbar;
+
+ AltosSites sites;
+ JLabel site_list_label;
+ JComboBox<AltosSite> site_list;
+
+ JToggleButton load_button;
+ boolean loading;
+ JButton close_button;
+
+ static final String[] lat_hemi_names = { "N", "S" };
+ static final String[] lon_hemi_names = { "E", "W" };
+
+ class updatePbar implements Runnable {
+ int n;
+ String s;
+
+ public updatePbar(int x, int y, String in_s) {
+ n = (x + radius) + (y + radius) * width + 1;
+ s = in_s;
+ }
+
+ public void run() {
+ pbar.setValue(n);
+ pbar.setString(s);
+ if (n < width * height) {
+ pbar.setValue(n);
+ pbar.setString(s);
+ } else {
+ pbar.setValue(0);
+ pbar.setString("");
+ load_button.setSelected(false);
+ loading = false;
+ }
+ }
+ }
+
+ class bgLoad extends Thread {
+
+ AltosSiteMap map;
+
+ public bgLoad(AltosSiteMap in_map) {
+ map = in_map;
+ }
+
+ public void run() {
+ for (int y = -map.radius; y <= map.radius; y++) {
+ for (int x = -map.radius; x <= map.radius; x++) {
+ String pngfile;
+ pngfile = map.initMap(new Point(x,y));
+ SwingUtilities.invokeLater(new updatePbar(x, y, pngfile));
+ }
+ }
+ }
+ }
+
+ public void set_sites() {
+ int i = 1;
+ for (AltosSite site : sites.sites) {
+ site_list.insertItemAt(site, i);
+ i++;
+ }
+ }
+
+ public void itemStateChanged(ItemEvent e) {
+ int state = e.getStateChange();
+
+ if (state == ItemEvent.SELECTED) {
+ Object o = e.getItem();
+ if (o instanceof AltosSite) {
+ AltosSite site = (AltosSite) o;
+ lat.set_value(site.latitude);
+ lon.set_value(site.longitude);
+ }
+ }
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ String cmd = e.getActionCommand();
+
+ if (cmd.equals("close"))
+ setVisible(false);
+
+ if (cmd.equals("load")) {
+ if (!loading) {
+ try {
+ final double latitude = lat.get_value();
+ final double longitude = lon.get_value();
+ map.setBaseLocation(latitude,longitude);
+ map.draw_circle(latitude,longitude);
+ loading = true;
+ bgLoad thread = new bgLoad(map);
+ thread.start();
+ } catch (NumberFormatException ne) {
+ load_button.setSelected(false);
+ }
+ }
+ }
+ }
+
+ public AltosSiteMapPreload(AltosUIFrame in_owner) {
+ owner = in_owner;
+
+ Container pane = getContentPane();
+ GridBagConstraints c = new GridBagConstraints();
+ Insets i = new Insets(4,4,4,4);
+
+ pane.setLayout(new GridBagLayout());
+
+ map = new AltosSiteMap(radius);
+
+ c.fill = GridBagConstraints.BOTH;
+ c.anchor = GridBagConstraints.CENTER;
+ c.insets = i;
+ c.weightx = 1;
+ c.weighty = 1;
+
+ c.gridx = 0;
+ c.gridy = 0;
+ c.gridwidth = 2;
+ c.anchor = GridBagConstraints.CENTER;
+
+ pane.add(map, c);
+
+ pbar = new JProgressBar();
+ pbar.setMinimum(0);
+ pbar.setMaximum(width * height);
+ pbar.setValue(0);
+ pbar.setString("");
+ pbar.setStringPainted(true);
+
+ c.fill = GridBagConstraints.HORIZONTAL;
+ c.anchor = GridBagConstraints.CENTER;
+ c.insets = i;
+ c.weightx = 1;
+ c.weighty = 0;
+
+ c.gridx = 0;
+ c.gridy = 1;
+ c.gridwidth = 2;
+
+ pane.add(pbar, c);
+
+ site_list_label = new JLabel ("Known Launch Sites:");
+
+ c.fill = GridBagConstraints.NONE;
+ c.anchor = GridBagConstraints.CENTER;
+ c.insets = i;
+ c.weightx = 1;
+ c.weighty = 0;
+
+ c.gridx = 0;
+ c.gridy = 2;
+ c.gridwidth = 1;
+
+ pane.add(site_list_label, c);
+
+ site_list = new JComboBox<AltosSite>(new AltosSite[] { new AltosSite("Site List", 0, 0) });
+ site_list.addItemListener(this);
+
+ sites = new AltosSites(this);
+
+ c.fill = GridBagConstraints.HORIZONTAL;
+ c.anchor = GridBagConstraints.CENTER;
+ c.insets = i;
+ c.weightx = 1;
+ c.weighty = 0;
+
+ c.gridx = 1;
+ c.gridy = 2;
+ c.gridwidth = 1;
+
+ pane.add(site_list, c);
+
+ lat = new AltosMapPos(owner,
+ "Latitude:",
+ lat_hemi_names,
+ 37.167833333);
+ c.fill = GridBagConstraints.NONE;
+ c.anchor = GridBagConstraints.CENTER;
+ c.insets = i;
+ c.weightx = 0;
+ c.weighty = 0;
+
+ c.gridx = 0;
+ c.gridy = 3;
+ c.gridwidth = 1;
+ c.anchor = GridBagConstraints.CENTER;
+
+ pane.add(lat, c);
+
+ lon = new AltosMapPos(owner,
+ "Longitude:",
+ lon_hemi_names,
+ -97.73975);
+
+ c.fill = GridBagConstraints.NONE;
+ c.anchor = GridBagConstraints.CENTER;
+ c.insets = i;
+ c.weightx = 0;
+ c.weighty = 0;
+
+ c.gridx = 1;
+ c.gridy = 3;
+ c.gridwidth = 1;
+ c.anchor = GridBagConstraints.CENTER;
+
+ pane.add(lon, c);
+
+ load_button = new JToggleButton("Load Map");
+ load_button.addActionListener(this);
+ load_button.setActionCommand("load");
+
+ c.fill = GridBagConstraints.NONE;
+ c.anchor = GridBagConstraints.CENTER;
+ c.insets = i;
+ c.weightx = 1;
+ c.weighty = 0;
+
+ c.gridx = 0;
+ c.gridy = 4;
+ c.gridwidth = 1;
+ c.anchor = GridBagConstraints.CENTER;
+
+ pane.add(load_button, c);
+
+ close_button = new JButton("Close");
+ close_button.addActionListener(this);
+ close_button.setActionCommand("close");
+
+ c.fill = GridBagConstraints.NONE;
+ c.anchor = GridBagConstraints.CENTER;
+ c.insets = i;
+ c.weightx = 1;
+ c.weighty = 0;
+
+ c.gridx = 1;
+ c.gridy = 4;
+ c.gridwidth = 1;
+ c.anchor = GridBagConstraints.CENTER;
+
+ pane.add(close_button, c);
+
+ pack();
+ setLocationRelativeTo(owner);
+ setVisible(true);
+ }
+}
diff --git a/altosuilib/AltosSiteMapTile.java b/altosuilib/AltosSiteMapTile.java
new file mode 100644
index 00000000..1046d6bd
--- /dev/null
+++ b/altosuilib/AltosSiteMapTile.java
@@ -0,0 +1,142 @@
+/*
+ * 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 org.altusmetrum.altosuilib_2;
+
+import java.awt.*;
+import java.awt.image.*;
+import javax.swing.*;
+import javax.imageio.*;
+import java.awt.geom.Point2D;
+import java.awt.geom.Line2D;
+import java.io.*;
+import java.util.*;
+import org.altusmetrum.altoslib_4.*;
+
+class AltosPoint {
+ Point2D.Double pt;
+ int state;
+
+ AltosPoint(Point2D.Double pt, int state) {
+ this.pt = pt;
+ this.state = state;
+ }
+}
+
+public class AltosSiteMapTile extends JComponent {
+ int px_size;
+ File file;
+
+ Point2D.Double boost;
+ Point2D.Double landed;
+
+ LinkedList<AltosPoint> points;
+
+ public void loadMap(File pngFile) {
+ file = pngFile;
+ repaint();
+ }
+
+ public void clearMap() {
+ boost = null;
+ landed = null;
+ points = null;
+ file = null;
+ }
+
+ 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
+ };
+
+ private void draw_circle(Graphics g, Point2D.Double pt) {
+ g.drawOval((int)pt.x-5, (int)pt.y-5, 10, 10);
+ g.drawOval((int)pt.x-20, (int)pt.y-20, 40, 40);
+ g.drawOval((int)pt.x-35, (int)pt.y-35, 70, 70);
+ }
+
+ public void set_boost(Point2D.Double boost) {
+ this.boost = boost;
+ repaint();
+ }
+
+ public void paint(Graphics g) {
+ Graphics2D g2d = (Graphics2D) g;
+ AltosPoint prev = null;
+ Image img = null;
+
+ if (file != null)
+ img = AltosSiteMapCache.get_image(this, file, px_size, px_size);
+
+ if (img != null) {
+ g2d.drawImage(img, 0, 0, null);
+ } else {
+ g2d.setColor(Color.GRAY);
+ g2d.fillRect(0, 0, getWidth(), getHeight());
+ }
+
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ g2d.setStroke(new BasicStroke(6, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+
+ if (points != null) {
+ for (AltosPoint point : points) {
+ if (prev != null) {
+ if (0 <= point.state && point.state < stateColors.length)
+ g2d.setColor(stateColors[point.state]);
+ g2d.draw(new Line2D.Double(prev.pt, point.pt));
+ }
+ prev = point;
+ }
+ }
+ if (boost != null) {
+ g2d.setColor(Color.RED);
+ draw_circle(g2d, boost);
+ }
+ if (landed != null) {
+ g2d.setColor(Color.BLACK);
+ draw_circle(g2d, landed);
+ }
+ }
+
+ public synchronized void show(AltosState state, AltosListenerState listener_state,
+ Point2D.Double last_pt, Point2D.Double pt)
+ {
+ if (points == null)
+ points = new LinkedList<AltosPoint>();
+
+ points.add(new AltosPoint(pt, state.state));
+
+ if (state.state == AltosLib.ao_flight_boost && boost == null)
+ boost = pt;
+ if (state.state == AltosLib.ao_flight_landed && landed == null)
+ landed = pt;
+ repaint();
+ }
+
+ public AltosSiteMapTile(int in_px_size) {
+ px_size = in_px_size;
+ setPreferredSize(new Dimension(px_size, px_size));
+ }
+}
diff --git a/altosuilib/AltosVoice.java b/altosuilib/AltosVoice.java
new file mode 100644
index 00000000..a3995f68
--- /dev/null
+++ b/altosuilib/AltosVoice.java
@@ -0,0 +1,94 @@
+/*
+ * 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 org.altusmetrum.altosuilib_2;
+
+import com.sun.speech.freetts.Voice;
+import com.sun.speech.freetts.VoiceManager;
+import java.util.concurrent.LinkedBlockingQueue;
+
+public class AltosVoice implements Runnable {
+ VoiceManager voice_manager;
+ Voice voice;
+ LinkedBlockingQueue<String> phrases;
+ Thread thread;
+ boolean busy;
+
+ final static String voice_name = "kevin16";
+
+ public void run() {
+ try {
+ for (;;) {
+ String s = phrases.take();
+ voice.speak(s);
+ synchronized(this) {
+ if (phrases.isEmpty()) {
+ busy = false;
+ notifyAll();
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ }
+ }
+
+ public synchronized void drain() throws InterruptedException {
+ while (busy)
+ wait();
+ }
+
+ public void speak_always(String s) {
+ try {
+ if (voice != null) {
+ synchronized(this) {
+ busy = true;
+ phrases.put(s);
+ }
+ }
+ } catch (InterruptedException e) {
+ }
+ }
+
+ public void speak(String s) {
+ if (AltosUIPreferences.voice())
+ speak_always(s);
+ }
+
+ public void speak(String format, Object... parameters) {
+ speak(String.format(format, parameters));
+ }
+
+ public AltosVoice () {
+ busy = false;
+ voice_manager = VoiceManager.getInstance();
+ voice = voice_manager.getVoice(voice_name);
+ if (voice != null) {
+ voice.allocate();
+ phrases = new LinkedBlockingQueue<String> ();
+ thread = new Thread(this);
+ thread.start();
+ } else {
+ System.out.printf("Voice manager failed to open %s\n", voice_name);
+ Voice[] voices = voice_manager.getVoices();
+ System.out.printf("Available voices:\n");
+ for (int i = 0; i < voices.length; i++) {
+ System.out.println(" " + voices[i].getName()
+ + " (" + voices[i].getDomain() + " domain)");
+ }
+ }
+ }
+}
diff --git a/altosuilib/GrabNDrag.java b/altosuilib/GrabNDrag.java
new file mode 100644
index 00000000..5e5fdd52
--- /dev/null
+++ b/altosuilib/GrabNDrag.java
@@ -0,0 +1,48 @@
+/*
+ * 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 org.altusmetrum.altosuilib_2;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.MouseInputAdapter;
+
+class GrabNDrag extends MouseInputAdapter {
+ private JComponent scroll;
+ private Point startPt = new Point();
+
+ public GrabNDrag(JComponent scroll) {
+ this.scroll = scroll;
+ scroll.addMouseMotionListener(this);
+ scroll.addMouseListener(this);
+ scroll.setAutoscrolls(true);
+ }
+
+ public void mousePressed(MouseEvent e) {
+ startPt.setLocation(e.getPoint());
+ }
+ public void mouseDragged(MouseEvent e) {
+ int xd = e.getX() - startPt.x;
+ int yd = e.getY() - startPt.y;
+
+ Rectangle r = scroll.getVisibleRect();
+ r.x -= xd;
+ r.y -= yd;
+ scroll.scrollRectToVisible(r);
+ }
+}
diff --git a/altosuilib/Makefile.am b/altosuilib/Makefile.am
index 4b22af1f..b7d624e2 100644
--- a/altosuilib/Makefile.am
+++ b/altosuilib/Makefile.am
@@ -1,4 +1,4 @@
-AM_JAVACFLAGS=-target 1.6 -encoding UTF-8 -Xlint:deprecation -source 6
+AM_JAVACFLAGS=-target 1.6 -encoding UTF-8 -Xlint:deprecation -Xlint:unchecked -source 6
JAVAROOT=bin
@@ -9,8 +9,10 @@ SRC=.
altosuilibdir = $(datadir)/java
altosuilib_JAVA = \
+ GrabNDrag.java \
AltosDevice.java \
AltosDeviceDialog.java \
+ AltosFlightDisplay.java \
AltosFontListener.java \
AltosPositionListener.java \
AltosUIConfigure.java \
@@ -30,7 +32,14 @@ altosuilib_JAVA = \
AltosUIPreferences.java \
AltosUISeries.java \
AltosUIVersion.java \
- AltosUSBDevice.java
+ AltosUSBDevice.java \
+ AltosSiteMap.java \
+ AltosSiteMapCache.java \
+ AltosSiteMapPreload.java \
+ AltosSiteMapTile.java \
+ AltosVoice.java \
+ AltosDisplayThread.java \
+ AltosFreqList.java
JAR=altosuilib_$(ALTOSUILIB_VERSION).jar