From: Keith Packard Date: Wed, 28 May 2014 03:34:29 +0000 (-0700) Subject: altosuilib: Decompress map images asynchronously and in parallel X-Git-Tag: 1.3.2.2~65 X-Git-Url: https://git.gag.com/?p=fw%2Faltos;a=commitdiff_plain;h=e6cfa25702b3dc1d492c5f1a4d0b4ba4831d30bd;hp=8e44580cbe978f1570d4d2ac13d3dd7cd470ecf7 altosuilib: Decompress map images asynchronously and in parallel This speeds up loading map images from disk quite a bit, and keeps the UI responsive while that happens as well. Signed-off-by: Keith Packard --- diff --git a/altosuilib/AltosSiteMap.java b/altosuilib/AltosSiteMap.java index f22d9531..ee9b8c05 100644 --- a/altosuilib/AltosSiteMap.java +++ b/altosuilib/AltosSiteMap.java @@ -50,7 +50,7 @@ class MapPoint { } } -public class AltosSiteMap extends JComponent implements AltosFlightDisplay, MouseMotionListener, MouseListener { +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 @@ -213,11 +213,15 @@ public class AltosSiteMap extends JComponent implements AltosFlightDisplay, Mous tile.set_status(AltosSiteMapCache.loading); int status = AltosSiteMapCache.fetch_map(pngfile, pngurl); if (status == AltosSiteMapCache.success) { - SwingUtilities.invokeLater(new Runnable() { - public void run() { - tile.load_map(pngfile); - } - }); + if (SwingUtilities.isEventDispatchThread()) + tile.load_map(pngfile); + else { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + tile.load_map(pngfile); + } + }); + } } else { tile.set_status(status); System.out.printf("# Failed to fetch file %s (status %d)\n", pngfile, status); @@ -298,7 +302,11 @@ public class AltosSiteMap extends JComponent implements AltosFlightDisplay, Mous Thread thread = new Thread() { public void run() { init_map(offset, load_mode_cached|load_mode_uncached); - finishTileLater(tile, offset); + SwingUtilities.invokeLater( new Runnable() { + public void run() { + addTileAt(tile, offset); + } + } ); } }; thread.start(); @@ -315,8 +323,6 @@ public class AltosSiteMap extends JComponent implements AltosFlightDisplay, Mous } public void setBaseLocation(double lat, double lng) { - for (AltosSiteMapTile tile : mapTiles.values()) - tile.clearMap(); this.lat = lat; this.lon = lng; base_location_set = true; @@ -328,6 +334,8 @@ public class AltosSiteMap extends JComponent implements AltosFlightDisplay, Mous private void initMaps(double lat, double lng) { setBaseLocation(lat, lng); + for (AltosSiteMapTile tile : mapTiles.values()) + tile.clearMap(); Thread thread = new Thread() { public void run() { for (Point k : mapTiles.keySet()) @@ -379,7 +387,7 @@ public class AltosSiteMap extends JComponent implements AltosFlightDisplay, Mous JLabel zoom_label; public void set_zoom_label() { - zoom_label.setText(String.format("- %d -", zoom - default_zoom)); + zoom_label.setText(String.format("Zoom %d", zoom - default_zoom)); } public void set_zoom(int zoom) { @@ -524,15 +532,6 @@ public class AltosSiteMap extends JComponent implements AltosFlightDisplay, Mous 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++) { @@ -629,6 +628,14 @@ public class AltosSiteMap extends JComponent implements AltosFlightDisplay, Mous } } + 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; @@ -672,6 +679,24 @@ public class AltosSiteMap extends JComponent implements AltosFlightDisplay, Mous public void mouseReleased(MouseEvent e) { } + public 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(); public AltosSiteMap(int in_radius) { @@ -681,6 +706,7 @@ public class AltosSiteMap extends JComponent implements AltosFlightDisplay, Mous comp.addMouseMotionListener(this); comp.addMouseListener(this); + comp.addHierarchyBoundsListener(this); GrabNDrag scroller = new GrabNDrag(comp); diff --git a/altosuilib/AltosSiteMapCache.java b/altosuilib/AltosSiteMapCache.java index 42914deb..6e6046bc 100644 --- a/altosuilib/AltosSiteMapCache.java +++ b/altosuilib/AltosSiteMapCache.java @@ -24,62 +24,6 @@ import java.awt.*; import java.io.*; import java.net.*; -class AltosCacheImage { - Component component; - File file; - VolatileImage image; - int width; - int height; - long used; - - public void load_image() throws IOException { - BufferedImage bimg = ImageIO.read(file); - if (bimg == null) - throw new IOException("Can't load image file"); - Graphics2D g = image.createGraphics(); - g.drawImage(bimg, 0, 0, null); - bimg.flush(); - bimg = null; - } - - public Image validate() { - int returnCode; - - if (image != null) - returnCode = image.validate(component.getGraphicsConfiguration()); - else - returnCode = VolatileImage.IMAGE_INCOMPATIBLE; - if (returnCode == VolatileImage.IMAGE_RESTORED) { - try { - load_image(); - } catch (IOException e) { - return null; - } - } else if (returnCode == VolatileImage.IMAGE_INCOMPATIBLE) { - image = component.createVolatileImage(width, height); - try { - load_image(); - } catch (IOException e) { - return null; - } - } - return image; - } - - public void flush() { - image.flush(); - } - - public AltosCacheImage(Component component, File file, int w, int h) throws IOException { - this.component = component; - this.file = file; - width = w; - height = h; - image = component.createVolatileImage(w, h); - used = 0; - } -} - public class AltosSiteMapCache { static final long google_maps_ratelimit_ms = 1200; // Google limits static map queries to 50 per minute per IP, so @@ -99,121 +43,197 @@ public class AltosSiteMapCache { static boolean forbidden_set = false; static final long forbidden_interval = 60l * 1000l * 1000l * 1000l; - public static synchronized int fetch_map(File file, String url) { + static private Object fetch_lock = new Object(); + + public static int fetch_map(File file, String url) { if (file.exists()) return success; if (forbidden_set && (System.nanoTime() - forbidden_time) < forbidden_interval) return forbidden; - URL u; - long startTime = System.nanoTime(); + synchronized (fetch_lock) { + URL u; + long startTime = System.nanoTime(); - try { - u = new URL(url); - } catch (java.net.MalformedURLException e) { - return bad_request; - } + 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: - forbidden_time = System.nanoTime(); - forbidden_set = true; - return forbidden; + 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: + 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(); + 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) + if (offset != contentLength) + return failed; + + } catch (IOException e) { 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; + } - 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; + long duration_ms = (System.nanoTime() - startTime) / 1000000; + if (duration_ms < google_maps_ratelimit_ms) { + try { + Thread.sleep(google_maps_ratelimit_ms - duration_ms); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + return 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(); + static final int min_cache_size = 9; + static final int max_cache_size = 24; + + static int cache_size = min_cache_size; + + static AltosSiteMapImage[] images = new AltosSiteMapImage[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) { + AltosSiteMapImage[] new_images = new AltosSiteMapImage[new_size]; + + for (int i = 0; i < cache_size; i++) { + if (i < new_size) + new_images[i] = images[i]; + else + images[i].flush(); } + images = new_images; + cache_size = new_size; } + } - return success; + static long used; + + private static Point tile_loc(AltosSiteMapTile tile) { + Rectangle r = tile.getBounds(); + int x = r.x / 512; + int y = r.y / 512; + + return new Point (x, y); } - static final int cache_size = 12; + private static void dump_cache() { + int min_x = 1000, max_x = -1000, min_y = 1000, max_y = -1000; - static AltosCacheImage[] images; + for (int i = 0; i < cache_size; i++) { + AltosSiteMapImage image = images[i]; + if (image != null) { + Point p = tile_loc(image.tile); + min_x = min_x < p.x ? min_x : p.x; + max_x = max_x > p.x ? max_x : p.x; + min_y = min_y < p.y ? min_y : p.y; + max_y = max_y > p.y ? max_y : p.y; + System.out.printf ("entry %d %d,%d used %d\n", i, p.x, p.y, image.used); + } else { + System.out.printf ("entry %d empty\n", i); + } + } - static long used; + int[][] map = new int[max_x - min_x + 1][max_y - min_y + 1]; + for (int i = 0; i < cache_size; i++) { + AltosSiteMapImage image = images[i]; + if (image != null) { + Point p = tile_loc(image.tile); + map[p.x - min_x][p.y - min_y]++; + } + } - public static Image get_image(Component component, File file, int width, int height) { + for (int y = min_y; y <= max_y; y++) { + for (int x = min_x; x <= max_x; x++) + System.out.printf (" %2d", map[x - min_x][y - min_y]); + System.out.printf("\n"); + } + } + + public static AltosSiteMapImage get_image(AltosSiteMapTile tile, File file, int width, int height) { int oldest = -1; long age = used; - AltosCacheImage image; - if (images == null) - images = new AltosCacheImage[cache_size]; - for (int i = 0; i < cache_size; i++) { - image = images[i]; - if (image == null) { - oldest = i; - break; + synchronized(cache_lock) { + AltosSiteMapImage image = null; + for (int i = 0; i < cache_size; i++) { + image = images[i]; + + if (image == null) { + oldest = i; + break; + } + if (image.tile == tile && file.equals(image.file)) { + image.used = used++; + return image; + } + if (image.used < age) { + oldest = i; + age = image.used; + } } - if (image.component == component && file.equals(image.file)) { + + try { + image = new AltosSiteMapImage(tile, file, width, height); image.used = used++; - return image.validate(); - } - if (image.used < age) { - oldest = i; - age = image.used; + if (images[oldest] != null) { +// dump_cache(); + AltosSiteMap.debug_component(images[oldest].tile, "replacing cache"); + AltosSiteMap.debug_component(tile, "replaced cache"); + images[oldest].flush(); + } + images[oldest] = image; + return image; + } catch (IOException e) { + return null; } } - - try { - image = new AltosCacheImage(component, file, width, height); - image.used = used++; - if (images[oldest] != null) - images[oldest].flush(); - images[oldest] = image; - return image.validate(); - } catch (IOException e) { - return null; - } } } diff --git a/altosuilib/AltosSiteMapImage.java b/altosuilib/AltosSiteMapImage.java new file mode 100644 index 00000000..f08c0b26 --- /dev/null +++ b/altosuilib/AltosSiteMapImage.java @@ -0,0 +1,82 @@ +/* + * Copyright © 2014 Keith Packard + * + * 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.imageio.ImageIO; +import javax.swing.*; +import java.io.*; +import org.altusmetrum.altoslib_4.*; + +public class AltosSiteMapImage { + AltosSiteMapTile tile; + File file; + BufferedImage image; + int width; + int height; + long used; + + Thread load_thread; + + public boolean validate() { + if (image != null) { + AltosSiteMap.debug_component(tile, "valid"); + return true; + } else { + AltosSiteMap.debug_component(tile, "loading"); + load_thread = new Thread() { + public void run() { + image = null; + try { + image = ImageIO.read(file); + } catch (Exception e) { + } + SwingUtilities.invokeLater( new Runnable() { + public void run() { + AltosSiteMap.debug_component(tile, "later"); + Graphics2D g2d = (Graphics2D) tile.getGraphics(); + tile.paint_graphics(g2d, image); + load_thread = null; + } + }); + } + }; + load_thread.start(); + return false; + } + } + + public void flush() { + if (load_thread == null) { + AltosSiteMap.debug_component(tile, "flush"); + image.flush(); + image = null; + } + } + + public AltosSiteMapImage (AltosSiteMapTile tile, File file, int w, int h) throws IOException { + this.tile = tile; + this.file = file; + width = w; + height = h; + image = null; + used = 0; + } +} + diff --git a/altosuilib/AltosSiteMapTile.java b/altosuilib/AltosSiteMapTile.java index 09f184a3..136fbd7a 100644 --- a/altosuilib/AltosSiteMapTile.java +++ b/altosuilib/AltosSiteMapTile.java @@ -51,11 +51,15 @@ public class AltosSiteMapTile extends JComponent { LinkedList points; public synchronized void queue_repaint() { - SwingUtilities.invokeLater(new Runnable() { - public void run() { - repaint(); - } - }); + if (SwingUtilities.isEventDispatchThread()) + repaint(); + else { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + repaint(); + } + }); + } } public void load_map(File pngFile) { @@ -68,13 +72,14 @@ public class AltosSiteMapTile extends JComponent { public void set_font(Font font) { this.font = font; this.status = AltosSiteMapCache.success; - queue_repaint(); } public void set_status(int status) { - file = null; - this.status = status; - queue_repaint(); + if (status != this.status || file != null) { + file = null; + this.status = status; + queue_repaint(); + } } public void clearMap() { @@ -83,7 +88,6 @@ public class AltosSiteMapTile extends JComponent { points = null; file = null; status = AltosSiteMapCache.success; - queue_repaint(); line = null; } @@ -151,17 +155,14 @@ public class AltosSiteMapTile extends JComponent { return String.format(format, distance); } - public void paint(Graphics g) { - Graphics2D g2d = (Graphics2D) g; - AltosPoint prev = null; - Image img = null; + boolean painting; - if (file != null) - img = AltosSiteMapCache.get_image(this, file, px_size, px_size); - - if (img != null) { - g2d.drawImage(img, 0, 0, null); + public void paint_graphics(Graphics2D g2d, Image image) { + if (image != null) { + AltosSiteMap.debug_component(this, "paint_graphics"); + g2d.drawImage(image, 0, 0, null); } else { + AltosSiteMap.debug_component(this, "erase_graphics"); g2d.setColor(Color.GRAY); g2d.fillRect(0, 0, getWidth(), getHeight()); String message = null; @@ -199,6 +200,7 @@ public class AltosSiteMapTile extends JComponent { g2d.setStroke(new BasicStroke(6, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); if (points != null) { + AltosPoint prev = null; for (AltosPoint point : points) { if (prev != null) { if (0 <= point.state && point.state < stateColors.length) @@ -237,9 +239,37 @@ public class AltosSiteMapTile extends JComponent { } g2d.drawString(message, x, y); } + painting = false; + } + + public void paint(Graphics g) { + Graphics2D g2d = (Graphics2D) g; + Image image = null; + boolean queued = false; + + if (painting) { + AltosSiteMap.debug_component(this, "already painting"); + return; + } + AltosSiteMap.debug_component(this, "paint"); + + if (file != null) { + AltosSiteMapImage aimage; + + aimage = AltosSiteMapCache.get_image(this, file, px_size, px_size); + if (aimage != null) { + if (aimage.validate()) + image = aimage.image; + else + queued = true; + } + } + if (!queued) + paint_graphics(g2d, image); + painting = queued; } - public synchronized void show(int state, Point2D.Double last_pt, Point2D.Double pt) + public void show(int state, Point2D.Double last_pt, Point2D.Double pt) { if (points == null) points = new LinkedList(); diff --git a/altosuilib/Makefile.am b/altosuilib/Makefile.am index b7d624e2..10b756b8 100644 --- a/altosuilib/Makefile.am +++ b/altosuilib/Makefile.am @@ -37,6 +37,7 @@ altosuilib_JAVA = \ AltosSiteMapCache.java \ AltosSiteMapPreload.java \ AltosSiteMapTile.java \ + AltosSiteMapImage.java \ AltosVoice.java \ AltosDisplayThread.java \ AltosFreqList.java