altosuilib: Rewrite map GUI bits
authorKeith Packard <keithp@keithp.com>
Thu, 12 Jun 2014 01:46:47 +0000 (18:46 -0700)
committerKeith Packard <keithp@keithp.com>
Thu, 12 Jun 2014 01:46:47 +0000 (18:46 -0700)
Use a single large Canvas and draw images on top by hand.

Signed-off-by: Keith Packard <keithp@keithp.com>
22 files changed:
altosui/AltosFlightUI.java
altosui/AltosGraphUI.java
altosui/AltosUI.java
altosuilib/AltosUILatLon.java [new file with mode: 0644]
altosuilib/AltosUIMap.java [new file with mode: 0644]
altosuilib/AltosUIMapCache.java [new file with mode: 0644]
altosuilib/AltosUIMapImage.java [new file with mode: 0644]
altosuilib/AltosUIMapLine.java [new file with mode: 0644]
altosuilib/AltosUIMapMark.java [new file with mode: 0644]
altosuilib/AltosUIMapPath.java [new file with mode: 0644]
altosuilib/AltosUIMapPreload.java [new file with mode: 0644]
altosuilib/AltosUIMapRectangle.java [new file with mode: 0644]
altosuilib/AltosUIMapStore.java [new file with mode: 0644]
altosuilib/AltosUIMapStoreListener.java [new file with mode: 0644]
altosuilib/AltosUIMapTile.java [new file with mode: 0644]
altosuilib/AltosUIMapTileListener.java [new file with mode: 0644]
altosuilib/AltosUIMapTransform.java [new file with mode: 0644]
altosuilib/AltosUIMapView.java [new file with mode: 0644]
altosuilib/AltosUIMapZoomListener.java [new file with mode: 0644]
altosuilib/Makefile.am
telegps/TeleGPS.java
telegps/TeleGPSGraphUI.java

index baa1868..f2bd70a 100644 (file)
@@ -37,7 +37,7 @@ public class AltosFlightUI extends AltosUIFrame implements AltosFlightDisplay {
        AltosDescent    descent;
        AltosLanded     landed;
        AltosCompanionInfo      companion;
-       AltosSiteMap    sitemap;
+       AltosUIMap      sitemap;
        boolean         has_map;
        boolean         has_companion;
        boolean         has_state;
@@ -310,7 +310,7 @@ public class AltosFlightUI extends AltosUIFrame implements AltosFlightDisplay {
                has_companion = false;
                has_state = false;
 
-               sitemap = new AltosSiteMap();
+               sitemap = new AltosUIMap();
                has_map = false;
 
                /* Make the tabbed pane use the rest of the window space */
index 9e8a193..0df92ea 100644 (file)
@@ -34,7 +34,7 @@ public class AltosGraphUI extends AltosUIFrame
        JTabbedPane             pane;
        AltosGraph              graph;
        AltosUIEnable           enable;
-       AltosSiteMap            map;
+       AltosUIMap              map;
        AltosState              state;
        AltosGraphDataSet       graphDataSet;
        AltosFlightStats        stats;
@@ -46,7 +46,7 @@ public class AltosGraphUI extends AltosUIFrame
                for (AltosState state : states) {
                        if (state.gps != null && state.gps.locked && state.gps.nsat >= 4) {
                                if (map == null)
-                                       map = new AltosSiteMap();
+                                       map = new AltosUIMap();
                                map.show(state, null);
                                has_gps = true;
                        }
index 302f623..6137487 100644 (file)
@@ -280,7 +280,7 @@ public class AltosUI extends AltosUIFrame {
        }
 
        void LoadMaps() {
-               new AltosSiteMapPreload(AltosUI.this);
+               new AltosUIMapPreload(AltosUI.this);
        }
 
        void LaunchController() {
@@ -578,7 +578,7 @@ public class AltosUI extends AltosUIFrame {
                                        } else {
                                                double lat = Double.parseDouble(args[i+1]);
                                                double lon = Double.parseDouble(args[i+2]);
-                                               AltosSiteMap.prefetchMaps(lat, lon);
+//                                             AltosSiteMap.prefetchMaps(lat, lon);
                                                i += 2;
                                        }
                                } else if (args[i].equals("--replay"))
diff --git a/altosuilib/AltosUILatLon.java b/altosuilib/AltosUILatLon.java
new file mode 100644 (file)
index 0000000..688dd58
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2014 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.lang.Math;
+import java.awt.geom.*;
+import java.util.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_4.*;
+
+public class AltosUILatLon {
+       public double   lat;
+       public double   lon;
+
+       public boolean equals(AltosUILatLon other) {
+               if (other == null)
+                       return false;
+               return lat == other.lat && lon == other.lon;
+       }
+
+       public AltosUILatLon(double lat, double lon) {
+               this.lat = lat;
+               this.lon = lon;
+       }
+}
diff --git a/altosuilib/AltosUIMap.java b/altosuilib/AltosUIMap.java
new file mode 100644 (file)
index 0000000..fa974d3
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+ * 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 java.io.*;
+import java.lang.Math;
+import java.awt.geom.*;
+import java.util.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_4.*;
+
+public class AltosUIMap extends JComponent implements AltosFlightDisplay, AltosUIMapZoomListener {
+
+       static final int px_size = 512;
+
+       static final int maptype_hybrid = 0;
+       static final int maptype_roadmap = 1;
+       static final int maptype_satellite = 2;
+       static final int maptype_terrain = 3;
+       static final int maptype_default = maptype_hybrid;
+
+       static final String[] maptype_names = {
+               "hybrid",
+               "roadmap",
+               "satellite",
+               "terrain"
+       };
+
+       public static final String[] maptype_labels = {
+               "Hybrid",
+               "Roadmap",
+               "Satellite",
+               "Terrain"
+       };
+
+       public static final 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
+               Color.BLACK,  // invalid
+               Color.CYAN,   // stateless
+       };
+
+       public void reset() {
+               // nothing
+       }
+
+       public void font_size_changed(int font_size) {
+               view.set_font();
+       }
+
+       public void units_changed(boolean imperial_units) {
+               repaint();
+       }
+
+       JLabel  zoom_label;
+
+       private void set_zoom_label() {
+               zoom_label.setText(String.format("Zoom %d", view.zoom() - view.default_zoom));
+       }
+
+       public void zoom_changed(int zoom) {
+               set_zoom_label();
+       }
+
+       public void set_zoom(int zoom) {
+               view.set_zoom(zoom);
+       }
+
+       public int get_zoom() {
+               return view.zoom();
+       }
+
+       public void set_maptype(int type) {
+               view.set_maptype(type);
+               maptype_combo.setSelectedIndex(type);
+       }
+
+       public void show(AltosState state, AltosListenerState listener_state) {
+               view.show(state, listener_state);
+       }
+
+       public void centre(double lat, double lon) {
+               view.centre(lat, lon);
+       }
+
+       public void centre(AltosState state) {
+               if (!state.gps.locked && state.gps.nsat < 4)
+                       return;
+               centre(state.gps.lat, state.gps.lon);
+       }
+
+       public void add_mark(double lat, double lon, int state) {
+               view.add_mark(lat, lon, state);
+       }
+
+       public void clear_marks() {
+               view.clear_marks();
+       }
+
+       AltosUIMapView  view;
+
+       private GridBagLayout layout = new GridBagLayout();
+
+       JComboBox<String>       maptype_combo;
+
+       public void set_load_params(double lat, double lon, int radius, AltosUIMapTileListener listener) {
+               view.set_load_params(lat, lon, radius, listener);
+       }
+
+       public boolean all_fetched() {
+               return view.all_fetched();
+       }
+
+       public static void prefetch_maps(double lat, double lon) {
+       }
+
+       public AltosUIMap() {
+
+               view = new AltosUIMapView();
+
+               view.setPreferredSize(new Dimension(500,500));
+               view.setVisible(true);
+               view.setEnabled(true);
+               view.add_zoom_listener(this);
+
+               GridBagLayout   my_layout = new GridBagLayout();
+
+               setLayout(my_layout);
+
+               GridBagConstraints c = new GridBagConstraints();
+               c.anchor = GridBagConstraints.CENTER;
+               c.fill = GridBagConstraints.BOTH;
+               c.gridx = 0;
+               c.gridy = 0;
+               c.gridwidth = 1;
+               c.gridheight = 10;
+               c.weightx = 1;
+               c.weighty = 1;
+               add(view, c);
+
+               int     y = 0;
+
+               zoom_label = new JLabel("", JLabel.CENTER);
+               set_zoom_label();
+
+               c = new GridBagConstraints();
+               c.anchor = GridBagConstraints.CENTER;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.gridx = 1;
+               c.gridy = y++;
+               c.weightx = 0;
+               c.weighty = 0;
+               add(zoom_label, c);
+
+               JButton zoom_reset = new JButton("0");
+               zoom_reset.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       set_zoom(view.default_zoom);
+                               }
+                       });
+
+               c = new GridBagConstraints();
+               c.anchor = GridBagConstraints.CENTER;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.gridx = 1;
+               c.gridy = y++;
+               c.weightx = 0;
+               c.weighty = 0;
+               add(zoom_reset, c);
+
+               JButton zoom_in = new JButton("+");
+               zoom_in.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       set_zoom(get_zoom() + 1);
+                               }
+                       });
+
+               c = new GridBagConstraints();
+               c.anchor = GridBagConstraints.CENTER;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.gridx = 1;
+               c.gridy = y++;
+               c.weightx = 0;
+               c.weighty = 0;
+               add(zoom_in, c);
+
+               JButton zoom_out = new JButton("-");
+               zoom_out.addActionListener(new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       set_zoom(get_zoom() - 1);
+                               }
+                       });
+               c = new GridBagConstraints();
+               c.anchor = GridBagConstraints.CENTER;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.gridx = 1;
+               c.gridy = y++;
+               c.weightx = 0;
+               c.weighty = 0;
+               add(zoom_out, c);
+
+               maptype_combo = new JComboBox<String>(maptype_labels);
+
+               maptype_combo.setEditable(false);
+               maptype_combo.setMaximumRowCount(maptype_combo.getItemCount());
+               maptype_combo.addItemListener(new ItemListener() {
+                               public void itemStateChanged(ItemEvent e) {
+                                       view.set_maptype(maptype_combo.getSelectedIndex());
+                               }
+                       });
+
+               c = new GridBagConstraints();
+               c.anchor = GridBagConstraints.CENTER;
+               c.fill = GridBagConstraints.HORIZONTAL;
+               c.gridx = 1;
+               c.gridy = y++;
+               c.weightx = 0;
+               c.weighty = 0;
+               add(maptype_combo, c);
+       }
+}
diff --git a/altosuilib/AltosUIMapCache.java b/altosuilib/AltosUIMapCache.java
new file mode 100644 (file)
index 0000000..e849da7
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * 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.*;
+
+public class AltosUIMapCache {
+       static final int        success = 0;
+       static final int        loading = 1;
+       static final int        failed = 2;
+       static final int        bad_request = 3;
+       static final int        forbidden = 4;
+
+       static private Object fetch_lock = new Object();
+
+       static final int                min_cache_size = 9;
+       static final int                max_cache_size = 24;
+
+       static int                      cache_size = min_cache_size;
+
+       static AltosUIMapImage[]        images = new AltosUIMapImage[cache_size];
+
+       static Object cache_lock = new Object();
+
+       public  static void set_cache_size(int new_size) {
+               if (new_size < min_cache_size)
+                       new_size = min_cache_size;
+               if (new_size > max_cache_size)
+                       new_size = max_cache_size;
+               if (new_size == cache_size)
+                       return;
+
+               synchronized(cache_lock) {
+                       AltosUIMapImage[]       new_images = new AltosUIMapImage[new_size];
+
+                       for (int i = 0; i < cache_size; i++) {
+                               if (i < new_size)
+                                       new_images[i] = images[i];
+                               else if (images[i] != null)
+                                       images[i].flush();
+                       }
+                       images = new_images;
+                       cache_size = new_size;
+               }
+       }
+
+       static long                     used;
+
+       public static Image get(AltosUIMapTile tile, AltosUIMapStore store, int width, int height) {
+               int             oldest = -1;
+               long            age = used;
+
+               synchronized(cache_lock) {
+                       AltosUIMapImage image = null;
+                       for (int i = 0; i < cache_size; i++) {
+                               image = images[i];
+
+                               if (image == null) {
+                                       oldest = i;
+                                       break;
+                               }
+                               if (store.equals(image.store)) {
+                                       image.used = used++;
+                                       return image.image;
+                               }
+                               if (image.used < age) {
+                                       oldest = i;
+                                       age = image.used;
+                               }
+                       }
+
+                       try {
+                               image = new AltosUIMapImage(tile, store);
+                               image.used = used++;
+                               if (images[oldest] != null)
+                                       images[oldest].flush();
+
+                               images[oldest] = image;
+
+                               if (image.image == null)
+                                       tile.set_status(loading);
+                               else
+                                       tile.set_status(success);
+
+                               return image.image;
+                       } catch (IOException e) {
+                               tile.set_status(failed);
+                               return null;
+                       }
+               }
+       }
+}
diff --git a/altosuilib/AltosUIMapImage.java b/altosuilib/AltosUIMapImage.java
new file mode 100644 (file)
index 0000000..3819d07
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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.*;
+
+public class AltosUIMapImage implements AltosUIMapStoreListener {
+       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.
+
+       static final int        success = 0;
+       static final int        loading = 1;
+       static final int        failed = 2;
+       static final int        bad_request = 3;
+       static final int        forbidden = 4;
+
+       static long             forbidden_time;
+       static boolean          forbidden_set = false;
+       static final long       forbidden_interval = 60l * 1000l * 1000l * 1000l;
+
+       AltosUIMapTile          tile;           /* Notify when image has been loaded */
+       Image                   image;
+       AltosUIMapStore         store;
+       long                    used;
+
+       class loader implements Runnable {
+               public void run() {
+                       if (image != null)
+                               tile.notify_image(image);
+                       try {
+                               image = ImageIO.read(store.file);
+                       } catch (Exception ex) {
+                       }
+                       if (image == null)
+                               tile.set_status(failed);
+                       else
+                               tile.set_status(success);
+                       tile.notify_image(image);
+               }
+       }
+
+       private void load() {
+               loader  l = new loader();
+               Thread  lt = new Thread(l);
+               lt.start();
+       }
+
+       public void flush() {
+               if (image != null) {
+                       image.flush();
+                       image = null;
+               }
+       }
+
+       public boolean has_map() {
+               return store.status() == AltosUIMapStore.success;
+       }
+
+       public synchronized void notify_store(AltosUIMapStore store, int status) {
+               switch (status) {
+               case AltosUIMapStore.loading:
+                       break;
+               case AltosUIMapStore.success:
+                       load();
+                       break;
+               default:
+                       tile.set_status(status);
+                       tile.notify_image(null);
+               }
+       }
+
+       public AltosUIMapImage(AltosUIMapTile tile, AltosUIMapStore store) throws IOException {
+               this.tile = tile;
+               this.image = null;
+               this.store = store;
+               this.used = 0;
+
+               int status = store.status();
+               switch (status) {
+               case AltosUIMapStore.loading:
+                       store.add_listener(this);
+                       break;
+               case AltosUIMapStore.success:
+                       load();
+                       break;
+               default:
+                       tile.set_status(status);
+                       tile.notify_image(null);
+                       break;
+               }
+       }
+}
diff --git a/altosuilib/AltosUIMapLine.java b/altosuilib/AltosUIMapLine.java
new file mode 100644 (file)
index 0000000..e09a2d9
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * Copyright © 2014 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.lang.Math;
+import java.awt.geom.*;
+import java.util.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_4.*;
+
+public class AltosUIMapLine {
+       AltosUILatLon   start, end;
+
+       private Font    font = null;
+
+       public void set_font(Font font) {
+               this.font = font;
+       }
+
+       private AltosUILatLon lat_lon(MouseEvent e, AltosUIMapTransform t) {
+               return t.screen_lat_lon(e.getPoint());
+       }
+
+       public void dragged(MouseEvent e, AltosUIMapTransform t) {
+               end = lat_lon(e, t);
+       }
+
+       public void pressed(MouseEvent e, AltosUIMapTransform t) {
+               start = lat_lon(e, t);
+               end = null;
+       }
+
+       private String line_dist() {
+               String  format;
+               AltosGreatCircle        g = new AltosGreatCircle(start.lat, start.lon,
+                                                                end.lat, end.lon);
+               double  distance = g.distance;
+
+               if (AltosConvert.imperial_units) {
+                       distance = AltosConvert.meters_to_feet(distance);
+                       if (distance < 10000) {
+                               format = "%4.0fft";
+                       } else {
+                               distance /= 5280;
+                               if (distance < 10)
+                                       format = "%5.3fmi";
+                               else if (distance < 100)
+                                       format = "%5.2fmi";
+                               else if (distance < 1000)
+                                       format = "%5.1fmi";
+                               else
+                                       format = "%5.0fmi";
+                       }
+               } else {
+                       if (distance < 10000) {
+                               format = "%4.0fm";
+                       } else {
+                               distance /= 1000;
+                               if (distance < 100)
+                                       format = "%5.2fkm";
+                               else if (distance < 1000)
+                                       format = "%5.1fkm";
+                               else
+                                       format = "%5.0fkm";
+                       }
+               }
+               return String.format(format, distance);
+       }
+
+       public void paint(Graphics2D g, AltosUIMapTransform t) {
+               g.setColor(Color.BLUE);
+
+               if (start == null || end == null)
+                       return;
+
+               Line2D.Double line = new Line2D.Double(t.screen(start),
+                                                      t.screen(end));
+
+               g.draw(line);
+
+               String  message = line_dist();
+               g.setFont(font);
+               g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+               Rectangle2D     bounds;
+               bounds = font.getStringBounds(message, g.getFontRenderContext());
+
+               float x = (float) line.x1;
+               float y = (float) line.y1 + (float) bounds.getHeight() / 2.0f;
+
+               if (line.x1 < line.x2) {
+                       x -= (float) bounds.getWidth() + 2.0f;
+               } else {
+                       x += 2.0f;
+               }
+               g.drawString(message, x, y);
+       }
+}
diff --git a/altosuilib/AltosUIMapMark.java b/altosuilib/AltosUIMapMark.java
new file mode 100644 (file)
index 0000000..8c640e5
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2014 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.lang.Math;
+import java.awt.geom.*;
+import java.util.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_4.*;
+
+public class AltosUIMapMark {
+
+       AltosUILatLon   lat_lon;
+       int             state;
+
+       static public int stroke_width = 6;
+
+       public void paint(Graphics2D g, AltosUIMapTransform t) {
+
+               Point2D.Double pt = t.screen(lat_lon);
+
+               g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                                  RenderingHints.VALUE_ANTIALIAS_ON);
+               g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+
+               if (0 <= state && state < AltosUIMap.stateColors.length)
+                       g.setColor(AltosUIMap.stateColors[state]);
+               else
+                       g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
+
+               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 AltosUIMapMark (double lat, double lon, int state) {
+               lat_lon = new AltosUILatLon(lat, lon);
+               this.state = state;
+       }
+}
diff --git a/altosuilib/AltosUIMapPath.java b/altosuilib/AltosUIMapPath.java
new file mode 100644 (file)
index 0000000..ff17be6
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * Copyright © 2014 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.lang.Math;
+import java.awt.geom.*;
+import java.util.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_4.*;
+
+class PathPoint {
+       AltosUILatLon   lat_lon;
+       int             state;
+
+       public PathPoint(AltosUILatLon lat_lon, int state) {
+               this.lat_lon = lat_lon;
+               this.state = state;
+       }
+
+       public boolean equals(PathPoint other) {
+               if (other == null)
+                       return false;
+
+               return lat_lon.equals(other.lat_lon) && state == other.state;
+       }
+}
+
+public class AltosUIMapPath {
+
+       LinkedList<PathPoint>   points = new LinkedList<PathPoint>();
+       PathPoint               last_point = null;
+
+       static public int stroke_width = 6;
+
+       public void paint(Graphics2D g, AltosUIMapTransform t) {
+               Point2D.Double  prev = null;
+
+               g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                                  RenderingHints.VALUE_ANTIALIAS_ON);
+               g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+
+               for (PathPoint point : points) {
+                       Point2D.Double  cur = t.screen(point.lat_lon);
+                       if (prev != null) {
+                               Line2D.Double   line = new Line2D.Double (prev, cur);
+                               Rectangle       bounds = line.getBounds();
+
+                               if (g.hitClip(bounds.x, bounds.y, bounds.width, bounds.height)) {
+                                       if (0 <= point.state && point.state < AltosUIMap.stateColors.length)
+                                               g.setColor(AltosUIMap.stateColors[point.state]);
+                                       else
+                                               g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
+
+                                       g.draw(line);
+                               }
+                       }
+                       prev = cur;
+               }
+       }
+
+       public AltosUIMapRectangle add(double lat, double lon, int state) {
+               PathPoint               point = new PathPoint(new AltosUILatLon (lat, lon), state);
+               AltosUIMapRectangle     rect = null;
+
+               if (!point.equals(last_point)) {
+                       if (last_point != null)
+                               rect = new AltosUIMapRectangle(last_point.lat_lon, point.lat_lon);
+                       points.add (point);
+                       last_point = point;
+               }
+               return rect;
+       }
+
+       public void clear () {
+               points = new LinkedList<PathPoint>();
+       }
+}
diff --git a/altosuilib/AltosUIMapPreload.java b/altosuilib/AltosUIMapPreload.java
new file mode 100644 (file)
index 0000000..d702ddd
--- /dev/null
@@ -0,0 +1,608 @@
+/*
+ * 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 AltosUIMapPos 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 AltosUIMapPos(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 AltosUISite {
+       String  name;
+       double  latitude;
+       double  longitude;
+
+       public String toString() {
+               return name;
+       }
+
+       public AltosUISite(String in_name, double in_latitude, double in_longitude) {
+               name = in_name;
+               latitude = in_latitude;
+               longitude = in_longitude;
+       }
+
+       public AltosUISite(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 AltosUISites extends Thread {
+       AltosUIMapPreload       preload;
+       URL                     url;
+       LinkedList<AltosUISite> sites;
+
+       void notify_complete() {
+               SwingUtilities.invokeLater(new Runnable() {
+                               public void run() {
+                                       preload.set_sites();
+                               }
+                       });
+       }
+
+       void add(AltosUISite site) {
+               sites.add(site);
+       }
+
+       void add(String line) {
+               try {
+                       add(new AltosUISite(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 AltosUISites(AltosUIMapPreload in_preload) {
+               sites = new LinkedList<AltosUISite>();
+               preload = in_preload;
+               try {
+                       url = new URL(AltosLib.launch_sites_url);
+               } catch (java.net.MalformedURLException e) {
+                       notify_complete();
+               }
+               start();
+       }
+}
+
+public class AltosUIMapPreload extends AltosUIFrame implements ActionListener, ItemListener, AltosUIMapTileListener {
+       AltosUIFrame    owner;
+       AltosUIMap      map;
+
+       AltosUIMapPos   lat;
+       AltosUIMapPos   lon;
+
+       JProgressBar    pbar;
+       int             pbar_max;
+       int             pbar_cur;
+
+       AltosUISites    sites;
+       JLabel          site_list_label;
+       JComboBox<AltosUISite>  site_list;
+
+       JToggleButton   load_button;
+       boolean         loading;
+       JButton         close_button;
+
+       JCheckBox[]     maptypes = new JCheckBox[AltosUIMap.maptype_terrain - AltosUIMap.maptype_hybrid + 1];
+
+       JComboBox<Integer>      min_zoom;
+       JComboBox<Integer>      max_zoom;
+       JComboBox<Integer>      radius;
+
+       Integer[]               zooms = { -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6 };
+       Integer[]               radii = { 1, 2, 3, 4, 5 };
+
+       static final String[]   lat_hemi_names = { "N", "S" };
+       static final String[]   lon_hemi_names = { "E", "W" };
+
+       class updatePbar implements Runnable {
+               String          s;
+
+               public updatePbar(String in_s) {
+                       s = in_s;
+               }
+
+               public void run() {
+                       int     n = ++pbar_cur;
+
+                       pbar.setMaximum(pbar_max);
+                       pbar.setValue(n);
+                       pbar.setString(s);
+               }
+       }
+
+       double  latitude, longitude;
+       int     min_z;
+       int     max_z;
+       int     cur_z;
+       int     all_types;
+       int     cur_type;
+       int     r;
+
+       int     tiles_per_layer;
+       int     tiles_loaded;
+       int     layers_total;
+       int     layers_loaded;
+
+
+       private void do_load() {
+               tiles_loaded = 0;
+               map.set_zoom(cur_z + AltosUIMapView.default_zoom);
+               map.set_maptype(cur_type);
+               map.set_load_params(latitude, longitude, r, this);
+       }
+
+       private int next_type(int start) {
+               int next_type;
+               for (next_type = start;
+                    next_type <= AltosUIMap.maptype_terrain && (all_types & (1 << next_type)) == 0;
+                    next_type++)
+                       ;
+               return next_type;
+       }
+
+       private void next_load() {
+               int next_type = next_type(cur_type + 1);
+
+               if (next_type > AltosUIMap.maptype_terrain) {
+                       if (cur_z == max_z) {
+                               return;
+                       } else {
+                               cur_z++;
+                       }
+                       next_type = next_type(0);
+               }
+               cur_type = next_type;
+               do_load();
+       }
+
+       private void start_load() {
+               cur_z = min_z;
+               int ntype = 0;
+               all_types = 0;
+               for (int t = AltosUIMap.maptype_hybrid; t <= AltosUIMap.maptype_terrain; t++)
+                       if (maptypes[t].isSelected()) {
+                               all_types |= (1 << t);
+                               ntype++;
+                       }
+               if (ntype == 0) {
+                       all_types |= (1 << AltosUIMap.maptype_hybrid);
+                       ntype = 1;
+               }
+
+               cur_type = next_type(0);
+               tiles_per_layer = (r * 2 + 1) * (r * 2 + 1);
+               layers_total = (max_z - min_z + 1) * ntype;
+               layers_loaded = 0;
+               pbar_max = layers_total * tiles_per_layer;
+               pbar_cur = 0;
+
+               map.clear_marks();
+               map.add_mark(latitude,longitude, AltosLib.ao_flight_boost);
+               do_load();
+       }
+
+       /* AltosUIMapTileListener methods */
+
+       public void notify_tile(AltosUIMapTile tile, int status) {
+               if (status == AltosUIMapStore.loading)
+                       return;
+
+               SwingUtilities.invokeLater(new updatePbar(tile.store.file.toString()));
+               ++tiles_loaded;
+               if (tiles_loaded == tiles_per_layer) {
+                       ++layers_loaded;
+                       if (layers_loaded == layers_total) {
+                               SwingUtilities.invokeLater(new Runnable() {
+                                               public void run() {
+                                                       pbar.setValue(0);
+                                                       pbar.setString("");
+                                                       load_button.setSelected(false);
+                                                       loading = false;
+                                               }
+                                       });
+                       } else {
+                               SwingUtilities.invokeLater(new Runnable() {
+                                               public void run() {
+                                                       next_load();
+                                               }
+                                       });
+                       }
+               }
+       }
+
+       public void set_sites() {
+               int     i = 1;
+               for (AltosUISite 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 AltosUISite) {
+                               AltosUISite     site = (AltosUISite) 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 {
+                                       latitude = lat.get_value();
+                                       longitude = lon.get_value();
+                                       min_z = (Integer) min_zoom.getSelectedItem();
+                                       max_z = (Integer) max_zoom.getSelectedItem();
+                                       if (max_z < min_z)
+                                               max_z = min_z;
+                                       r = (Integer) radius.getSelectedItem();
+                                       loading = true;
+                               } catch (NumberFormatException ne) {
+                                       load_button.setSelected(false);
+                               }
+                               start_load();
+                       }
+               }
+       }
+
+       public AltosUIMapPreload(AltosUIFrame in_owner) {
+               System.out.printf("start creating preload ui\n");
+
+               owner = in_owner;
+
+               Container               pane = getContentPane();
+               GridBagConstraints      c = new GridBagConstraints();
+               Insets                  i = new Insets(4,4,4,4);
+
+               setTitle("AltOS Load Maps");
+
+               pane.setLayout(new GridBagLayout());
+
+               map = new AltosUIMap();
+
+               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 = 10;
+               c.anchor = GridBagConstraints.CENTER;
+
+               pane.add(map, c);
+
+               pbar = new JProgressBar();
+               pbar.setMinimum(0);
+               pbar.setMaximum(1);
+               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 = 10;
+
+               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<AltosUISite>(new AltosUISite[] { new AltosUISite("Site List", 0, 0) });
+               site_list.addItemListener(this);
+
+               sites = new AltosUISites(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 AltosUIMapPos(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 AltosUIMapPos(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);
+
+               JLabel  types_label = new JLabel("Map Types");
+               c.gridx = 2;
+               c.gridwidth = 2;
+               c.gridy = 2;
+               pane.add(types_label, c);
+
+               c.gridwidth = 1;
+
+               for (int type = AltosUIMap.maptype_hybrid; type <= AltosUIMap.maptype_terrain; type++) {
+                       maptypes[type] = new JCheckBox(AltosUIMap.maptype_labels[type],
+                                                      type == AltosUIMap.maptype_hybrid);
+                       c.gridx = 2 + (type >> 1);
+                       c.fill = GridBagConstraints.HORIZONTAL;
+                       c.gridy = (type & 1) + 3;
+                       pane.add(maptypes[type], c);
+               }
+
+               JLabel  min_zoom_label = new JLabel("Minimum Zoom");
+               c.gridx = 4;
+               c.gridy = 2;
+               pane.add(min_zoom_label, c);
+
+               min_zoom = new JComboBox<Integer>(zooms);
+               min_zoom.setSelectedItem(zooms[10]);
+               min_zoom.setEditable(false);
+               c.gridx = 5;
+               c.gridy = 2;
+               pane.add(min_zoom, c);
+
+               JLabel  max_zoom_label = new JLabel("Maximum Zoom");
+               c.gridx = 4;
+               c.gridy = 3;
+               pane.add(max_zoom_label, c);
+
+               max_zoom = new JComboBox<Integer>(zooms);
+               max_zoom.setSelectedItem(zooms[14]);
+               max_zoom.setEditable(false);
+               c.gridx = 5;
+               c.gridy = 3;
+               pane.add(max_zoom, c);
+
+               JLabel radius_label = new JLabel("Tile Radius");
+               c.gridx = 4;
+               c.gridy = 4;
+               pane.add(radius_label, c);
+
+               radius = new JComboBox<Integer>(radii);
+               radius.setSelectedItem(radii[4]);
+               radius.setEditable(true);
+               c.gridx = 5;
+               c.gridy = 4;
+               pane.add(radius, c);
+
+               pack();
+               setLocationRelativeTo(owner);
+               setVisible(true);
+
+               System.out.printf("done creating preload ui\n");
+       }
+}
diff --git a/altosuilib/AltosUIMapRectangle.java b/altosuilib/AltosUIMapRectangle.java
new file mode 100644 (file)
index 0000000..8a5b16e
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2014 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;
+
+public class AltosUIMapRectangle {
+       AltosUILatLon   ul, lr;
+
+       public AltosUIMapRectangle(AltosUILatLon a, AltosUILatLon b) {
+               double  ul_lat, ul_lon;
+               double  lr_lat, lr_lon;
+
+               if (a.lat > b.lat) {
+                       ul_lat = a.lat;
+                       lr_lat = b.lat;
+               } else {
+                       ul_lat = b.lat;
+                       lr_lat = a.lat;
+               }
+               if (a.lon < b.lon) {
+                       ul_lon = a.lon;
+                       lr_lon = b.lon;
+               } else {
+                       ul_lon = b.lon;
+                       lr_lon = a.lon;
+               }
+
+               ul = new AltosUILatLon(ul_lat, ul_lon);
+               lr = new AltosUILatLon(lr_lat, lr_lon);
+       }
+}
diff --git a/altosuilib/AltosUIMapStore.java b/altosuilib/AltosUIMapStore.java
new file mode 100644 (file)
index 0000000..4cecb54
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * Copyright © 2014 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.io.*;
+import java.net.*;
+import java.util.*;
+
+public class AltosUIMapStore {
+       String                                  url;
+       File                                    file;
+       LinkedList<AltosUIMapStoreListener>     listeners = new LinkedList<AltosUIMapStoreListener>();
+
+       static final int                        success = 0;
+       static final int                        loading = 1;
+       static final int                        failed = 2;
+       static final int                        bad_request = 3;
+       static final int                        forbidden = 4;
+
+       int                                     status;
+
+       public int status() {
+               return status;
+       }
+
+       public synchronized void add_listener(AltosUIMapStoreListener listener) {
+               if (!listeners.contains(listener))
+                       listeners.add(listener);
+       }
+
+       public synchronized void remove_listener(AltosUIMapStoreListener listener) {
+               listeners.remove(listener);
+       }
+
+       private synchronized void notify_listeners(int status) {
+               this.status = status;
+               for (AltosUIMapStoreListener listener : listeners)
+                       listener.notify_store(this, status);
+       }
+
+       static Object   forbidden_lock = new Object();
+       static long     forbidden_time;
+       static boolean  forbidden_set;
+
+       private int fetch_url() {
+               URL u;
+
+               try {
+                       u = new URL(url);
+               } catch (java.net.MalformedURLException e) {
+                       return bad_request;
+               }
+
+               byte[] data;
+               URLConnection uc = null;
+               try {
+                       uc = u.openConnection();
+                       String type = uc.getContentType();
+                       int contentLength = uc.getContentLength();
+                       if (uc instanceof HttpURLConnection) {
+                               int response = ((HttpURLConnection) uc).getResponseCode();
+                               switch (response) {
+                               case HttpURLConnection.HTTP_FORBIDDEN:
+                               case HttpURLConnection.HTTP_PAYMENT_REQUIRED:
+                               case HttpURLConnection.HTTP_UNAUTHORIZED:
+                                       synchronized (forbidden_lock) {
+                                               forbidden_time = System.nanoTime();
+                                               forbidden_set = true;
+                                               return forbidden;
+                                       }
+                               }
+                       }
+                       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 failed;
+
+               } catch (IOException e) {
+                       return failed;
+               }
+
+               try {
+                       FileOutputStream out = new FileOutputStream(file);
+                       out.write(data);
+                       out.flush();
+                       out.close();
+               } catch (FileNotFoundException e) {
+                       return bad_request;
+               } catch (IOException e) {
+                       if (file.exists())
+                               file.delete();
+                       return bad_request;
+               }
+               return success;
+       }
+
+       static Object   fetch_lock = new Object();
+
+       static final long       forbidden_interval = 60l * 1000l * 1000l * 1000l;
+       static final long       google_maps_ratelimit_ms = 1200;
+
+       class loader implements Runnable {
+
+               public void run() {
+                       if (file.exists()) {
+                               notify_listeners(success);
+                               return;
+                       }
+
+                       synchronized(forbidden_lock) {
+                               if (forbidden_set && (System.nanoTime() - forbidden_time) < forbidden_interval) {
+                                       notify_listeners(forbidden);
+                                       return;
+                               }
+                       }
+
+                       int new_status;
+
+                       if (!AltosUIVersion.has_google_maps_api_key()) {
+                               synchronized (fetch_lock) {
+                                       long startTime = System.nanoTime();
+                                       new_status = fetch_url();
+                                       if (new_status == success) {
+                                               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();
+                                                       }
+                                               }
+                                       }
+                               }
+                       } else {
+                               new_status = fetch_url();
+                       }
+                       notify_listeners(new_status);
+               }
+       }
+
+       private void load() {
+               loader  l = new loader();
+               Thread  lt = new Thread(l);
+               lt.start();
+       }
+
+       private AltosUIMapStore (String url, File file) {
+               this.url = url;
+               this.file = file;
+
+               if (file.exists())
+                       status = success;
+               else {
+                       status = loading;
+                       load();
+               }
+       }
+
+       public boolean equals(AltosUIMapStore other) {
+               return url.equals(other.url);
+       }
+
+       static HashMap<String,AltosUIMapStore> stores = new HashMap<String,AltosUIMapStore>();
+
+       public static AltosUIMapStore get(String url, File file) {
+               AltosUIMapStore store;
+               synchronized(stores) {
+                       if (stores.containsKey(url)) {
+                               store = stores.get(url);
+                       } else {
+                               store = new AltosUIMapStore(url, file);
+                               stores.put(url, store);
+                       }
+               }
+               return store;
+       }
+
+}
diff --git a/altosuilib/AltosUIMapStoreListener.java b/altosuilib/AltosUIMapStoreListener.java
new file mode 100644 (file)
index 0000000..91aff00
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Copyright © 2014 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;
+
+public interface AltosUIMapStoreListener {
+       abstract void notify_store(AltosUIMapStore store, int status);
+}
diff --git a/altosuilib/AltosUIMapTile.java b/altosuilib/AltosUIMapTile.java
new file mode 100644 (file)
index 0000000..6fbcdb4
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * 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.*;
+import java.io.*;
+import java.util.*;
+import java.awt.RenderingHints.*;
+import org.altusmetrum.altoslib_4.*;
+
+public class AltosUIMapTile {
+       AltosUIMapTileListener  listener;
+       AltosUILatLon   upper_left, center;
+       int             px_size;
+       int             zoom;
+       int             maptype;
+       AltosUIMapStore store;
+       int             status;
+
+       private File map_file() {
+               double lat = center.lat;
+               double lon = center.lon;
+               char chlat = lat < 0 ? 'S' : 'N';
+               char chlon = lon < 0 ? 'W' : 'E';
+
+               if (lat < 0) lat = -lat;
+               if (lon < 0) lon = -lon;
+               String maptype_string = String.format("%s-", AltosUIMap.maptype_names[maptype]);
+               String format_string;
+               if (maptype == AltosUIMap.maptype_hybrid || maptype == AltosUIMap.maptype_satellite || maptype == AltosUIMap.maptype_terrain)
+                       format_string = "jpg";
+               else
+                       format_string = "png";
+               return new File(AltosUIPreferences.mapdir(),
+                               String.format("map-%c%.6f,%c%.6f-%s%d.%s",
+                                             chlat, lat, chlon, lon, maptype_string, zoom, format_string));
+       }
+
+       private String map_url() {
+               String format_string;
+               if (maptype == AltosUIMap.maptype_hybrid || maptype == AltosUIMap.maptype_satellite || maptype == AltosUIMap.maptype_terrain)
+                       format_string = "jpg";
+               else
+                       format_string = "png32";
+
+               if (AltosUIVersion.has_google_maps_api_key())
+                       return String.format("http://maps.google.com/maps/api/staticmap?center=%.6f,%.6f&zoom=%d&size=%dx%d&sensor=false&maptype=%s&format=%s&key=%s",
+                                            center.lat, center.lon, zoom, px_size, px_size, AltosUIMap.maptype_names[maptype], format_string, AltosUIVersion.google_maps_api_key);
+               else
+                       return String.format("http://maps.google.com/maps/api/staticmap?center=%.6f,%.6f&zoom=%d&size=%dx%d&sensor=false&maptype=%s&format=%s",
+                                            center.lat, center.lon, zoom, px_size, px_size, AltosUIMap.maptype_names[maptype], format_string);
+       }
+       private Font    font = null;
+
+       public void set_font(Font font) {
+               this.font = font;
+       }
+
+       int     painting_serial;
+       int     painted_serial;
+
+       Image   image;
+
+       public void paint_graphics(Graphics2D g2d, AltosUIMapTransform t, int serial) {
+               if (serial < painted_serial)
+                       return;
+
+               Point2D.Double  point_double = t.screen(upper_left);
+               Point           point = new Point((int) (point_double.x + 0.5),
+                                                 (int) (point_double.y + 0.5));
+
+               painted_serial = serial;
+
+               if (!g2d.hitClip(point.x, point.y, px_size, px_size))
+                       return;
+
+               if (image != null) {
+                       g2d.drawImage(image, point.x, point.y, null);
+                       image = null;
+               } else {
+                       g2d.setColor(Color.GRAY);
+                       g2d.fillRect(point.x, point.y, px_size, px_size);
+
+                       if (t.has_location()) {
+                               String  message = null;
+                               switch (status) {
+                               case AltosUIMapCache.loading:
+                                       message = "Loading...";
+                                       break;
+                               case AltosUIMapCache.bad_request:
+                                       message = "Internal error";
+                                       break;
+                               case AltosUIMapCache.failed:
+                                       message = "Network error, check connection";
+                                       break;
+                               case AltosUIMapCache.forbidden:
+                                       message = "Too many requests, try later";
+                                       break;
+                               }
+                               if (message != null && font != null) {
+                                       g2d.setFont(font);
+                                       g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+                                       Rectangle2D bounds = font.getStringBounds(message, g2d.getFontRenderContext());
+
+                                       float x = px_size / 2.0f;
+                                       float y = px_size / 2.0f;
+                                       x = x - (float) bounds.getWidth() / 2.0f;
+                                       y = y + (float) bounds.getHeight() / 2.0f;
+                                       g2d.setColor(Color.BLACK);
+                                       g2d.drawString(message, (float) point_double.x + x, (float) point_double.y + y);
+                               }
+                       }
+               }
+       }
+
+       public void set_status(int status) {
+               this.status = status;
+               listener.notify_tile(this, status);
+       }
+
+       public void notify_image(Image image) {
+               listener.notify_tile(this, status);
+       }
+
+       public void paint(Graphics g, AltosUIMapTransform t) {
+               Graphics2D              g2d = (Graphics2D) g;
+               boolean                 queued = false;
+
+               Point2D.Double  point = t.screen(upper_left);
+
+               if (!g.hitClip((int) (point.x + 0.5), (int) (point.y + 0.5), px_size, px_size))
+                       return;
+
+               ++painting_serial;
+
+               if (image == null && t.has_location())
+                       image = AltosUIMapCache.get(this, store, px_size, px_size);
+
+               paint_graphics(g2d, t, painting_serial);
+       }
+
+       public int store_status() {
+               return store.status();
+       }
+
+       public void add_store_listener(AltosUIMapStoreListener listener) {
+               store.add_listener(listener);
+       }
+
+       public void remove_store_listener(AltosUIMapStoreListener listener) {
+               store.remove_listener(listener);
+       }
+
+       public AltosUIMapTile(AltosUIMapTileListener listener, AltosUILatLon upper_left, AltosUILatLon center, int zoom, int maptype, int px_size, Font font) {
+               this.listener = listener;
+               this.upper_left = upper_left;
+
+               while (center.lon < -180.0)
+                       center.lon += 360.0;
+               while (center.lon > 180.0)
+                       center.lon -= 360.0;
+
+               this.center = center;
+               this.zoom = zoom;
+               this.maptype = maptype;
+               this.px_size = px_size;
+               this.font = font;
+               status = AltosUIMapCache.loading;
+               store = AltosUIMapStore.get(map_url(), map_file());
+       }
+}
diff --git a/altosuilib/AltosUIMapTileListener.java b/altosuilib/AltosUIMapTileListener.java
new file mode 100644 (file)
index 0000000..4cc3ff2
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Copyright © 2014 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;
+
+public interface AltosUIMapTileListener {
+       abstract public void notify_tile(AltosUIMapTile tile, int status);
+}
diff --git a/altosuilib/AltosUIMapTransform.java b/altosuilib/AltosUIMapTransform.java
new file mode 100644 (file)
index 0000000..e6f1ffe
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * Copyright © 2014 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.lang.Math;
+import java.awt.geom.*;
+import java.util.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_4.*;
+
+public class AltosUIMapTransform {
+
+       double  scale_x, scale_y;
+
+       double  offset_x, offset_y;
+
+       public AltosUILatLon lat_lon (Point2D.Double point) {
+               double lat, lon;
+               double rads;
+
+               lon = point.x/scale_x;
+               rads = 2 * Math.atan(Math.exp(-point.y/scale_y));
+               lat = Math.toDegrees(rads - Math.PI/2);
+
+               return new AltosUILatLon(lat,lon);
+       }
+
+       public Point2D.Double screen_point(Point screen) {
+               return new Point2D.Double(screen.x + offset_x, screen.y + offset_y);
+       }
+
+       public AltosUILatLon screen_lat_lon(Point screen) {
+               return lat_lon(screen_point(screen));
+       }
+
+       public Point2D.Double point(AltosUILatLon lat_lon) {
+               double x, y;
+               double e;
+
+               x = lat_lon.lon * scale_x;
+
+               e = Math.sin(Math.toRadians(lat_lon.lat));
+               e = Math.max(e,-(1-1.0E-15));
+               e = Math.min(e,  1-1.0E-15 );
+
+               y = 0.5*Math.log((1+e)/(1-e))*-scale_y;
+
+               return new Point2D.Double(x, y);
+       }
+
+       public Point2D.Double screen(Point2D.Double point) {
+               return new Point2D.Double(point.x - offset_x, point.y - offset_y);
+       }
+
+       public Point screen(Point point) {
+               return new Point((int) (point.x - offset_x + 0.5),
+                                (int) (point.y - offset_y + 0.5));
+       }
+
+       public Rectangle screen(AltosUIMapRectangle map_rect) {
+               Point2D.Double  ul = screen(map_rect.ul);
+               Point2D.Double  lr = screen(map_rect.lr);
+
+               return new Rectangle((int) ul.x, (int) ul.y, (int) (lr.x - ul.x), (int) (lr.y - ul.y));
+       }
+
+       public Point2D.Double screen(AltosUILatLon lat_lon) {
+               return screen(point(lat_lon));
+       }
+
+       private boolean has_location;
+
+       public boolean has_location() {
+               return has_location;
+       }
+
+       public AltosUIMapTransform(int width, int height, int zoom, AltosUILatLon centre_lat_lon) {
+               scale_x = 256/360.0 * Math.pow(2, zoom);
+               scale_y = 256/(2.0*Math.PI) * Math.pow(2, zoom);
+
+               Point2D.Double centre_pt = point(centre_lat_lon);
+
+               has_location = (centre_lat_lon.lat != 0 || centre_lat_lon.lon != 0);
+               offset_x = centre_pt.x - width / 2.0;
+               offset_y = centre_pt.y - height / 2.0;
+       }
+}
diff --git a/altosuilib/AltosUIMapView.java b/altosuilib/AltosUIMapView.java
new file mode 100644 (file)
index 0000000..c558118
--- /dev/null
@@ -0,0 +1,462 @@
+/*
+ * Copyright © 2014 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 java.awt.image.*;
+import javax.swing.*;
+import java.io.*;
+import java.lang.*;
+import java.awt.geom.*;
+import java.util.*;
+import java.util.concurrent.*;
+import org.altusmetrum.altoslib_4.*;
+
+public class AltosUIMapView extends Canvas implements MouseMotionListener, MouseListener, MouseWheelListener, ComponentListener, AltosUIMapTileListener, AltosUIMapStoreListener {
+
+       AltosUIMapPath  path = new AltosUIMapPath();
+
+       AltosUIMapLine  line = new AltosUIMapLine();
+
+       LinkedList<AltosUIMapMark> marks = new LinkedList<AltosUIMapMark>();
+
+       LinkedList<AltosUIMapZoomListener> zoom_listeners = new LinkedList<AltosUIMapZoomListener>();
+
+       boolean         have_boost = false;
+       boolean         have_landed = false;
+
+       ConcurrentHashMap<Point,AltosUIMapTile> tiles = new ConcurrentHashMap<Point,AltosUIMapTile>();
+
+       static final int default_zoom = 15;
+       static final int min_zoom = 3;
+       static final int max_zoom = 21;
+       static final int px_size = 512;
+
+       int             load_radius;
+       AltosUILatLon   load_centre = null;
+       AltosUIMapTileListener  load_listener;
+
+       int             zoom = default_zoom;
+       int             maptype = AltosUIMap.maptype_default;
+
+       long            user_input_time;
+
+       /* Milliseconds to wait after user action before auto-scrolling
+        */
+       static final long auto_scroll_delay = 20 * 1000;
+
+       AltosUIMapTransform     transform;
+       AltosUILatLon           centre;
+
+       public void set_font() {
+               line.set_font(AltosUILib.value_font);
+               for (AltosUIMapTile tile : tiles.values())
+                       tile.set_font(AltosUILib.value_font);
+       }
+
+       private boolean is_drag_event(MouseEvent e) {
+               return e.getModifiers() == InputEvent.BUTTON1_MASK;
+       }
+
+       Point   drag_start;
+
+       private void drag(MouseEvent e) {
+               if (drag_start == null)
+                       return;
+
+               int dx = e.getPoint().x - drag_start.x;
+               int dy = e.getPoint().y - drag_start.y;
+
+               AltosUILatLon   new_centre = transform.screen_lat_lon(new Point(getWidth() / 2 - dx, getHeight() / 2 - dy));
+               centre (new_centre.lat, new_centre.lon);
+               drag_start = e.getPoint();
+       }
+
+       private void drag_start(MouseEvent e) {
+               drag_start = e.getPoint();
+       }
+
+       private void notice_user_input() {
+               user_input_time = System.currentTimeMillis();
+       }
+
+       private boolean recent_user_input() {
+               return (System.currentTimeMillis() - user_input_time) < auto_scroll_delay;
+       }
+
+       /* MouseMotionListener methods */
+
+       public void mouseDragged(MouseEvent e) {
+               notice_user_input();
+               if (is_drag_event(e))
+                       drag(e);
+               else {
+                       line.dragged(e, transform);
+                       repaint();
+               }
+       }
+
+       public void mouseMoved(MouseEvent e) {
+       }
+
+       /* MouseListener methods */
+       public void mouseClicked(MouseEvent e) {
+       }
+
+       public void mouseEntered(MouseEvent e) {
+       }
+
+       public void mouseExited(MouseEvent e) {
+       }
+
+       public void mousePressed(MouseEvent e) {
+               notice_user_input();
+               if (is_drag_event(e))
+                       drag_start(e);
+               else
+                       line.pressed(e, transform);
+       }
+
+       public void mouseReleased(MouseEvent e) {
+       }
+
+       /* MouseWheelListener methods */
+
+       public void mouseWheelMoved(MouseWheelEvent e) {
+               int     zoom_change = e.getWheelRotation();
+
+               notice_user_input();
+               AltosUILatLon   mouse_lat_lon = transform.screen_lat_lon(e.getPoint());
+               set_zoom(zoom() - zoom_change);
+
+               Point2D.Double  new_mouse = transform.screen(mouse_lat_lon);
+
+               int     dx = getWidth()/2 - e.getPoint().x;
+               int     dy = getHeight()/2 - e.getPoint().y;
+
+               AltosUILatLon   new_centre = transform.screen_lat_lon(new Point((int) new_mouse.x + dx, (int) new_mouse.y + dy));
+
+               centre(new_centre.lat, new_centre.lon);
+       }
+
+       /* ComponentListener methods */
+
+       public void componentHidden(ComponentEvent e) {
+       }
+
+       public void componentMoved(ComponentEvent e) {
+       }
+
+       public void componentResized(ComponentEvent e) {
+               set_transform();
+       }
+
+       public void componentShown(ComponentEvent e) {
+               set_transform();
+       }
+
+       public void repaint(Rectangle r, int pad) {
+               repaint(r.x - pad, r.y - pad, r.width + pad*2, r.height + pad*2);
+       }
+
+       public void repaint(AltosUIMapRectangle rect, int pad) {
+               repaint (transform.screen(rect), pad);
+       }
+
+       private boolean far_from_centre(AltosUILatLon lat_lon) {
+
+               if (centre == null || transform == null)
+                       return true;
+
+               Point2D.Double  screen = transform.screen(lat_lon);
+
+               int             width = getWidth();
+               int             dx = Math.abs ((int) screen.x - width/2);
+
+               if (dx > width / 4)
+                       return true;
+
+               int             height = getHeight();
+               int             dy = Math.abs ((int) screen.y - height/2);
+
+               if (dy > height / 4)
+                       return true;
+
+               return false;
+       }
+
+       public void show(AltosState state, 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;
+
+               AltosUIMapRectangle     damage = path.add(gps.lat, gps.lon, state.state);
+
+               switch (state.state) {
+               case AltosLib.ao_flight_boost:
+                       if (!have_boost) {
+                               add_mark(gps.lat, gps.lon, state.state);
+                               have_boost = true;
+                       }
+                       break;
+               case AltosLib.ao_flight_landed:
+                       if (!have_landed) {
+                               add_mark(gps.lat, gps.lon, state.state);
+                               have_landed = true;
+                       }
+                       break;
+               }
+
+               if (damage != null)
+                       repaint(damage, AltosUIMapPath.stroke_width);
+               maybe_centre(gps.lat, gps.lon);
+       }
+
+       private void set_transform() {
+               Rectangle       bounds = getBounds();
+
+               transform = new AltosUIMapTransform(bounds.width, bounds.height, zoom, centre);
+               repaint();
+       }
+
+       public boolean set_zoom(int zoom) {
+               if (min_zoom <= zoom && zoom <= max_zoom && zoom != this.zoom) {
+                       this.zoom = zoom;
+                       tiles.clear();
+                       set_transform();
+
+                       for (AltosUIMapZoomListener listener : zoom_listeners)
+                               listener.zoom_changed(this.zoom);
+
+                       return true;
+               }
+               return false;
+       }
+
+       public void add_zoom_listener(AltosUIMapZoomListener listener) {
+               if (!zoom_listeners.contains(listener))
+                       zoom_listeners.add(listener);
+       }
+
+       public void remove_zoom_listener(AltosUIMapZoomListener listener) {
+               zoom_listeners.remove(listener);
+       }
+
+       public void set_load_params(double lat, double lon, int radius, AltosUIMapTileListener listener) {
+               load_centre = new AltosUILatLon(lat, lon);
+               load_radius = radius;
+               load_listener = listener;
+               centre(lat, lon);
+               make_tiles();
+               for (AltosUIMapTile tile : tiles.values()) {
+                       tile.add_store_listener(this);
+                       if (tile.store_status() != AltosUIMapStore.loading)
+                               listener.notify_tile(tile, tile.store_status());
+               }
+               repaint();
+       }
+
+       public boolean all_fetched() {
+               for (AltosUIMapTile tile : tiles.values()) {
+                       if (tile.store_status() == AltosUIMapStore.loading)
+                               return false;
+               }
+               return true;
+       }
+
+       public boolean set_maptype(int maptype) {
+               if (maptype != this.maptype) {
+                       this.maptype = maptype;
+                       tiles.clear();
+                       repaint();
+                       return true;
+               }
+               return false;
+       }
+
+       public int get_maptype() {
+               return maptype;
+       }
+
+       public int zoom() {
+               return zoom;
+       }
+
+       public void centre(AltosUILatLon lat_lon) {
+               centre = lat_lon;
+               set_transform();
+       }
+
+       public void centre(double lat, double lon) {
+               centre(new AltosUILatLon(lat, lon));
+       }
+
+       public void maybe_centre(double lat, double lon) {
+               AltosUILatLon   lat_lon = new AltosUILatLon(lat, lon);
+               if (centre == null || (!recent_user_input() && far_from_centre(lat_lon)))
+                       centre(lat_lon);
+       }
+
+       private VolatileImage create_back_buffer() {
+               return getGraphicsConfiguration().createCompatibleVolatileImage(getWidth(), getHeight());
+       }
+
+       private Point floor(Point2D.Double point) {
+               return new Point ((int) Math.floor(point.x / px_size) * px_size,
+                                 (int) Math.floor(point.y / px_size) * px_size);
+       }
+
+       private Point ceil(Point2D.Double point) {
+               return new Point ((int) Math.ceil(point.x / px_size) * px_size,
+                                 (int) Math.ceil(point.y / px_size) * px_size);
+       }
+
+       private void make_tiles() {
+               Point   upper_left;
+               Point   lower_right;
+
+               if (load_centre != null) {
+                       Point centre = floor(transform.point(load_centre));
+
+                       upper_left = new Point(centre.x - load_radius * px_size,
+                                              centre.y - load_radius * px_size);
+                       lower_right = new Point(centre.x + load_radius * px_size,
+                                              centre.y + load_radius * px_size);
+               } else {
+                       upper_left = floor(transform.screen_point(new Point(0, 0)));
+                       lower_right = floor(transform.screen_point(new Point(getWidth(), getHeight())));
+               }
+               LinkedList<Point> to_remove = new LinkedList<Point>();
+
+               for (Point point : tiles.keySet()) {
+                       if (point.x < upper_left.x || lower_right.x < point.x ||
+                           point.y < upper_left.y || lower_right.y < point.y) {
+                               to_remove.add(point);
+                       }
+               }
+
+               for (Point point : to_remove)
+                       tiles.remove(point);
+
+               AltosUIMapCache.set_cache_size(((lower_right.y - upper_left.y) / px_size + 1) * ((lower_right.x - upper_left.x) / px_size + 1));
+               for (int y = upper_left.y; y <= lower_right.y; y += px_size) {
+                       for (int x = upper_left.x; x <= lower_right.x; x += px_size) {
+                               Point point = new Point(x, y);
+
+                               if (!tiles.containsKey(point)) {
+                                       AltosUILatLon   ul = transform.lat_lon(new Point2D.Double(x, y));
+                                       AltosUILatLon   center = transform.lat_lon(new Point2D.Double(x + px_size/2, y + px_size/2));
+                                       AltosUIMapTile tile = new AltosUIMapTile(this, ul, center, zoom, maptype,
+                                                                                px_size, AltosUILib.value_font);
+                                       tiles.put(point, tile);
+                               }
+                       }
+               }
+       }
+
+       /* AltosUIMapTileListener methods */
+       public void notify_tile(AltosUIMapTile tile, int status) {
+               for (Point point : tiles.keySet()) {
+                       if (tile == tiles.get(point)) {
+                               Point   screen = transform.screen(point);
+                               repaint(screen.x, screen.y, px_size, px_size);
+                       }
+               }
+       }
+
+       /* AltosUIMapStoreListener methods */
+       public void notify_store(AltosUIMapStore store, int status) {
+               if (load_listener != null) {
+                       for (AltosUIMapTile tile : tiles.values())
+                               if (store.equals(tile.store))
+                                       load_listener.notify_tile(tile, status);
+               }
+       }
+
+       private void do_paint(Graphics g) {
+               Graphics2D      g2d = (Graphics2D) g;
+
+               make_tiles();
+
+               for (AltosUIMapTile tile : tiles.values())
+                       tile.paint(g2d, transform);
+
+               synchronized(marks) {
+                       for (AltosUIMapMark mark : marks)
+                               mark.paint(g2d, transform);
+               }
+
+               path.paint(g2d, transform);
+
+               line.paint(g2d, transform);
+       }
+
+       public void paint(Graphics g) {
+
+               VolatileImage   back_buffer = create_back_buffer();
+               do {
+                       GraphicsConfiguration gc = getGraphicsConfiguration();
+                       int code = back_buffer.validate(gc);
+                       if (code == VolatileImage.IMAGE_INCOMPATIBLE)
+                               back_buffer = create_back_buffer();
+
+                       Graphics g_back = back_buffer.getGraphics();
+                       g_back.setClip(g.getClip());
+                       do_paint(g_back);
+                       g_back.dispose();
+
+                       g.drawImage(back_buffer, 0, 0, this);
+               } while (back_buffer.contentsLost());
+               back_buffer.flush();
+       }
+
+       public void update(Graphics g) {
+               paint(g);
+       }
+
+       public void add_mark(double lat, double lon, int state) {
+               synchronized(marks) {
+                       marks.add(new AltosUIMapMark(lat, lon, state));
+               }
+               repaint();
+       }
+
+       public void clear_marks() {
+               synchronized(marks) {
+                       marks.clear();
+               }
+       }
+
+       public AltosUIMapView() {
+               centre(0, 0);
+
+               addComponentListener(this);
+               addMouseMotionListener(this);
+               addMouseListener(this);
+               addMouseWheelListener(this);
+               set_font();
+       }
+}
diff --git a/altosuilib/AltosUIMapZoomListener.java b/altosuilib/AltosUIMapZoomListener.java
new file mode 100644 (file)
index 0000000..02e8bb5
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Copyright © 2014 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;
+
+public interface AltosUIMapZoomListener {
+       abstract public void zoom_changed(int zoom);
+}
index b90669c..466cfd9 100644 (file)
@@ -33,11 +33,6 @@ altosuilib_JAVA = \
        AltosUISeries.java \
        AltosUIVersion.java \
        AltosUSBDevice.java \
-       AltosSiteMap.java \
-       AltosSiteMapCache.java \
-       AltosSiteMapPreload.java \
-       AltosSiteMapTile.java \
-       AltosSiteMapImage.java \
        AltosVoice.java \
        AltosDisplayThread.java \
        AltosDeviceUIDialog.java \
@@ -65,8 +60,23 @@ altosuilib_JAVA = \
        AltosBTDevice.java \
        AltosBTDeviceIterator.java \
        AltosBTManage.java \
-       AltosBTKnown.java
-
+       AltosBTKnown.java \
+       AltosUIMap.java \
+       AltosUIMapView.java \
+       AltosUIMapLine.java \
+       AltosUIMapMark.java \
+       AltosUIMapPath.java \
+       AltosUIMapTile.java \
+       AltosUIMapCache.java \
+       AltosUIMapImage.java \
+       AltosUIMapTransform.java \
+       AltosUIMapRectangle.java \
+       AltosUIMapZoomListener.java \
+       AltosUIMapTileListener.java \
+       AltosUIMapPreload.java \
+       AltosUIMapStore.java \
+       AltosUIMapStoreListener.java \
+       AltosUILatLon.java
 
 JAR=altosuilib_$(ALTOSUILIB_VERSION).jar
 
index c61b245..2503d53 100644 (file)
@@ -67,7 +67,7 @@ public class TeleGPS
 
        JTabbedPane             pane;
 
-       AltosSiteMap            sitemap;
+       AltosUIMap              map;
        TeleGPSInfo             gps_info;
        AltosInfoTable          info_table;
 
@@ -167,7 +167,7 @@ public class TeleGPS
        }
 
        void load_maps() {
-               new AltosSiteMapPreload(this);
+               new AltosUIMapPreload(this);
        }
 
        void disconnect() {
@@ -438,9 +438,9 @@ public class TeleGPS
                c.gridwidth = 2;
                bag.add(pane, c);
 
-               sitemap = new AltosSiteMap();
-               pane.add("Site Map", sitemap);
-               displays.add(sitemap);
+               map = new AltosUIMap();
+               pane.add("Map", map);
+               displays.add(map);
 
                gps_info = new TeleGPSInfo();
                pane.add("Info", gps_info);
@@ -578,7 +578,7 @@ public class TeleGPS
                                } else {
                                        double lat = Double.parseDouble(args[i+1]);
                                        double lon = Double.parseDouble(args[i+2]);
-                                       AltosSiteMap.prefetchMaps(lat, lon);
+                                       AltosUIMap.prefetch_maps(lat, lon);
                                        i += 2;
                                }
                        } else if (args[i].equals("--replay"))
index b7fc4ca..fbc9657 100644 (file)
@@ -38,7 +38,7 @@ public class TeleGPSGraphUI extends AltosUIFrame
        JTabbedPane             pane;
        AltosGraph              graph;
        AltosUIEnable           enable;
-       AltosSiteMap            map;
+       AltosUIMap              map;
        AltosState              state;
        AltosFlightStats        stats;
        AltosGraphDataSet       graphDataSet;
@@ -69,7 +69,7 @@ public class TeleGPSGraphUI extends AltosUIFrame
                graph = new AltosGraph(enable, stats, graphDataSet);
                statsTable = new AltosFlightStatsTable(stats);
 
-               map = new AltosSiteMap();
+               map = new AltosUIMap();
 
                pane.add("Flight Graph", graph.panel);
                pane.add("Configure Graph", enable);