altosuilib: Publish AltosSiteMap.centre. Add prefetchMaps with old API
[fw/altos] / altosuilib / AltosSiteMap.java
index 1cfbc8b5f3be25384d8ddd74df75d60a75ceede4..7f0e1844b264d3efe5508e8719ff2626f46eef1e 100644 (file)
 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.Point2D;
+import java.awt.geom.*;
+import java.util.*;
 import java.util.concurrent.*;
 import org.altusmetrum.altoslib_4.*;
 
-public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
+class MapPoint {
+       double  lat, lon;
+       int     state;
+
+       public MapPoint(double lat, double lon, int state) {
+               this.lat = lat;
+               this.lon = lon;
+               this.state = state;
+       }
+
+       public boolean equals(MapPoint other) {
+               if (other == null)
+                       return false;
+               if (other.lat != lat)
+                       return false;
+               if (other.lon != lon)
+                       return false;
+               if (other.state != state)
+                       return false;
+               return true;
+       }
+}
+
+public class AltosSiteMap extends JComponent implements AltosFlightDisplay, MouseMotionListener, MouseListener, HierarchyBoundsListener {
        // preferred vertical step in a tile in naut. miles
        // will actually choose a step size between x and 2x, where this
        // is 1.5x
@@ -35,6 +60,29 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
 
        static final int MAX_TILE_DELTA = 100;
 
+       static final int maptype_hybrid = 0;
+       static final int maptype_roadmap = 1;
+       static final int maptype_satellite = 2;
+       static final int maptype_terrain = 3;
+
+       int maptype = maptype_hybrid;
+
+       static final String[] maptype_names = {
+               "hybrid",
+               "roadmap",
+               "satellite",
+               "terrain"
+       };
+
+       public static final String[] maptype_labels = {
+               "Hybrid",
+               "Roadmap",
+               "Satellite",
+               "Terrain"
+       };
+
+       LinkedList<MapPoint> points = new LinkedList<MapPoint>();
+
        private static Point2D.Double translatePoint(Point2D.Double p,
                        Point2D.Double d)
        {
@@ -89,7 +137,12 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
                return new LatLng(lat,lng);
        }
 
-       int zoom;
+       static final int default_zoom = 15;
+       static final int min_zoom = 3;
+       static final int max_zoom = 21;
+
+       int zoom = default_zoom;
+
        double scale_x, scale_y;
 
        int radius;     /* half width/height of tiles to load */
@@ -107,12 +160,16 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
        }
        */
 
+       private LatLng latlng(Point pt) {
+               return latlng(new Point2D.Double(pt.x, pt.y), scale_x, scale_y);
+       }
+
        ConcurrentHashMap<Point,AltosSiteMapTile> mapTiles = new ConcurrentHashMap<Point,AltosSiteMapTile>();
        Point2D.Double centre;
 
        private Point2D.Double tileCoordOffset(Point p) {
                return new Point2D.Double(centre.x - p.x*px_size,
-                                         centre.y - p.y * px_size);
+                                         centre.y - p.y*px_size);
        }
 
        private Point tileOffset(Point2D.Double p) {
@@ -121,19 +178,11 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
        }
 
        private Point2D.Double getBaseLocation(double lat, double lng) {
-               Point2D.Double locn, north_step;
-
-               zoom = 2;
-               // stupid loop structure to please Java's control flow analysis
-               do {
-                       zoom++;
-                       scale_x = 256/360.0 * Math.pow(2, zoom);
-                       scale_y = 256/(2.0*Math.PI) * Math.pow(2, zoom);
-                       locn = pt(lat, lng);
-                       north_step = pt(lat+tile_size_nmi*4/3/60.0, lng);
-                       if (locn.y - north_step.y > px_size)
-                               break;
-               } while (zoom < 22);
+               Point2D.Double locn = pt(0,0), north_step;
+
+               scale_x = 256/360.0 * Math.pow(2, zoom);
+               scale_y = 256/(2.0*Math.PI) * Math.pow(2, zoom);
+               locn = pt(lat, lng);
                locn.x = -px_size * Math.floor(locn.x/px_size);
                locn.y = -px_size * Math.floor(locn.y/px_size);
                return locn;
@@ -144,23 +193,42 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
        }
 
        public void set_font() {
-               // nothing
+               for (AltosSiteMapTile tile : mapTiles.values())
+                       tile.set_font(AltosUILib.value_font);
        }
 
-       private void loadMap(final AltosSiteMapTile tile,
-                            final File pngfile, String pngurl)
+       static final int load_mode_cached = 1;
+       static final int load_mode_uncached = 2;
+
+       private boolean load_map(final AltosSiteMapTile tile,
+                                final File pngfile, String pngurl,
+                                int load_mode)
        {
-               boolean loaded = AltosSiteMapCache.fetchMap(pngfile, pngurl);
-               if (loaded) {
-                       SwingUtilities.invokeLater(new Runnable() {
-                                       public void run() {
-                                               tile.loadMap(pngfile);
-                                       }
-                               });
+               boolean has_map = AltosSiteMapCache.has_map(pngfile, pngurl);
+               if ((load_mode & load_mode_uncached) == 0 && !has_map)
+                       return false;
+               if ((load_mode & load_mode_cached) == 0 && has_map)
+                       return false;
+
+               tile.set_status(AltosSiteMapCache.loading);
+               int status = AltosSiteMapCache.fetch_map(pngfile, pngurl);
+               if (status == AltosSiteMapCache.success) {
+                       if (SwingUtilities.isEventDispatchThread())
+                               tile.load_map(pngfile);
+                       else {
+                               SwingUtilities.invokeLater(new Runnable() {
+                                               public void run() {
+                                                       tile.load_map(pngfile);
+                                               }
+                                       });
+                       }
                } else {
-                       System.out.printf("# Failed to fetch file %s\n", pngfile);
+                       tile.set_status(status);
+                       System.out.printf("# Failed to fetch file %s (status %d)\n", pngfile, status);
                        System.out.printf(" wget -O '%s' '%s'\n", pngfile, pngurl);
+                       System.out.printf(" sleep 1\n");
                }
+               return true;
        }
 
 
@@ -172,16 +240,16 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
                String  pngurl;
        }
 
-       public AltosSiteMapPrefetch prefetchMap(int x, int y) {
+       private AltosSiteMapPrefetch prefetchMap(int x, int y) {
                AltosSiteMapPrefetch    prefetch = new AltosSiteMapPrefetch();
                LatLng map_latlng = latlng(
                        -centre.x + x*px_size + px_size/2,
                        -centre.y + y*px_size + px_size/2);
-               prefetch.pngfile = MapFile(map_latlng.lat, map_latlng.lng, zoom);
-               prefetch.pngurl = MapURL(map_latlng.lat, map_latlng.lng, zoom);
-               if (prefetch.pngfile.exists()) {
+               prefetch.pngfile = MapFile(map_latlng.lat, map_latlng.lng, zoom, maptype_hybrid);
+               prefetch.pngurl = MapURL(map_latlng.lat, map_latlng.lng, zoom, maptype_hybrid);
+               if (AltosSiteMapCache.has_map(prefetch.pngfile, prefetch.pngurl)) {
                        prefetch.result = 1;
-               } else if (AltosSiteMapCache.fetchMap(prefetch.pngfile, prefetch.pngurl)) {
+               } else if (AltosSiteMapCache.fetch_map(prefetch.pngfile, prefetch.pngurl) == AltosSiteMapCache.success) {
                        prefetch.result = 0;
                } else {
                        prefetch.result = -1;
@@ -189,62 +257,87 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
                return prefetch;
        }
 
-       public static void prefetchMaps(double lat, double lng) {
-               int w = AltosSiteMapPreload.width;
-               int h = AltosSiteMapPreload.height;
+       public static void prefetchMaps(double lat, double lng, int radius, int maptypes, int min_zoom, int max_zoom) {
                AltosSiteMap asm = new AltosSiteMap(true);
-               asm.centre = asm.getBaseLocation(lat, lng);
-
-               //Point2D.Double p = new Point2D.Double();
-               //Point2D.Double p2;
-               int dx = -w/2, dy = -h/2;
-               for (int y = dy; y < h+dy; y++) {
-                       for (int x = dx; x < w+dx; x++) {
-                               AltosSiteMapPrefetch prefetch = asm.prefetchMap(x, y);
-                               switch (prefetch.result) {
-                               case 1:
-                                       System.out.printf("Already have %s\n", prefetch.pngfile);
-                                       break;
-                               case 0:
-                                       System.out.printf("Fetched map %s\n", prefetch.pngfile);
-                                       break;
-                               case -1:
-                                       System.out.printf("# Failed to fetch file %s\n", prefetch.pngfile);
-                                       System.out.printf(" wget -O '%s' ''\n",
-                                                         prefetch.pngfile, prefetch.pngurl);
-                                       break;
+
+               for (int z = min_zoom; z <= max_zoom; z++) {
+                       asm.zoom = z;
+                       asm.set_radius(radius);
+                       asm.centre = asm.getBaseLocation(lat, lng);
+                       for (int t = maptype_hybrid; t <= maptype_terrain; t++) {
+                               if ((maptypes & (1 << t)) !=0) {
+                                       asm.maptype = t;
+                                       for (int y = -radius; y <= radius; y++) {
+                                               for (int x = -radius; x <= radius; x++) {
+                                                       AltosSiteMapPrefetch prefetch = asm.prefetchMap(x, y);
+                                                       switch (prefetch.result) {
+                                                       case 1:
+                                                               System.out.printf("Already have %s\n", prefetch.pngfile);
+                                                               break;
+                                                       case 0:
+                                                               System.out.printf("Fetched map %s\n", prefetch.pngfile);
+                                                               break;
+                                                       case -1:
+                                                               System.out.printf("# Failed to fetch file %s\n", prefetch.pngfile);
+                                                               System.out.printf(" wget -O '%s' ''\n",
+                                                                                 prefetch.pngfile, prefetch.pngurl);
+                                                               break;
+                                                       }
+                                               }
+                                       }
                                }
                        }
                }
        }
 
-       public String initMap(Point offset) {
+       public static void prefetchMaps(double lat, double lon) {
+               prefetchMaps(lat, lon, 2, 1 << maptype_hybrid, 0, 0);
+       }
+
+       public File init_map(Point offset, int load_mode) {
                AltosSiteMapTile tile = mapTiles.get(offset);
                Point2D.Double coord = tileCoordOffset(offset);
 
                LatLng map_latlng = latlng(px_size/2-coord.x, px_size/2-coord.y);
 
-               File pngfile = MapFile(map_latlng.lat, map_latlng.lng, zoom);
-               String pngurl = MapURL(map_latlng.lat, map_latlng.lng, zoom);
-               loadMap(tile, pngfile, pngurl);
-               return pngfile.toString();
+               File pngfile = MapFile(map_latlng.lat, map_latlng.lng, zoom, maptype);
+               String pngurl = MapURL(map_latlng.lat, map_latlng.lng, zoom, maptype);
+               load_map(tile, pngfile, pngurl, load_mode);
+               return pngfile;
        }
 
-       public void initAndFinishMapAsync (final AltosSiteMapTile tile, final Point offset) {
+       private void initAndFinishMapAsync (final AltosSiteMapTile tile, final Point offset) {
                Thread thread = new Thread() {
                                public void run() {
-                                       initMap(offset);
-                                       finishTileLater(tile, offset);
+                                       init_map(offset, load_mode_cached|load_mode_uncached);
+                                       SwingUtilities.invokeLater( new Runnable() {
+                                                       public void run() {
+                                                               addTileAt(tile, offset);
+                                                       }
+                                               } );
                                }
                        };
                thread.start();
        }
 
-       public void setBaseLocation(double lat, double lng) {
-               for (Point k : mapTiles.keySet()) {
-                       AltosSiteMapTile tile = mapTiles.get(k);
+       double  lat, lon;
+       boolean base_location_set = false;
+
+       public void clear_base_location() {
+               base_location_set = false;
+               circle_set = false;
+               points = new LinkedList<MapPoint>();
+               line_start = line_end = null;
+               for (AltosSiteMapTile tile : mapTiles.values()) {
                        tile.clearMap();
+                       tile.set_status(AltosSiteMapCache.loading);
                }
+       }
+
+       public void setBaseLocation(double lat, double lng) {
+               this.lat = lat;
+               this.lon = lng;
+               base_location_set = true;
 
                centre = getBaseLocation(lat, lng);
                scrollRocketToVisible(pt(lat,lng));
@@ -253,75 +346,110 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
        private void initMaps(double lat, double lng) {
                setBaseLocation(lat, lng);
 
+               for (AltosSiteMapTile tile : mapTiles.values()) {
+                       tile.clearMap();
+                       tile.set_status(AltosSiteMapCache.loading);
+               }
                Thread thread = new Thread() {
                                public void run() {
                                        for (Point k : mapTiles.keySet())
-                                               initMap(k);
+                                               init_map(k, load_mode_cached);
+                                       for (Point k : mapTiles.keySet())
+                                               init_map(k, load_mode_uncached);
                                }
                        };
                thread.start();
        }
 
-       private static File MapFile(double lat, double lng, int zoom) {
+       private static File MapFile(double lat, double lng, int zoom, int maptype) {
                char chlat = lat < 0 ? 'S' : 'N';
                char chlng = lng < 0 ? 'W' : 'E';
                if (lat < 0) lat = -lat;
                if (lng < 0) lng = -lng;
+               String maptype_string = String.format("%s-", maptype_names[maptype]);
+               String format_string;
+               if (maptype == maptype_hybrid || maptype == maptype_satellite || maptype == maptype_terrain)
+                       format_string = "jpg";
+               else
+                       format_string = "png";
                return new File(AltosUIPreferences.mapdir(),
-                               String.format("map-%c%.6f,%c%.6f-%d.png",
-                                             chlat, lat, chlng, lng, zoom));
+                               String.format("map-%c%.6f,%c%.6f-%s%d.%s",
+                                             chlat, lat, chlng, lng, maptype_string, zoom, format_string));
        }
 
-       private static String MapURL(double lat, double lng, int zoom) {
-               return String.format("http://maps.google.com/maps/api/staticmap?center=%.6f,%.6f&zoom=%d&size=%dx%d&sensor=false&maptype=hybrid&format=png32", lat, lng, zoom, px_size, px_size);
+       private static String MapURL(double lat, double lng, int zoom, int maptype) {
+               String format_string;
+               if (maptype == maptype_hybrid || maptype == maptype_satellite || maptype == 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",
+                                            lat, lng, zoom, px_size, px_size, 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",
+                                            lat, lng, zoom, px_size, px_size, maptype_names[maptype], format_string);
        }
 
        boolean initialised = false;
-       Point2D.Double last_pt = null;
+       MapPoint last_point = null;
        int last_state = -1;
 
-       public void show(double lat, double lon) {
+       private void show(double lat, double lon) {
                System.out.printf ("show %g %g\n", lat, lon);
                return;
 //             initMaps(lat, lon);
 //             scrollRocketToVisible(pt(lat, lon));
        }
-       public void show(final AltosState state, final AltosListenerState listener_state) {
-               // if insufficient gps data, nothing to update
-               AltosGPS        gps = state.gps;
 
-               if (gps == null)
-                       return;
+       JLabel  zoom_label;
 
-               if (!gps.locked && gps.nsat < 4)
-                       return;
+       private void set_zoom_label() {
+               zoom_label.setText(String.format("Zoom %d", zoom - default_zoom));
+       }
 
-               if (!initialised) {
-                       if (state.pad_lat != AltosLib.MISSING && state.pad_lon != AltosLib.MISSING) {
-                               initMaps(state.pad_lat, state.pad_lon);
-                               initialised = true;
-                       } else if (gps.lat != AltosLib.MISSING && gps.lon != AltosLib.MISSING) {
-                               initMaps(gps.lat, gps.lon);
-                               initialised = true;
-                       } else {
-                               return;
+       public void set_zoom(int zoom) {
+               if (min_zoom <= zoom && zoom <= max_zoom) {
+                       this.zoom = zoom;
+                       if (base_location_set) {
+                               set_tiles();
+                               initMaps(lat, lon);
                        }
+                       redraw();
+                       set_zoom_label();
                }
+       }
 
-               final Point2D.Double pt = pt(gps.lat, gps.lon);
-               if (last_pt == pt && last_state == state.state)
-                       return;
+       public int get_zoom() {
+               return zoom;
+       }
+
+       public void set_maptype(int type) {
+               maptype = type;
+               maptype_combo.setSelectedIndex(type);
+               if (base_location_set)
+                       initMaps(lat, lon);
+               redraw();
+       }
 
-               if (last_pt == null) {
-                       last_pt = pt;
+       private void draw(MapPoint last_point, MapPoint point) {
+               boolean force_ensure = false;
+               if (last_point == null) {
+                       force_ensure = true;
+                       last_point = point;
                }
+
+               Point2D.Double pt = pt(point.lat, point.lon);
+               Point2D.Double last_pt = pt(last_point.lat, last_point.lon);
+
                boolean in_any = false;
                for (Point offset : mapTiles.keySet()) {
                        AltosSiteMapTile tile = mapTiles.get(offset);
                        Point2D.Double ref, lref;
                        ref = translatePoint(pt, tileCoordOffset(offset));
                        lref = translatePoint(last_pt, tileCoordOffset(offset));
-                       tile.show(state, listener_state, lref, ref);
+                       tile.show(point.state, lref, ref);
                        if (0 <= ref.x && ref.x < px_size)
                                if (0 <= ref.y && ref.y < px_size)
                                        in_any = true;
@@ -334,18 +462,62 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
                        lref = translatePoint(last_pt, tileCoordOffset(offset));
 
                        AltosSiteMapTile tile = createTile(offset);
-                       tile.show(state, listener_state, lref, ref);
+                       tile.show(point.state, lref, ref);
                        initAndFinishMapAsync(tile, offset);
                }
 
                scrollRocketToVisible(pt);
 
-               if (offset != tileOffset(last_pt)) {
+               if (force_ensure || offset != tileOffset(last_pt)) {
                        ensureTilesAround(offset);
                }
+       }
+
+       private void redraw() {
+               MapPoint        last_point = null;
+
+               for (MapPoint point : points) {
+                       draw(last_point, point);
+                       last_point = point;
+               }
+               if (circle_set)
+                       draw_circle(circle_lat, circle_lon);
+               if (line_start != null)
+                       set_line();
+       }
+
+       public void show(final AltosState state, final AltosListenerState listener_state) {
+               // if insufficient gps data, nothing to update
+               AltosGPS        gps = state.gps;
+
+               if (gps == null)
+                       return;
+
+               if (!gps.locked && gps.nsat < 4)
+                       return;
+
+               if (!initialised) {
+                       if (state.pad_lat != AltosLib.MISSING && state.pad_lon != AltosLib.MISSING) {
+                               initMaps(state.pad_lat, state.pad_lon);
+                               initialised = true;
+                       } else if (gps.lat != AltosLib.MISSING && gps.lon != AltosLib.MISSING) {
+                               initMaps(gps.lat, gps.lon);
+                               initialised = true;
+                       } else {
+                               return;
+                       }
+               }
+
+               MapPoint        point = new MapPoint(gps.lat, gps.lon, state.state);
 
-               last_pt = pt;
-               last_state = state.state;
+               if (point.equals(last_point))
+                       return;
+
+               points.add(point);
+
+               draw(last_point, point);
+
+               last_point = point;
        }
 
        public void centre(Point2D.Double pt) {
@@ -355,6 +527,8 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
                int dy = (int)copt.y - r.height/2 - r.y;
                r.x += dx;
                r.y += dy;
+               r.width = 1;
+               r.height = 1;
                comp.scrollRectToVisible(r);
        }
 
@@ -364,8 +538,15 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
                centre(pt(state.gps.lat, state.gps.lon));
        }
 
+       private double circle_lat, circle_lon;
+       private boolean circle_set = false;
+
        public void draw_circle(double lat, double lon) {
-               final Point2D.Double pt = pt(lat, lon);
+               circle_lat = lat;
+               circle_lon = lon;
+               circle_set = true;
+
+               Point2D.Double pt = pt(lat, lon);
 
                for (Point offset : mapTiles.keySet()) {
                        AltosSiteMapTile tile = mapTiles.get(offset);
@@ -376,18 +557,10 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
 
        private AltosSiteMapTile createTile(Point offset) {
                AltosSiteMapTile tile = new AltosSiteMapTile(px_size);
+               tile.set_font(AltosUILib.value_font);
                mapTiles.put(offset, tile);
                return tile;
        }
-       private void finishTileLater(final AltosSiteMapTile tile,
-                                    final Point offset)
-       {
-               SwingUtilities.invokeLater( new Runnable() {
-                       public void run() {
-                               addTileAt(tile, offset);
-                       }
-               } );
-       }
 
        private void ensureTilesAround(Point base_offset) {
                for (int x = -radius; x <= radius; x++) {
@@ -401,10 +574,37 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
                }
        }
 
+       private void set_tiles() {
+               for (int x = -radius; x <= radius; x++) {
+                       for (int y = -radius; y <= radius; y++) {
+                               Point offset = new Point(x, y);
+                               if (mapTiles.containsKey(offset))
+                                       continue;
+                               AltosSiteMapTile t = createTile(offset);
+                               addTileAt(t, offset);
+                       }
+               }
+               for (Point offset : mapTiles.keySet()) {
+                       if (offset.x < -radius || offset.x > radius ||
+                           offset.y < -radius || offset.y > radius)
+                       {
+                               removeTileAt(offset);
+                       }
+               }
+       }
+
+       public void set_radius(int radius) {
+               if (radius != this.radius) {
+                       this.radius = radius;
+                       set_tiles();
+               }
+       }
+
        private Point topleft = new Point(0,0);
        private void scrollRocketToVisible(Point2D.Double pt) {
                Rectangle r = comp.getVisibleRect();
                Point2D.Double copt = translatePoint(pt, tileCoordOffset(topleft));
+
                int dx = (int)copt.x - r.width/2 - r.x;
                int dy = (int)copt.y - r.height/2 - r.y;
                if (Math.abs(dx) > r.width/4 || Math.abs(dy) > r.height/4) {
@@ -423,18 +623,11 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
                        return;
                }
 
-               boolean review = false;
-               Rectangle r = comp.getVisibleRect();
-               if (offset.x < topleft.x) {
-                       r.x += (topleft.x - offset.x) * px_size;
+               if (offset.x < topleft.x)
                        topleft.x = offset.x;
-                       review = true;
-               }
-               if (offset.y < topleft.y) {
-                       r.y += (topleft.y - offset.y) * px_size;
+               if (offset.y < topleft.y)
                        topleft.y = offset.y;
-                       review = true;
-               }
+
                GridBagConstraints c = new GridBagConstraints();
                c.anchor = GridBagConstraints.CENTER;
                c.fill = GridBagConstraints.BOTH;
@@ -446,9 +639,6 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
                layout.setConstraints(tile, c);
 
                comp.add(tile);
-               if (review) {
-                       comp.scrollRectToVisible(r);
-               }
        }
 
        private AltosSiteMap(boolean knowWhatYouAreDoing) {
@@ -457,25 +647,239 @@ public class AltosSiteMap extends JScrollPane implements AltosFlightDisplay {
                }
        }
 
-       JComponent comp = new JComponent() { };
+       private void removeTileAt(Point offset) {
+               AltosSiteMapTile        tile = mapTiles.get(offset);
+
+               mapTiles.remove(offset);
+               comp.remove(tile);
+
+               topleft = new Point(MAX_TILE_DELTA, MAX_TILE_DELTA);
+               for (Point o : mapTiles.keySet()) {
+                       if (o.x < topleft.x)
+                               topleft.x = o.x;
+                       if (o.y < topleft.y)
+                               topleft.y = o.y;
+               }
+       }
+
+       JComponent comp;
+
        private GridBagLayout layout = new GridBagLayout();
 
+       LatLng  line_start, line_end;
+
+       private void set_line() {
+               if (line_start != null && line_end != null) {
+                       Point2D.Double  start = pt(line_start.lat, line_start.lng);
+                       Point2D.Double  end = pt(line_end.lat, line_end.lng);
+                       AltosGreatCircle        g = new AltosGreatCircle(line_start.lat, line_start.lng,
+                                                                        line_end.lat, line_end.lng);
+
+                       for (Point offset : mapTiles.keySet()) {
+                               AltosSiteMapTile tile = mapTiles.get(offset);
+                               Point2D.Double s, e;
+                               s = translatePoint(start, tileCoordOffset(offset));
+                               e = translatePoint(end, tileCoordOffset(offset));
+                               tile.set_line(new Line2D.Double(s.x, s.y, e.x, e.y), g.distance);
+                       }
+               } else {
+                       for (AltosSiteMapTile tile : mapTiles.values())
+                               tile.set_line(null, 0);
+               }
+       }
+
+       static void debug_component(Component who, String where) {
+/*
+               Rectangle       r = who.getBounds();
+               int             x = r.x / px_size;
+               int             y = r.y / px_size;
+
+               System.out.printf ("%3d, %3d: %s\n", x, y, where);
+*/
+       }
+
+       LatLng latlng(MouseEvent e) {
+               if (!base_location_set)
+                       return null;
+
+               Rectangle       zerozero = mapTiles.get(new Point(0, 0)).getBounds();
+
+               return latlng(-centre.x + e.getPoint().x - zerozero.x, -centre.y + e.getPoint().y - zerozero.y);
+       }
+
+       /* MouseMotionListener methods */
+       public void mouseDragged(MouseEvent e) {
+               if (!GrabNDrag.grab_n_drag(e)) {
+                       LatLng  loc = latlng(e);
+                       line_end = loc;
+                       set_line();
+               }
+       }
+
+       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) {
+               if (!GrabNDrag.grab_n_drag(e)) {
+                       LatLng  loc = latlng(e);
+                       line_start = loc;
+                       line_end = null;
+                       set_line();
+               }
+       }
+
+       public void mouseReleased(MouseEvent e) {
+       }
+
+       private void set_cache_size() {
+               Rectangle       r = comp.getVisibleRect();
+
+               int     width_tiles = (r.width + 2*px_size) / px_size;
+               int     height_tiles = (r.height + 2*px_size) / px_size;
+               int     tiles = width_tiles * height_tiles;
+               AltosSiteMapCache.set_cache_size(tiles);
+       }
+
+       /* HierarchyBoundsListener methods */
+       public void ancestorMoved(HierarchyEvent e) {
+               set_cache_size();
+       }
+
+       public void ancestorResized(HierarchyEvent e) {
+               set_cache_size();
+       }
+
+       JScrollPane     pane = new JScrollPane();
+
+       JComboBox<String>       maptype_combo;
+
        public AltosSiteMap(int in_radius) {
                radius = in_radius;
 
+               comp = new JComponent() { };
+
+               comp.addMouseMotionListener(this);
+               comp.addMouseListener(this);
+               comp.addHierarchyBoundsListener(this);
+
                GrabNDrag scroller = new GrabNDrag(comp);
 
                comp.setLayout(layout);
 
-               for (int x = -radius; x <= radius; x++) {
-                       for (int y = -radius; y <= radius; y++) {
-                               Point offset = new Point(x, y);
-                               AltosSiteMapTile t = createTile(offset);
-                               addTileAt(t, offset);
-                       }
-               }
-               setViewportView(comp);
-               setPreferredSize(new Dimension(500,500));
+               set_tiles();
+
+               pane.setViewportView(comp);
+               pane.setPreferredSize(new Dimension(500,500));
+               pane.setVisible(true);
+               pane.setEnabled(true);
+
+               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(pane, 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(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) {
+                                       maptype = maptype_combo.getSelectedIndex();
+                                       if (base_location_set)
+                                               initMaps(lat, lon);
+                                       redraw();
+                               }
+                       });
+
+               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);
        }
 
        public AltosSiteMap() {