diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorImageTile.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorImageTile.java new file mode 100644 index 000000000..444a568e4 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorImageTile.java @@ -0,0 +1,144 @@ +package gov.nasa.worldwind.layer.mercator; + +import android.graphics.Bitmap; + +import java.util.Collection; + +import gov.nasa.worldwind.render.ImageTile; +import gov.nasa.worldwind.util.DownloadPostprocessor; +import gov.nasa.worldwind.util.Level; +import gov.nasa.worldwind.util.LevelSet; +import gov.nasa.worldwind.util.Logger; +import gov.nasa.worldwind.util.Tile; +import gov.nasa.worldwind.util.TileFactory; + +class MercatorImageTile extends ImageTile implements DownloadPostprocessor { + + /** + * Constructs a tile with a specified sector, level, row and column. + * + * @param sector the sector spanned by the tile + * @param level the tile's level in a {@link LevelSet} + * @param row the tile's row within the specified level + * @param column the tile's column within the specified level + */ + MercatorImageTile(MercatorSector sector, Level level, int row, int column) { + super(sector, level, row, column); + } + + /** + * Creates all Mercator tiles for a specified level within a {@link LevelSet}. + * + * @param level the level to create the tiles for + * @param tileFactory the tile factory to use for creating tiles. + * @param result an pre-allocated Collection in which to store the results + */ + static void assembleMercatorTilesForLevel(Level level, TileFactory tileFactory, Collection result) { + if (level == null) { + throw new IllegalArgumentException( + Logger.logMessage(Logger.ERROR, "Tile", "assembleTilesForLevel", "missingLevel")); + } + + if (tileFactory == null) { + throw new IllegalArgumentException( + Logger.logMessage(Logger.ERROR, "Tile", "assembleTilesForLevel", "missingTileFactory")); + } + + if (result == null) { + throw new IllegalArgumentException( + Logger.logMessage(Logger.ERROR, "Tile", "assembleTilesForLevel", "missingResult")); + } + + // NOTE LevelSet.sector is final Sector attribute and thus can not be cast to MercatorSector! + MercatorSector sector = MercatorSector.fromSector(level.parent.sector); + double dLat = level.tileDelta / 2; + double dLon = level.tileDelta; + + int firstRow = Tile.computeRow(dLat, sector.minLatitude()); + int lastRow = Tile.computeLastRow(dLat, sector.maxLatitude()); + int firstCol = Tile.computeColumn(dLon, sector.minLongitude()); + int lastCol = Tile.computeLastColumn(dLon, sector.maxLongitude()); + + double deltaLat = dLat / 90; + double d1 = sector.minLatPercent() + deltaLat * firstRow; + for (int row = firstRow; row <= lastRow; row++) { + double d2 = d1 + deltaLat; + double t1 = sector.minLongitude() + (firstCol * dLon); + for (int col = firstCol; col <= lastCol; col++) { + double t2; + t2 = t1 + dLon; + result.add(tileFactory.createTile(MercatorSector.fromDegrees(d1, d2, t1, t2), level, row, col)); + t1 = t2; + } + d1 = d2; + } + } + + /** + * Returns the four children formed by subdividing this tile. This tile's sector is subdivided into four quadrants + * as follows: Southwest; Southeast; Northwest; Northeast. A new tile is then constructed for each quadrant and + * configured with the next level within this tile's LevelSet and its corresponding row and column within that + * level. This returns null if this tile's level is the last level within its {@link LevelSet}. + * + * @param tileFactory the tile factory to use to create the children + * + * @return an array containing the four child tiles, or null if this tile's level is the last level + */ + @Override + public Tile[] subdivide(TileFactory tileFactory) { + if (tileFactory == null) { + throw new IllegalArgumentException( + Logger.logMessage(Logger.ERROR, "Tile", "subdivide", "missingTileFactory")); + } + + Level childLevel = this.level.nextLevel(); + if (childLevel == null) { + return null; + } + + MercatorSector sector = (MercatorSector) this.sector; + + double d0 = sector.minLatPercent(); + double d2 = sector.maxLatPercent(); + double d1 = d0 + (d2 - d0) / 2.0; + + double t0 = sector.minLongitude(); + double t2 = sector.maxLongitude(); + double t1 = 0.5 * (t0 + t2); + + int northRow = 2 * this.row; + int southRow = northRow + 1; + int westCol = 2 * this.column; + int eastCol = westCol + 1; + + Tile[] children = new Tile[4]; + children[0] = tileFactory.createTile(MercatorSector.fromDegrees(d0, d1, t0, t1), childLevel, northRow, westCol); + children[1] = tileFactory.createTile(MercatorSector.fromDegrees(d0, d1, t1, t2), childLevel, northRow, eastCol); + children[2] = tileFactory.createTile(MercatorSector.fromDegrees(d1, d2, t0, t1), childLevel, southRow, westCol); + children[3] = tileFactory.createTile(MercatorSector.fromDegrees(d1, d2, t1, t2), childLevel, southRow, eastCol); + + return children; + } + + @Override + public Bitmap process(Bitmap resource) { + // Re-project mercator tile to equirectangular + int[] pixels = new int[resource.getWidth() * resource.getHeight()]; + int[] result = new int[resource.getWidth() * resource.getHeight()]; + resource.getPixels(pixels, 0, resource.getWidth(), 0, 0, resource.getWidth(), resource.getHeight()); + double miny = ((MercatorSector) sector).minLatPercent(); + double maxy = ((MercatorSector) sector).maxLatPercent(); + for (int y = 0; y < resource.getHeight(); y++) { + double sy = 1.0 - y / (double) (resource.getHeight() - 1); + double lat = sy * (sector.maxLatitude() - sector.minLatitude()) + sector.minLatitude(); + double dy = 1.0 - (MercatorSector.gudermannianInverse(lat) - miny) / (maxy - miny); + dy = Math.max(0.0, Math.min(1.0, dy)); + int iy = (int) (dy * (resource.getHeight() - 1)); + for (int x = 0; x < resource.getWidth(); x++) { + result[x + y * resource.getWidth()] = pixels[x + iy * resource.getWidth()]; + } + } + return Bitmap.createBitmap(result, resource.getWidth(), resource.getHeight(), resource.getConfig()); + } + +} \ No newline at end of file diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorSector.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorSector.java new file mode 100644 index 000000000..ac9541696 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorSector.java @@ -0,0 +1,47 @@ +package gov.nasa.worldwind.layer.mercator; + +import gov.nasa.worldwind.geom.Sector; + +public class MercatorSector extends Sector { + + private final double minLatPercent, maxLatPercent; + + private MercatorSector(double minLatPercent, double maxLatPercent, + double minLongitude, double maxLongitude) { + this.minLatPercent = minLatPercent; + this.maxLatPercent = maxLatPercent; + this.minLatitude = gudermannian(minLatPercent); + this.maxLatitude = gudermannian(maxLatPercent); + this.minLongitude = minLongitude; + this.maxLongitude = maxLongitude; + } + + public static MercatorSector fromDegrees(double minLatPercent, double maxLatPercent, + double minLongitude, double maxLongitude) { + return new MercatorSector(minLatPercent, maxLatPercent, minLongitude, maxLongitude); + } + + static MercatorSector fromSector(Sector sector) { + return new MercatorSector(gudermannianInverse(sector.minLatitude()), + gudermannianInverse(sector.maxLatitude()), + sector.minLongitude(), sector.maxLongitude()); + } + + static double gudermannianInverse(double latitude) { + return Math.log(Math.tan(Math.PI / 4.0 + Math.toRadians(latitude) / 2.0)) / Math.PI; + } + + private static double gudermannian(double percent) { + return Math.toDegrees(Math.atan(Math.sinh(percent * Math.PI))); + } + + double minLatPercent() { + return minLatPercent; + } + + double maxLatPercent() + { + return maxLatPercent; + } + +} \ No newline at end of file diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledImageLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledImageLayer.java new file mode 100644 index 000000000..8b364b7c6 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledImageLayer.java @@ -0,0 +1,44 @@ +package gov.nasa.worldwind.layer.mercator; + +import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Sector; +import gov.nasa.worldwind.layer.RenderableLayer; +import gov.nasa.worldwind.render.ImageOptions; +import gov.nasa.worldwind.render.ImageSource; +import gov.nasa.worldwind.util.Level; +import gov.nasa.worldwind.util.LevelSet; +import gov.nasa.worldwind.util.Tile; +import gov.nasa.worldwind.util.TileFactory; + +public abstract class MercatorTiledImageLayer extends RenderableLayer implements TileFactory { + + private static final double FULL_SPHERE = 360; + + private final int firstLevelOffset; + + public MercatorTiledImageLayer(String name, int numLevels, int firstLevelOffset, int tileSize, boolean overlay) { + super(name); + this.setPickEnabled(false); + this.firstLevelOffset = firstLevelOffset; + + MercatorTiledSurfaceImage surfaceImage = new MercatorTiledSurfaceImage(); + surfaceImage.setLevelSet(new LevelSet( + MercatorSector.fromDegrees(-1.0, 1.0, - FULL_SPHERE / 2, FULL_SPHERE / 2), + FULL_SPHERE / (1 << firstLevelOffset), numLevels - firstLevelOffset, tileSize, tileSize)); + surfaceImage.setTileFactory(this); + if(!overlay) { + surfaceImage.setImageOptions(new ImageOptions(WorldWind.RGB_565)); // reduce memory usage by using a 16-bit configuration with no alpha + } + this.addRenderable(surfaceImage); + } + + @Override + public Tile createTile(Sector sector, Level level, int row, int column) { + MercatorImageTile tile = new MercatorImageTile((MercatorSector) sector, level, row, column); + tile.setImageSource(ImageSource.fromUrl(getImageSourceUrl(column, (1 << (level.levelNumber + firstLevelOffset)) - 1 - row, level.levelNumber + firstLevelOffset), tile)); + return tile; + } + + protected abstract String getImageSourceUrl(int x, int y, int z); + +} \ No newline at end of file diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledSurfaceImage.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledSurfaceImage.java new file mode 100644 index 000000000..6cb7de676 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledSurfaceImage.java @@ -0,0 +1,16 @@ +package gov.nasa.worldwind.layer.mercator; + +import gov.nasa.worldwind.shape.TiledSurfaceImage; +import gov.nasa.worldwind.util.Level; + +public class MercatorTiledSurfaceImage extends TiledSurfaceImage { + + @Override + protected void createTopLevelTiles() { + Level firstLevel = this.levelSet.firstLevel(); + if (firstLevel != null) { + MercatorImageTile.assembleMercatorTilesForLevel(firstLevel, this.tileFactory, this.topLevelTiles); + } + } + +} \ No newline at end of file diff --git a/worldwind/src/main/java/gov/nasa/worldwind/render/ImageRetriever.java b/worldwind/src/main/java/gov/nasa/worldwind/render/ImageRetriever.java index c760dbd09..cd937bbbe 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/render/ImageRetriever.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/render/ImageRetriever.java @@ -16,6 +16,7 @@ import java.net.URLConnection; import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.util.DownloadPostprocessor; import gov.nasa.worldwind.util.Logger; import gov.nasa.worldwind.util.Retriever; import gov.nasa.worldwind.util.WWUtil; @@ -72,7 +73,7 @@ protected Bitmap decodeImage(ImageSource imageSource, ImageOptions imageOptions) } if (imageSource.isUrl()) { - return this.decodeUrl(imageSource.asUrl(), imageOptions); + return this.decodeUrl(imageSource.asUrl(), imageOptions, imageSource.postprocessor); } return this.decodeUnrecognized(imageSource); @@ -88,7 +89,7 @@ protected Bitmap decodeFilePath(String pathName, ImageOptions imageOptions) { return BitmapFactory.decodeFile(pathName, factoryOptions); } - protected Bitmap decodeUrl(String urlString, ImageOptions imageOptions) throws IOException { + protected Bitmap decodeUrl(String urlString, ImageOptions imageOptions, DownloadPostprocessor postprocessor) throws IOException { // TODO establish a file caching service for remote resources // TODO retry absent resources, they are currently handled but suppressed entirely after the first failure // TODO configurable connect and read timeouts @@ -102,7 +103,14 @@ protected Bitmap decodeUrl(String urlString, ImageOptions imageOptions) throws I stream = new BufferedInputStream(conn.getInputStream()); BitmapFactory.Options factoryOptions = this.bitmapFactoryOptions(imageOptions); - return BitmapFactory.decodeStream(stream, null, factoryOptions); + Bitmap bitmap = BitmapFactory.decodeStream(stream, null, factoryOptions); + + // Apply bitmap transformation if required + if (postprocessor != null && bitmap != null) { + bitmap = postprocessor.process(bitmap); + } + + return bitmap; } finally { WWUtil.closeSilently(stream); } diff --git a/worldwind/src/main/java/gov/nasa/worldwind/render/ImageSource.java b/worldwind/src/main/java/gov/nasa/worldwind/render/ImageSource.java index 5d65dd852..70954daf0 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/render/ImageSource.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/render/ImageSource.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.HashMap; +import gov.nasa.worldwind.util.DownloadPostprocessor; import gov.nasa.worldwind.util.Logger; import gov.nasa.worldwind.util.WWUtil; @@ -68,6 +69,8 @@ public interface BitmapFactory { protected Object source; + protected DownloadPostprocessor postprocessor; + protected ImageSource() { } @@ -173,6 +176,23 @@ public static ImageSource fromUrl(String urlString) { return imageSource; } + /** + * Constructs an image source with a URL string. The image's dimensions should be no greater than 2048 x 2048. The + * application's manifest must include the permissions that allow network connections. + * + * @param urlString complete URL string + * @param postprocessor implementation of image post-transformation routine + * + * @return the new image source + * + * @throws IllegalArgumentException If the URL string is null + */ + public static ImageSource fromUrl(String urlString, DownloadPostprocessor postprocessor) { + ImageSource imageSource = fromUrl(urlString); + imageSource.postprocessor = postprocessor; + return imageSource; + } + /** * Constructs a bitmap image source with a line stipple pattern. The result is a one-dimensional bitmap with pixels * representing the specified stipple factor and stipple pattern. Line stipple images can be used for displaying diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/DownloadPostprocessor.java b/worldwind/src/main/java/gov/nasa/worldwind/util/DownloadPostprocessor.java new file mode 100644 index 000000000..80c6ed2a3 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/DownloadPostprocessor.java @@ -0,0 +1,14 @@ +package gov.nasa.worldwind.util; + +/** + * Interface for resource download post-processing + */ +public interface DownloadPostprocessor { + /** + * Process resource according to specified algorithm implementation + * + * @param resource original resource + * @return processed resource + */ + T process(T resource); +}