Skip to content

Commit

Permalink
Add MercatorTiledImageLayer support.
Browse files Browse the repository at this point in the history
  • Loading branch information
ComBatVision committed Jul 20, 2022
1 parent 0dacfec commit 33cfdbe
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package gov.nasa.worldwind.layer.mercator;

import android.graphics.Bitmap;

import java.util.Collection;

import gov.nasa.worldwind.render.DownloadPostprocessor;
import gov.nasa.worldwind.render.ImageTile;
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<Bitmap> {

/**
* 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<Tile> 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());
}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
@@ -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);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package gov.nasa.worldwind.render;

/**
* Interface for resource download post-processing
*/
public interface DownloadPostprocessor<T> {
/**
* Process resource according to specified algorithm implementation
*
* @param resource original resource
* @return processed resource
*/
T process(T resource);
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,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);
Expand All @@ -88,7 +88,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<Bitmap> 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
Expand All @@ -102,7 +102,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);
}
Expand Down
19 changes: 19 additions & 0 deletions worldwind/src/main/java/gov/nasa/worldwind/render/ImageSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public interface BitmapFactory {

protected Object source;

protected DownloadPostprocessor<Bitmap> postprocessor;

protected ImageSource() {
}

Expand Down Expand Up @@ -173,6 +175,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<Bitmap> 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
Expand Down

0 comments on commit 33cfdbe

Please sign in to comment.