WorldWindEarth / WorldWindAndroid

The WorldWind Android "Community Edition" SDK (WWA-CE) includes the library, examples and tutorials for building 3D virtual globe applications for phones and tablets.
https://worldwind.earth/WorldWindAndroid/
Other
15 stars 7 forks source link

Add MercatorTiledImageLayer support #12

Closed ComBatVision closed 4 years ago

ComBatVision commented 5 years ago

I have ported and optimized MercatorTiledImageLayer from WorldWindJava.

It requires a MercatorSector:

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;
    }

}

MercatorImageTile:

class MercatorImageTile extends ImageTile implements CustomImageSource.Transformer {

    /**
     * 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
     * @throws IllegalArgumentException if either the sector or the level is null
     */
    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
     *
     * @throws IllegalArgumentException If any argument is null
     */
    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
     *
     * @throws IllegalArgumentException If the tile factory is null
     */
    @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 transform(Bitmap bitmap) {
        // Re-project mercator tile to equirectangular
        Bitmap trans = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());
        double miny = ((MercatorSector) sector).minLatPercent();
        double maxy = ((MercatorSector) sector).maxLatPercent();
        for (int y = 0; y < bitmap.getHeight(); y++) {
            double sy = 1.0 - y / (double) (bitmap.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 * (bitmap.getHeight() - 1));
            for (int x = 0; x < bitmap.getWidth(); x++) {
                trans.setPixel(x, y, bitmap.getPixel(x, iy));
            }
        }
        return trans;
    }

}

MercatorTiledSurfaceImage:

public class MercatorTiledSurfaceImage extends TiledSurfaceImage {

    @Override
    protected void createTopLevelTiles() {
        Level firstLevel = this.levelSet.firstLevel();
        if (firstLevel != null) {
            MercatorImageTile.assembleMercatorTilesForLevel(firstLevel, this.tileFactory, this.topLevelTiles);
        }
    }

}

MercatorTiledImageLayer:

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(CustomImageSource.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);
}

And modified ImageSource, ImageRetriever and RenderResourceCache:

public class CustomImageSource extends ImageSource {

    public interface Transformer {
        Bitmap transform(Bitmap bitmap);
    }

    private Transformer transformer;

    public static CustomImageSource fromUrl(String urlString, Transformer transformer) {
        if (urlString == null) {
            throw new IllegalArgumentException(
                    Logger.logMessage(Logger.ERROR, CustomImageSource.class.getSimpleName(), "fromUrl", "missingUrl"));
        }

        CustomImageSource imageSource = new CustomImageSource();
        imageSource.type = TYPE_URL;
        imageSource.source = urlString;
        imageSource.transformer = transformer;
        return imageSource;
    }

    Transformer getTransformer() {
        return transformer;
    }

}
public class CustomImageRetriever extends ImageRetriever {

    CustomImageRetriever(int maxSimultaneousRetrievals) {
        super(maxSimultaneousRetrievals);
    }

    @Override
    protected Bitmap decodeImage(ImageSource imageSource, ImageOptions imageOptions) throws IOException {
        Bitmap bitmap = super.decodeImage(imageSource, imageOptions);
        // Apply bitmap transformation if required
        if(imageSource instanceof CustomImageSource) {
            CustomImageSource.Transformer transformer = ((CustomImageSource) imageSource).getTransformer();
            if (transformer != null) {
                bitmap = transformer.transform(bitmap);
            }
        }
        return bitmap;
    }

}
public class CustomRenderResourceCache extends RenderResourceCache {

    public CustomRenderResourceCache(int capacity) {
        super(capacity);
    }

    @Override
    protected void init() {
        super.init();
        // Use custom image retriever to support bitmap post processing
        this.urlImageRetriever = new CustomImageRetriever(8);
    }

}
ComBatVision commented 5 years ago
public class OSMLayer extends MercatorTiledImageLayer {

    public static final String NAME = "OpenStreetMap";

    private final Random random = new Random();

    public OSMLayer() {
        super(NAME, 19, 3,256, false);
    }

    @Override
    protected String getImageSourceUrl(int x, int y, int z) {
        char abc = "abc".charAt(random.nextInt(2));
        return  "https://"+abc+".tile.openstreetmap.org/"+z+"/"+x+"/"+y+".png";
    }

}
public class OTMLayer extends MercatorTiledImageLayer {

    public static final String NAME = "OpenTopoMap";

    private final Random random = new Random();

    public OTMLayer() {
        super(NAME, 17,3, 256, false);
    }

    @Override
    protected String getImageSourceUrl(int x, int y, int z) {
        char abc = "abc".charAt(random.nextInt(2));
        return  "https://"+abc+".tile.opentopomap.org/"+z+"/"+x+"/"+y+".png";
    }

}
public class WikiLayer extends MercatorTiledImageLayer {

    public enum Type {MAP, HYBRID}

    public static final String NAME = "Wiki";

    private final Type type;

    public WikiLayer() {
        this(Type.HYBRID);
    }

    public WikiLayer(Type type) {
        super(NAME + type.name().toLowerCase(), 19, 3, 256, Type.HYBRID.equals(type));
        this.type = type;
    }

    @Override
    protected String getImageSourceUrl(int x, int y, int z) {
        int i = x % 4 + (y % 4) * 4;
        return "http://i"+i+".wikimapia.org/?lng=1&x="+x+"&y="+y+"&zoom="+z+"&type="+ type.name().toLowerCase();
    }

}
public class GoogleLayer extends MercatorTiledImageLayer {

    public enum Type {
        ROADMAP("Google road map", "m", false),
        //ROADMAP2("Google road map 2", "r", false),
        TERRAIN("Google map w/ terrain", "p", false),
        //TERRAIN_ONLY("Google terrain only", "t", false),
        //HYBRID("Google hybrid", "y", false),
        SATELLITE("Google satellite", "s", false),
        ROADS("Google roads", "h", true),
        TRAFFIC("Google traffic", "h,traffic&style=15", true),
        ;

        Type(String name, String lyrs, boolean overlay) {
            this.name = name;
            this.lyrs = lyrs;
            this.overlay = overlay;
        }

        public String getName() {
            return name;
        }

        private final String name;
        private final String lyrs;
        private final boolean overlay;

    }

    public GoogleLayer(Type type) {
        super(type.name, 22, 3,256, type.overlay);
        this.lyrs = type.lyrs;
    }

    @Override
    protected String getImageSourceUrl(int x, int y, int z) {
        return "https://mt.google.com/vt/lyrs="+lyrs+"&x="+x+"&y="+y+"&z="+z+"&hl="+Locale.getDefault().getLanguage();
    }

    private final String lyrs;

}