package tim.prune.save; import tim.prune.data.DoubleRange; import tim.prune.data.Track; import tim.prune.data.TrackExtents; import tim.prune.gui.map.MapSource; import tim.prune.gui.map.MapTileManager; import tim.prune.gui.map.TileConsumer; import java.awt.Color; import java.awt.Graphics; import java.awt.Image; import java.awt.image.BufferedImage; /** * Class to handle the sticking together (grouting) of map tiles * to create a single map image for the current track */ public class MapGrouter implements TileConsumer { /** The most recently produced image */ private GroutedImage _lastGroutedImage = null; /** * Clear the last image, it's not needed any more */ public void clearMapImage() { _lastGroutedImage = null; } /** * Grout the required map tiles together according to the track's extent * @param inTrack track object * @param inMapSource map source to use (may have one or two layers) * @param inZoom selected zoom level * @return grouted image, or null if no image could be created */ public GroutedImage createMapImage(Track inTrack, MapSource inMapSource, int inZoom) { return createMapImage(inTrack, inMapSource, inZoom, false); } /** * Grout the required map tiles together according to the track's extent * @param inTrack track object * @param inMapSource map source to use (may have one or two layers) * @param inZoom selected zoom level * @param inDownload true to download tiles, false (by default) to just pull from disk * @return grouted image, or null if no image could be created */ public GroutedImage createMapImage(Track inTrack, MapSource inMapSource, int inZoom, boolean inDownload) { // Get the extents of the track including a standard (10%) border around the data TrackExtents extents = new TrackExtents(inTrack); extents.applySquareBorder(); DoubleRange xRange = extents.getXRange(); DoubleRange yRange = extents.getYRange(); // Work out which tiles are required final int zoomFactor = 1 << inZoom; final int minTileX = (int) (xRange.getMinimum() * zoomFactor); final int maxTileX = (int) (xRange.getMaximum() * zoomFactor); final int minTileY = (int) (yRange.getMinimum() * zoomFactor); final int maxTileY = (int) (yRange.getMaximum() * zoomFactor); // Work out how big the final image will be, create a BufferedImage final int pixCount = (int) (extents.getXRange().getRange() * zoomFactor * 256); if (pixCount < 2 || inZoom == 0) {return null;} BufferedImage resultImage = new BufferedImage(pixCount, pixCount, BufferedImage.TYPE_INT_RGB); Graphics g = resultImage.getGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, pixCount, pixCount); // Work out where to start drawing the tiles on the image int xOffset = (int) ((minTileX - xRange.getMinimum() * zoomFactor) * 256); // Make a map tile manager to load (or download) the tiles MapTileManager tileManager = new MapTileManager(this); tileManager.setMapSource(inMapSource); tileManager.enableTileDownloading(inDownload); tileManager.setReturnIncompleteImages(); tileManager.setZoom(inZoom); int numTilesUsed = 0; int numTilesMissing = 0; // Loop over the tiles for (int x = minTileX; x <= maxTileX; x++) { int yOffset = (int) ((minTileY - yRange.getMinimum() * zoomFactor) * 256); for (int y = minTileY; y <= maxTileY; y++) { for (int layer=0; layer < inMapSource.getNumLayers(); layer++) { Image tile = tileManager.getTile(layer, x, y, true); // If we're downloading tiles, wait until the tile isn't null int waitCount = 0; while (tile == null && inDownload && waitCount < 3) { // System.out.println("wait " + waitCount + " for tile to be not null"); try {Thread.sleep(250);} catch (InterruptedException e) {} tile = tileManager.getTile(layer, x, y, false); // don't request another download waitCount++; } // See if there's a tile or not if (tile != null) { // Wait until tile is available (loaded asynchronously) while (tile.getWidth(null) < 0 && !inDownload) { // System.out.println("Wait for tile width"); try { Thread.sleep(inDownload ? 500 : 100); } catch (InterruptedException ie) {} } // work out where to copy it to, paint it // System.out.println("Painting tile " + x + "," + y + " at " + xOffset + "," + yOffset); numTilesUsed++; g.drawImage(tile, xOffset, yOffset, null); } else { // null tile, that means it's either not available or really slow to start downloading numTilesMissing++; } } yOffset += 256; } xOffset += 256; } // Get rid of the image if it's empty if (numTilesUsed == 0) { resultImage = null; } // Store the xy limits in the GroutedImage to make it easier to draw on top GroutedImage result = new GroutedImage(resultImage, numTilesUsed, numTilesMissing); result.setXRange(xRange); result.setYRange(yRange); return result; } /** * Get the grouted map image, using the previously-created one if available * @param inTrack track object * @param inMapSource map source to use (may have one or two layers) * @param inZoom selected zoom level * @return grouted image, or null if no image could be created */ public synchronized GroutedImage getMapImage(Track inTrack, MapSource inMapSource, int inZoom) { if (_lastGroutedImage == null) { _lastGroutedImage = createMapImage(inTrack, inMapSource, inZoom); } return _lastGroutedImage; } /** * @param inTrack track object * @param inZoom selected zoom level * @return true if the image size is acceptable */ public static boolean isZoomLevelOk(Track inTrack, int inZoom) { // Get the extents of the track including a standard (10%) border around the data TrackExtents extents = new TrackExtents(inTrack); extents.applySquareBorder(); // Work out how big the final image will be final int zoomFactor = 1 << inZoom; final int pixCount = (int) (extents.getXRange().getRange() * zoomFactor * 256); return pixCount > 2 && pixCount < 4000; } /** React to tiles being updated by the tile manager */ public void tilesUpdated(boolean inIsOk) { // Doesn't need any action } }