From 8f023bb5488afb0687e92c57cd4069bb33ec7704 Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Mon, 7 Dec 2020 16:22:37 +0200 Subject: [PATCH 01/10] Add MercatorTiledImageLayer support. --- .../layer/mercator/MercatorImageTile.java | 142 ++++++++++++++++++ .../layer/mercator/MercatorSector.java | 47 ++++++ .../mercator/MercatorTiledImageLayer.java | 44 ++++++ .../mercator/MercatorTiledSurfaceImage.java | 16 ++ .../nasa/worldwind/render/ImageRetriever.java | 13 +- .../nasa/worldwind/render/ImageSource.java | 31 ++++ 6 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorImageTile.java create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorSector.java create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledImageLayer.java create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledSurfaceImage.java 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..09d45a213 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorImageTile.java @@ -0,0 +1,142 @@ +package gov.nasa.worldwind.layer.mercator; + +import android.graphics.Bitmap; + +import java.util.Collection; + +import gov.nasa.worldwind.render.ImageSource; +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 ImageSource.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 + */ + 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 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; + } + +} \ 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..f7bffaf63 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/render/ImageRetriever.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/render/ImageRetriever.java @@ -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.transformer); } return this.decodeUnrecognized(imageSource); @@ -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, ImageSource.Transformer transformer) 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 +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 (transformer != null && bitmap != null) { + bitmap = transformer.transform(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..7e2d93892 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/render/ImageSource.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/render/ImageSource.java @@ -50,6 +50,18 @@ public interface BitmapFactory { Bitmap createBitmap(); } + /** + * Interface for remote image source post-transformation + */ + public interface Transformer { + /** + * Transforms image according to specified algorithm implementation + * @param bitmap original bitmap + * @return transformed bitmap + */ + Bitmap transform(Bitmap bitmap); + } + protected static final HashMap lineStippleFactories = new HashMap<>(); protected static final int TYPE_UNRECOGNIZED = 0; @@ -68,6 +80,8 @@ public interface BitmapFactory { protected Object source; + protected Transformer transformer; + protected ImageSource() { } @@ -173,6 +187,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 transformer 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, Transformer transformer) { + ImageSource imageSource = fromUrl(urlString); + imageSource.transformer = transformer; + 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 From d9b5aba45259cf0483e9c38f69fc4caf4def7580 Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Mon, 7 Dec 2020 18:52:38 +0200 Subject: [PATCH 02/10] Add tile origin attribute to level set. --- .../worldwind/globe/BasicTessellator.java | 3 ++- .../nasa/worldwind/layer/LayerFactory.java | 3 ++- .../mercator/MercatorTiledImageLayer.java | 3 ++- .../gov/nasa/worldwind/util/LevelSet.java | 24 ++++++++++++++++--- .../nasa/worldwind/util/LevelSetConfig.java | 6 +++++ .../worldwind/globe/BasicTerrainTest.java | 3 ++- 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/worldwind/src/main/java/gov/nasa/worldwind/globe/BasicTessellator.java b/worldwind/src/main/java/gov/nasa/worldwind/globe/BasicTessellator.java index 86f55baed..7df48be31 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/globe/BasicTessellator.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/globe/BasicTessellator.java @@ -16,6 +16,7 @@ import java.util.List; import gov.nasa.worldwind.draw.BasicDrawableTerrain; +import gov.nasa.worldwind.geom.Location; import gov.nasa.worldwind.geom.Range; import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.geom.Vec3; @@ -32,7 +33,7 @@ public class BasicTessellator implements Tessellator, TileFactory { // ~0.6 meter resolution - protected LevelSet levelSet = new LevelSet(new Sector().setFullSphere(), 90, 20, 32, 32); + protected LevelSet levelSet = new LevelSet(new Sector().setFullSphere(), new Location(-90, -180), 90, 20, 32, 32); protected double detailControl = 80; diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java index 32e8efd2b..046fd76ef 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java @@ -22,6 +22,7 @@ import java.util.concurrent.RejectedExecutionException; import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Location; import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.ogc.WmsLayerConfig; import gov.nasa.worldwind.ogc.WmsTileFactory; @@ -670,7 +671,7 @@ protected LevelSet createWmtsLevelSet(WmtsLayer wmtsLayer, CompatibleTileMatrixS } int imageSize = tileMatrixSet.getTileMatrices().get(0).getTileHeight(); - return new LevelSet(boundingBox, 90.0, compatibleTileMatrixSet.tileMatrices.size(), imageSize, imageSize); + return new LevelSet(boundingBox, new Location(-90, -180), 90, compatibleTileMatrixSet.tileMatrices.size(), imageSize, imageSize); } protected String buildWmtsKvpTemplate(String kvpServiceAddress, String layer, String format, String styleIdentifier, String tileMatrixSet) { 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 index 8b364b7c6..cfb33e7a6 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledImageLayer.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledImageLayer.java @@ -1,6 +1,7 @@ package gov.nasa.worldwind.layer.mercator; import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Location; import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.layer.RenderableLayer; import gov.nasa.worldwind.render.ImageOptions; @@ -23,7 +24,7 @@ public MercatorTiledImageLayer(String name, int numLevels, int firstLevelOffset, MercatorTiledSurfaceImage surfaceImage = new MercatorTiledSurfaceImage(); surfaceImage.setLevelSet(new LevelSet( - MercatorSector.fromDegrees(-1.0, 1.0, - FULL_SPHERE / 2, FULL_SPHERE / 2), + MercatorSector.fromDegrees(-1.0, 1.0, - FULL_SPHERE / 2, FULL_SPHERE / 2), new Location(-90, -180), FULL_SPHERE / (1 << firstLevelOffset), numLevels - firstLevelOffset, tileSize, tileSize)); surfaceImage.setTileFactory(this); if(!overlay) { diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java index f619fed57..e94fdfdf7 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java @@ -5,6 +5,7 @@ package gov.nasa.worldwind.util; +import gov.nasa.worldwind.geom.Location; import gov.nasa.worldwind.geom.Sector; /** @@ -18,6 +19,11 @@ public class LevelSet { */ public final Sector sector = new Sector(); + /** + * Tile origin for this level set + */ + public final Location tileOrigin = new Location(); + /** * The geographic width and height in degrees of tiles in the first level (lowest resolution) of this level set. */ @@ -55,6 +61,7 @@ public LevelSet() { * Constructs a level set with specified parameters. * * @param sector the sector spanned by this level set + * @param tileOrigin the origin for this level set * @param firstLevelDelta the geographic width and height in degrees of tiles in the first level (lowest resolution) * of the level set * @param numLevels the number of levels in the level set @@ -67,12 +74,17 @@ public LevelSet() { * * @throws IllegalArgumentException If any argument is null, or if any dimension is zero */ - public LevelSet(Sector sector, double firstLevelDelta, int numLevels, int tileWidth, int tileHeight) { + public LevelSet(Sector sector, Location tileOrigin, double firstLevelDelta, int numLevels, int tileWidth, int tileHeight) { if (sector == null) { throw new IllegalArgumentException( Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "missingSector")); } + if (tileOrigin == null) { + throw new IllegalArgumentException( + Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "missingTileOrigin")); + } + if (firstLevelDelta <= 0) { throw new IllegalArgumentException( Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "invalidTileDelta")); @@ -89,6 +101,7 @@ public LevelSet(Sector sector, double firstLevelDelta, int numLevels, int tileWi } this.sector.set(sector); + this.tileOrigin.set(tileOrigin); this.firstLevelDelta = firstLevelDelta; this.tileWidth = tileWidth; this.tileHeight = tileHeight; @@ -116,6 +129,11 @@ public LevelSet(LevelSetConfig config) { Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "missingSector")); } + if (config.tileOrigin == null) { + throw new IllegalArgumentException( + Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "missingTileOrigin")); + } + if (config.firstLevelDelta <= 0) { throw new IllegalArgumentException( Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "invalidTileDelta")); @@ -132,6 +150,7 @@ public LevelSet(LevelSetConfig config) { } this.sector.set(config.sector); + this.tileOrigin.set(config.tileOrigin); this.firstLevelDelta = config.firstLevelDelta; this.tileWidth = config.tileWidth; this.tileHeight = config.tileHeight; @@ -141,8 +160,7 @@ public LevelSet(LevelSetConfig config) { protected void assembleLevels() { for (int i = 0, len = this.levels.length; i < len; i++) { - double n = Math.pow(2, i); - double delta = firstLevelDelta / n; + double delta = firstLevelDelta / (1 << i); this.levels[i] = new Level(this, i, delta); } } diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java index 4846bc1ed..f8876bf6a 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java @@ -5,6 +5,7 @@ package gov.nasa.worldwind.util; +import gov.nasa.worldwind.geom.Location; import gov.nasa.worldwind.geom.Sector; /** @@ -18,6 +19,11 @@ public class LevelSetConfig { */ public final Sector sector = new Sector().setFullSphere(); + /** + * Tile origin for level set + */ + public final Location tileOrigin = new Location(-90, -180); + /** * The geographic width and height in degrees of tiles in the first level (lowest resolution) of the level set. */ diff --git a/worldwind/src/test/java/gov/nasa/worldwind/globe/BasicTerrainTest.java b/worldwind/src/test/java/gov/nasa/worldwind/globe/BasicTerrainTest.java index 9822a1723..2eebe1267 100644 --- a/worldwind/src/test/java/gov/nasa/worldwind/globe/BasicTerrainTest.java +++ b/worldwind/src/test/java/gov/nasa/worldwind/globe/BasicTerrainTest.java @@ -14,6 +14,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import gov.nasa.worldwind.WorldWind; +import gov.nasa.worldwind.geom.Location; import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.geom.Vec3; import gov.nasa.worldwind.util.LevelSet; @@ -81,7 +82,7 @@ public void setUp() { this.terrain = new BasicTerrain(); // Add a terrain tile used to the mocked terrain - LevelSet levelSet = new LevelSet(new Sector().setFullSphere(), 1.0, 1, 5, 5); // tiles with 5x5 vertices + LevelSet levelSet = new LevelSet(new Sector().setFullSphere(), new Location(-90, -180), 1.0, 1, 5, 5); // tiles with 5x5 vertices TerrainTile tile = new TerrainTile(new Sector(0, 0, 1, 1), levelSet.firstLevel(), 90, 180); ((BasicTerrain) this.terrain).addTile(tile); From 46989e26f97f53c8b2aab4e6cbd6883d1316701f Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Mon, 7 Dec 2020 18:52:51 +0200 Subject: [PATCH 03/10] Enhance tile row and column calculation routines with tile origin parameter. --- .../layer/mercator/MercatorImageTile.java | 12 +++--- .../java/gov/nasa/worldwind/util/Tile.java | 42 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) 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 index 09d45a213..03eccaa62 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorImageTile.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorImageTile.java @@ -4,6 +4,7 @@ import java.util.Collection; +import gov.nasa.worldwind.geom.Location; import gov.nasa.worldwind.render.ImageSource; import gov.nasa.worldwind.render.ImageTile; import gov.nasa.worldwind.util.Level; @@ -51,19 +52,20 @@ static void assembleMercatorTilesForLevel(Level level, TileFactory tileFactory, // NOTE LevelSet.sector is final Sector attribute and thus can not be cast to MercatorSector! MercatorSector sector = MercatorSector.fromSector(level.parent.sector); + Location tileOrigin = level.parent.tileOrigin; 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()); + int firstRow = Tile.computeRow(dLat, sector.minLatitude(), tileOrigin.latitude); + int lastRow = Tile.computeLastRow(dLat, sector.maxLatitude(), tileOrigin.latitude); + int firstCol = Tile.computeColumn(dLon, sector.minLongitude(), tileOrigin.longitude); + int lastCol = Tile.computeLastColumn(dLon, sector.maxLongitude(), tileOrigin.longitude); 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); + double t1 = tileOrigin.longitude + (firstCol * dLon); for (int col = firstCol; col <= lastCol; col++) { double t2; t2 = t1 + dLon; diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/Tile.java b/worldwind/src/main/java/gov/nasa/worldwind/util/Tile.java index 6abd8d49c..319ea0c9e 100755 --- a/worldwind/src/main/java/gov/nasa/worldwind/util/Tile.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/Tile.java @@ -13,6 +13,7 @@ import gov.nasa.worldwind.WorldWind; import gov.nasa.worldwind.geom.BoundingBox; import gov.nasa.worldwind.geom.Frustum; +import gov.nasa.worldwind.geom.Location; import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.geom.Vec3; import gov.nasa.worldwind.render.RenderContext; @@ -116,13 +117,14 @@ public Tile(Sector sector, Level level, int row, int column) { * * @param tileDelta the level's tile delta in degrees * @param latitude the tile's minimum latitude in degrees + * @param origin the origin of the grid * * @return the computed row number */ - public static int computeRow(double tileDelta, double latitude) { - int row = (int) Math.floor((latitude + 90) / tileDelta); + public static int computeRow(double tileDelta, double latitude, double origin) { + int row = (int) Math.floor((latitude - origin) / tileDelta); - if (latitude == 90) { + if (latitude - origin == 180) { row -= 1; // if latitude is at the end of the grid, subtract 1 from the computed row to return the last row } @@ -134,13 +136,14 @@ public static int computeRow(double tileDelta, double latitude) { * * @param tileDelta the level's tile delta in degrees * @param longitude the tile's minimum longitude in degrees + * @param origin the origin of the grid * * @return The computed column number */ - public static int computeColumn(double tileDelta, double longitude) { - int col = (int) Math.floor((longitude + 180) / tileDelta); + public static int computeColumn(double tileDelta, double longitude, double origin) { + int col = (int) Math.floor((longitude - origin) / tileDelta); - if (longitude == 180) { + if (longitude - origin == 360) { col -= 1; // if longitude is at the end of the grid, subtract 1 from the computed column to return the last column } @@ -152,13 +155,14 @@ public static int computeColumn(double tileDelta, double longitude) { * * @param tileDelta the level's tile delta in degrees * @param maxLatitude the tile's maximum latitude in degrees + * @param origin the origin of the grid * * @return the computed row number */ - public static int computeLastRow(double tileDelta, double maxLatitude) { - int row = (int) Math.ceil((maxLatitude + 90) / tileDelta - 1); + public static int computeLastRow(double tileDelta, double maxLatitude, double origin) { + int row = (int) Math.ceil((maxLatitude - origin) / tileDelta - 1); - if (maxLatitude + 90 < tileDelta) { + if (maxLatitude - origin < tileDelta) { row = 0; // if max latitude is in the first row, set the max row to 0 } @@ -170,13 +174,14 @@ public static int computeLastRow(double tileDelta, double maxLatitude) { * * @param tileDelta the level's tile delta in degrees * @param maxLongitude the tile's maximum longitude in degrees + * @param origin the origin of the grid * * @return The computed column number */ - public static int computeLastColumn(double tileDelta, double maxLongitude) { - int col = (int) Math.ceil((maxLongitude + 180) / tileDelta - 1); + public static int computeLastColumn(double tileDelta, double maxLongitude, double origin) { + int col = (int) Math.ceil((maxLongitude - origin) / tileDelta - 1); - if (maxLongitude + 180 < tileDelta) { + if (maxLongitude - origin < tileDelta) { col = 0; // if max longitude is in the first column, set the max column to 0 } @@ -211,15 +216,16 @@ public static Collection assembleTilesForLevel(Level level, TileFactory ti } Sector sector = level.parent.sector; + Location tileOrigin = level.parent.tileOrigin; double tileDelta = level.tileDelta; - int firstRow = Tile.computeRow(tileDelta, sector.minLatitude()); - int lastRow = Tile.computeLastRow(tileDelta, sector.maxLatitude()); - int firstCol = Tile.computeColumn(tileDelta, sector.minLongitude()); - int lastCol = Tile.computeLastColumn(tileDelta, sector.maxLongitude()); + int firstRow = Tile.computeRow(tileDelta, sector.minLatitude(), tileOrigin.latitude); + int lastRow = Tile.computeLastRow(tileDelta, sector.maxLatitude(), tileOrigin.latitude); + int firstCol = Tile.computeColumn(tileDelta, sector.minLongitude(), tileOrigin.longitude); + int lastCol = Tile.computeLastColumn(tileDelta, sector.maxLongitude(), tileOrigin.longitude); - double firstRowLat = -90 + firstRow * tileDelta; - double firstRowLon = -180 + firstCol * tileDelta; + double firstRowLat = tileOrigin.latitude + firstRow * tileDelta; + double firstRowLon = tileOrigin.longitude + firstCol * tileDelta; double lat = firstRowLat; double lon; From 88e6f0e996c4bb445a39aa72011696983e989944 Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Mon, 7 Dec 2020 18:53:01 +0200 Subject: [PATCH 04/10] Fix level width and height calculation in case of non-full sphere level set sector used. --- worldwind/src/main/java/gov/nasa/worldwind/util/Level.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/Level.java b/worldwind/src/main/java/gov/nasa/worldwind/util/Level.java index aaf79fe4e..911bb836b 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/util/Level.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/Level.java @@ -67,8 +67,8 @@ public Level(LevelSet parent, int levelNumber, double tileDelta) { this.parent = parent; this.levelNumber = levelNumber; - this.levelWidth = (int) Math.round(parent.tileWidth * 360 / tileDelta); - this.levelHeight = (int) Math.round(parent.tileHeight * 180 / tileDelta); + this.levelWidth = (int) Math.round(parent.tileWidth * parent.sector.deltaLongitude() / tileDelta); + this.levelHeight = (int) Math.round(parent.tileHeight * parent.sector.deltaLatitude() / tileDelta); this.tileDelta = tileDelta; this.tileWidth = parent.tileWidth; this.tileHeight = parent.tileHeight; From 99c51c0a3bb721d594210f481bd624b8a60685d3 Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Mon, 7 Dec 2020 18:53:41 +0200 Subject: [PATCH 05/10] Improve GeoPackage support implementation: - Add support of local tile origin based on tile matrix set extent instead of full sphere origin. - Add surface image name based on content identifier. - Change level set sector initialization based on tile matrix set extent instead of content extent. - Change first level tile delta and number of levels calculation based on tile matrix zoom levels. - Fix row calculation in tile factory. --- .../nasa/worldwind/layer/LayerFactory.java | 22 ++++++++++---- .../worldwind/ogc/gpkg/GpkgTileFactory.java | 29 ++++++++++--------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java index 046fd76ef..64908bee0 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java @@ -8,6 +8,7 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.util.SparseArray; import java.io.BufferedInputStream; import java.io.InputStream; @@ -30,6 +31,7 @@ import gov.nasa.worldwind.ogc.gpkg.GpkgContent; import gov.nasa.worldwind.ogc.gpkg.GpkgSpatialReferenceSystem; import gov.nasa.worldwind.ogc.gpkg.GpkgTileFactory; +import gov.nasa.worldwind.ogc.gpkg.GpkgTileMatrix; import gov.nasa.worldwind.ogc.gpkg.GpkgTileMatrixSet; import gov.nasa.worldwind.ogc.gpkg.GpkgTileUserMetrics; import gov.nasa.worldwind.ogc.wms.WmsCapabilities; @@ -259,6 +261,13 @@ protected void createFromGeoPackageAsync(String pathName, Layer layer, Callback continue; } + SparseArray tileMatrix = geoPackage.getTileMatrix(content.getTableName()); + if (tileMatrix == null || tileMatrix.size() == 0) { + Logger.logMessage(Logger.WARN, "LayerFactory", "createFromGeoPackageAsync", + "Unsupported GeoPackage tile matrix"); + continue; + } + GpkgTileUserMetrics tileMetrics = geoPackage.getTileUserMetrics(content.getTableName()); if (tileMetrics == null) { Logger.logMessage(Logger.WARN, "LayerFactory", "createFromGeoPackageAsync", @@ -267,14 +276,15 @@ protected void createFromGeoPackageAsync(String pathName, Layer layer, Callback } LevelSetConfig config = new LevelSetConfig(); - config.sector.set(content.getMinY(), content.getMinX(), - content.getMaxY() - content.getMinY(), content.getMaxX() - content.getMinX()); - config.firstLevelDelta = 180; - config.numLevels = tileMetrics.getMaxZoomLevel() + 1; // zero when there are no zoom levels, (0 = -1 + 1) - config.tileWidth = 256; - config.tileHeight = 256; + config.sector.set(tileMatrixSet.getMinY(), tileMatrixSet.getMinX(), + tileMatrixSet.getMaxY() - tileMatrixSet.getMinY(), + tileMatrixSet.getMaxX() - tileMatrixSet.getMinX()); + config.tileOrigin.set(tileMatrixSet.getMinY(), tileMatrixSet.getMinX()); + config.firstLevelDelta = (tileMatrixSet.getMaxY() - tileMatrixSet.getMinY()) / tileMatrix.valueAt(0).getMatrixHeight(); + config.numLevels = tileMatrix.size(); TiledSurfaceImage surfaceImage = new TiledSurfaceImage(); + surfaceImage.setDisplayName(content.getIdentifier()); surfaceImage.setLevelSet(new LevelSet(config)); surfaceImage.setTileFactory(new GpkgTileFactory(content)); gpkgRenderables.addRenderable(surfaceImage); diff --git a/worldwind/src/main/java/gov/nasa/worldwind/ogc/gpkg/GpkgTileFactory.java b/worldwind/src/main/java/gov/nasa/worldwind/ogc/gpkg/GpkgTileFactory.java index d9aa16cad..246c46495 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/ogc/gpkg/GpkgTileFactory.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/ogc/gpkg/GpkgTileFactory.java @@ -5,6 +5,8 @@ package gov.nasa.worldwind.ogc.gpkg; +import android.util.SparseArray; + import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.render.ImageSource; import gov.nasa.worldwind.render.ImageTile; @@ -40,23 +42,24 @@ public Tile createTile(Sector sector, Level level, int row, int column) { ImageTile tile = new ImageTile(sector, level, row, column); - String tableName = this.tiles.getTableName(); - int zoomLevel = level.levelNumber; - - // Attempt to find the GeoPackage tile matrix associated with the WorldWind level. Assumes that the WorldWind - // levels match the GeoPackage tile matrix zoom levels. If there's no match then the GeoPackage contains no - // tiles for this level and this tile has no image source. GeoPackage geoPackage = this.tiles.getContainer(); - GpkgTileMatrix tileMatrix = geoPackage.getTileMatrix(tableName).get(zoomLevel); + String tableName = this.tiles.getTableName(); + SparseArray tileMatrixByZoomLevel = geoPackage.getTileMatrix(tableName); GpkgTileUserMetrics tileUserMetrics = geoPackage.getTileUserMetrics(tableName); + // Attempt to find the GeoPackage tile matrix associated with the WorldWind level. + int zoomLevel = level.levelNumber + tileMatrixByZoomLevel.keyAt(0); + GpkgTileMatrix tileMatrix = tileMatrixByZoomLevel.get(zoomLevel); + + // Check if content table has any tiles on this zoom level. if (tileMatrix != null && tileUserMetrics.hasZoomLevel(zoomLevel)) { - // Convert the WorldWind tile address to the equivalent GeoPackage tile address. Assumes that the World - // Wind level set matchs the GeoPackage tile matrix set, with the exception of tile rows which are inverted. - int gpkgRow = tileMatrix.getMatrixHeight() - row - 1; - // Configure the tile with a bitmap factory that reads directly from the GeoPackage. - ImageSource.BitmapFactory bitmapFactory = new GpkgBitmapFactory(this.tiles, zoomLevel, column, gpkgRow); - tile.setImageSource(ImageSource.fromBitmapFactory(bitmapFactory)); + // Convert the WorldWind tile row to the equivalent GeoPackage tile row. + int gpkgRow = level.levelHeight / level.tileHeight - row - 1; + if (column < tileMatrix.getMatrixWidth() && gpkgRow < tileMatrix.getMatrixHeight()) { + // Configure the tile with a bitmap factory that reads directly from the GeoPackage. + ImageSource.BitmapFactory bitmapFactory = new GpkgBitmapFactory(this.tiles, zoomLevel, column, gpkgRow); + tile.setImageSource(ImageSource.fromBitmapFactory(bitmapFactory)); + } } return tile; From c17e8b04da338ef5f78a46e02a63ac544b98e24f Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Sat, 23 Apr 2022 17:39:07 +0300 Subject: [PATCH 06/10] Calculate number of levels as (maximal - minimal + 1) instead of taking levels size in case of gaps in levels numeration. --- .../src/main/java/gov/nasa/worldwind/layer/LayerFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java index 64908bee0..d741cd338 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java @@ -281,7 +281,7 @@ protected void createFromGeoPackageAsync(String pathName, Layer layer, Callback tileMatrixSet.getMaxX() - tileMatrixSet.getMinX()); config.tileOrigin.set(tileMatrixSet.getMinY(), tileMatrixSet.getMinX()); config.firstLevelDelta = (tileMatrixSet.getMaxY() - tileMatrixSet.getMinY()) / tileMatrix.valueAt(0).getMatrixHeight(); - config.numLevels = tileMatrix.size(); + config.numLevels = tileMatrix.keyAt(tileMatrix.size() - 1) - tileMatrix.keyAt(0) + 1; TiledSurfaceImage surfaceImage = new TiledSurfaceImage(); surfaceImage.setDisplayName(content.getIdentifier()); From 99d1eabba6efe9594a074418867b277f42ba96dc Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Wed, 29 Jun 2022 19:17:04 +0300 Subject: [PATCH 07/10] Separate first level delta in Level Set for latitude and longitude. --- .../worldwind/globe/BasicTessellator.java | 2 +- .../layer/BlueMarbleLandsatLayer.java | 2 +- .../nasa/worldwind/layer/LayerFactory.java | 7 +++- .../layer/mercator/MercatorImageTile.java | 28 +++++++------ .../layer/mercator/MercatorSector.java | 3 +- .../mercator/MercatorTiledImageLayer.java | 10 ++--- .../java/gov/nasa/worldwind/util/Level.java | 12 +++--- .../gov/nasa/worldwind/util/LevelSet.java | 25 +++++------ .../nasa/worldwind/util/LevelSetConfig.java | 12 +++--- .../java/gov/nasa/worldwind/util/Tile.java | 41 +++++++++---------- .../worldwind/globe/BasicTerrainTest.java | 2 +- 11 files changed, 71 insertions(+), 73 deletions(-) diff --git a/worldwind/src/main/java/gov/nasa/worldwind/globe/BasicTessellator.java b/worldwind/src/main/java/gov/nasa/worldwind/globe/BasicTessellator.java index 7df48be31..bab566746 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/globe/BasicTessellator.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/globe/BasicTessellator.java @@ -33,7 +33,7 @@ public class BasicTessellator implements Tessellator, TileFactory { // ~0.6 meter resolution - protected LevelSet levelSet = new LevelSet(new Sector().setFullSphere(), new Location(-90, -180), 90, 20, 32, 32); + protected LevelSet levelSet = new LevelSet(new Sector().setFullSphere(), new Location(-90, -180), new Location(90, 90), 20, 32, 32); protected double detailControl = 80; diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/BlueMarbleLandsatLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/BlueMarbleLandsatLayer.java index bfc3825c1..8c9d17046 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/layer/BlueMarbleLandsatLayer.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/BlueMarbleLandsatLayer.java @@ -85,7 +85,7 @@ public BlueMarbleLandsatLayer(String serviceAddress) { @Override public Tile createTile(Sector sector, Level level, int row, int column) { - double radiansPerPixel = Math.toRadians(level.tileDelta) / level.tileHeight; + double radiansPerPixel = Math.toRadians(level.tileDelta.latitude) / level.tileHeight; double metersPerPixel = radiansPerPixel * WorldWind.WGS84_SEMI_MAJOR_AXIS; if (metersPerPixel < 2.0e3) { // switch to Landsat at 2km resolution diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java index d741cd338..ae8d45daa 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/LayerFactory.java @@ -280,7 +280,10 @@ protected void createFromGeoPackageAsync(String pathName, Layer layer, Callback tileMatrixSet.getMaxY() - tileMatrixSet.getMinY(), tileMatrixSet.getMaxX() - tileMatrixSet.getMinX()); config.tileOrigin.set(tileMatrixSet.getMinY(), tileMatrixSet.getMinX()); - config.firstLevelDelta = (tileMatrixSet.getMaxY() - tileMatrixSet.getMinY()) / tileMatrix.valueAt(0).getMatrixHeight(); + config.firstLevelDelta.set( + (tileMatrixSet.getMaxY() - tileMatrixSet.getMinY()) / tileMatrix.valueAt(0).getMatrixHeight(), + (tileMatrixSet.getMaxX() - tileMatrixSet.getMinX()) / tileMatrix.valueAt(0).getMatrixWidth() + ); config.numLevels = tileMatrix.keyAt(tileMatrix.size() - 1) - tileMatrix.keyAt(0) + 1; TiledSurfaceImage surfaceImage = new TiledSurfaceImage(); @@ -681,7 +684,7 @@ protected LevelSet createWmtsLevelSet(WmtsLayer wmtsLayer, CompatibleTileMatrixS } int imageSize = tileMatrixSet.getTileMatrices().get(0).getTileHeight(); - return new LevelSet(boundingBox, new Location(-90, -180), 90, compatibleTileMatrixSet.tileMatrices.size(), imageSize, imageSize); + return new LevelSet(boundingBox, new Location(-90, -180), new Location(90, 90), compatibleTileMatrixSet.tileMatrices.size(), imageSize, imageSize); } protected String buildWmtsKvpTemplate(String kvpServiceAddress, String layer, String format, String styleIdentifier, String tileMatrixSet) { 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 index 03eccaa62..2e82037c9 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorImageTile.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorImageTile.java @@ -53,23 +53,25 @@ static void assembleMercatorTilesForLevel(Level level, TileFactory tileFactory, // NOTE LevelSet.sector is final Sector attribute and thus can not be cast to MercatorSector! MercatorSector sector = MercatorSector.fromSector(level.parent.sector); Location tileOrigin = level.parent.tileOrigin; - double dLat = level.tileDelta / 2; - double dLon = level.tileDelta; + double dLat = level.tileDelta.latitude; + double dLon = level.tileDelta.longitude; int firstRow = Tile.computeRow(dLat, sector.minLatitude(), tileOrigin.latitude); int lastRow = Tile.computeLastRow(dLat, sector.maxLatitude(), tileOrigin.latitude); int firstCol = Tile.computeColumn(dLon, sector.minLongitude(), tileOrigin.longitude); int lastCol = Tile.computeLastColumn(dLon, sector.maxLongitude(), tileOrigin.longitude); - double deltaLat = dLat / 90; - double d1 = sector.minLatPercent() + deltaLat * firstRow; + double dLatPercent = dLat / sector.deltaLatitude() * (sector.maxLatPercent() - sector.minLatPercent()); + double firstRowPercent = MercatorSector.gudermannianInverse(tileOrigin.latitude) + firstRow * dLatPercent; + double firstColLon = tileOrigin.longitude + firstCol * dLon; + + double d1 = firstRowPercent; for (int row = firstRow; row <= lastRow; row++) { - double d2 = d1 + deltaLat; - double t1 = tileOrigin.longitude + (firstCol * dLon); + double d2 = d1 + dLatPercent; + double t1 = firstColLon; 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)); + double t2 = t1 + dLon; + result.add(tileFactory.createTile(new MercatorSector(d1, d2, t1, t2), level, row, col)); t1 = t2; } d1 = d2; @@ -114,10 +116,10 @@ public Tile[] subdivide(TileFactory tileFactory) { 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); + children[0] = tileFactory.createTile(new MercatorSector(d0, d1, t0, t1), childLevel, northRow, westCol); + children[1] = tileFactory.createTile(new MercatorSector(d0, d1, t1, t2), childLevel, northRow, eastCol); + children[2] = tileFactory.createTile(new MercatorSector(d1, d2, t0, t1), childLevel, southRow, westCol); + children[3] = tileFactory.createTile(new MercatorSector(d1, d2, t1, t2), childLevel, southRow, eastCol); return children; } 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 index ac9541696..dfb36c689 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorSector.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorSector.java @@ -6,8 +6,7 @@ public class MercatorSector extends Sector { private final double minLatPercent, maxLatPercent; - private MercatorSector(double minLatPercent, double maxLatPercent, - double minLongitude, double maxLongitude) { + public MercatorSector(double minLatPercent, double maxLatPercent, double minLongitude, double maxLongitude) { this.minLatPercent = minLatPercent; this.maxLatPercent = maxLatPercent; this.minLatitude = gudermannian(minLatPercent); 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 index cfb33e7a6..0af5e6b46 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledImageLayer.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledImageLayer.java @@ -13,8 +13,6 @@ 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) { @@ -22,10 +20,12 @@ public MercatorTiledImageLayer(String name, int numLevels, int firstLevelOffset, this.setPickEnabled(false); this.firstLevelOffset = firstLevelOffset; + MercatorSector sector = new MercatorSector(-1.0, 1.0, -180.0, 180.0); + Location tileOrigin = new Location(sector.minLatitude(), sector.minLongitude()); + int n = 1 << firstLevelOffset; + Location firstLevelDelta = new Location(sector.deltaLatitude() / n, sector.deltaLongitude() / n); MercatorTiledSurfaceImage surfaceImage = new MercatorTiledSurfaceImage(); - surfaceImage.setLevelSet(new LevelSet( - MercatorSector.fromDegrees(-1.0, 1.0, - FULL_SPHERE / 2, FULL_SPHERE / 2), new Location(-90, -180), - FULL_SPHERE / (1 << firstLevelOffset), numLevels - firstLevelOffset, tileSize, tileSize)); + surfaceImage.setLevelSet(new LevelSet(sector, tileOrigin, firstLevelDelta, 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 diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/Level.java b/worldwind/src/main/java/gov/nasa/worldwind/util/Level.java index 911bb836b..ac0ffa42f 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/util/Level.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/Level.java @@ -5,6 +5,8 @@ package gov.nasa.worldwind.util; +import gov.nasa.worldwind.geom.Location; + /** * Represents a level of a specific resolution in a {@link LevelSet}. */ @@ -35,7 +37,7 @@ public class Level { /** * The geographic width and height in degrees of tiles within this level. */ - public final double tileDelta; + public final Location tileDelta; /** * The parent LevelSet's tileWidth. @@ -54,21 +56,21 @@ public class Level { * @param levelNumber the level's ordinal within its parent level set * @param tileDelta the geographic width and height in degrees of tiles within this level */ - public Level(LevelSet parent, int levelNumber, double tileDelta) { + public Level(LevelSet parent, int levelNumber, Location tileDelta) { if (parent == null) { throw new IllegalArgumentException( Logger.logMessage(Logger.ERROR, "Level", "constructor", "The parent level set is null")); } - if (tileDelta <= 0) { + if (tileDelta == null || tileDelta.latitude <= 0 || tileDelta.longitude <= 0) { throw new IllegalArgumentException( Logger.logMessage(Logger.ERROR, "Level", "constructor", "The tile delta is zero")); } this.parent = parent; this.levelNumber = levelNumber; - this.levelWidth = (int) Math.round(parent.tileWidth * parent.sector.deltaLongitude() / tileDelta); - this.levelHeight = (int) Math.round(parent.tileHeight * parent.sector.deltaLatitude() / tileDelta); + this.levelWidth = (int) Math.round(parent.tileWidth * parent.sector.deltaLongitude() / tileDelta.longitude); + this.levelHeight = (int) Math.round(parent.tileHeight * parent.sector.deltaLatitude() / tileDelta.latitude); this.tileDelta = tileDelta; this.tileWidth = parent.tileWidth; this.tileHeight = parent.tileHeight; diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java index e94fdfdf7..71c1b5798 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java @@ -25,9 +25,9 @@ public class LevelSet { public final Location tileOrigin = new Location(); /** - * The geographic width and height in degrees of tiles in the first level (lowest resolution) of this level set. + * The geographic width and height in degrees of tiles in the first level (the lowest resolution) of this level set. */ - public final double firstLevelDelta; + public final Location firstLevelDelta = new Location(); /** * The width in pixels of images associated with tiles in this level set, or the number of sample points in the @@ -51,7 +51,6 @@ public class LevelSet { * firstLevel and lastLevel always return null. */ public LevelSet() { - this.firstLevelDelta = 0; this.tileWidth = 0; this.tileHeight = 0; this.levels = new Level[0]; @@ -74,7 +73,7 @@ public LevelSet() { * * @throws IllegalArgumentException If any argument is null, or if any dimension is zero */ - public LevelSet(Sector sector, Location tileOrigin, double firstLevelDelta, int numLevels, int tileWidth, int tileHeight) { + public LevelSet(Sector sector, Location tileOrigin, Location firstLevelDelta, int numLevels, int tileWidth, int tileHeight) { if (sector == null) { throw new IllegalArgumentException( Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "missingSector")); @@ -85,7 +84,7 @@ public LevelSet(Sector sector, Location tileOrigin, double firstLevelDelta, int Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "missingTileOrigin")); } - if (firstLevelDelta <= 0) { + if (firstLevelDelta == null || firstLevelDelta.latitude <= 0 || firstLevelDelta.longitude <= 0) { throw new IllegalArgumentException( Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "invalidTileDelta")); } @@ -102,7 +101,7 @@ public LevelSet(Sector sector, Location tileOrigin, double firstLevelDelta, int this.sector.set(sector); this.tileOrigin.set(tileOrigin); - this.firstLevelDelta = firstLevelDelta; + this.firstLevelDelta.set(firstLevelDelta); this.tileWidth = tileWidth; this.tileHeight = tileHeight; this.levels = new Level[numLevels]; @@ -129,12 +128,7 @@ public LevelSet(LevelSetConfig config) { Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "missingSector")); } - if (config.tileOrigin == null) { - throw new IllegalArgumentException( - Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "missingTileOrigin")); - } - - if (config.firstLevelDelta <= 0) { + if (config.firstLevelDelta.latitude <= 0 || config.firstLevelDelta.longitude <= 0) { throw new IllegalArgumentException( Logger.logMessage(Logger.ERROR, "LevelSet", "constructor", "invalidTileDelta")); } @@ -151,7 +145,7 @@ public LevelSet(LevelSetConfig config) { this.sector.set(config.sector); this.tileOrigin.set(config.tileOrigin); - this.firstLevelDelta = config.firstLevelDelta; + this.firstLevelDelta.set(config.firstLevelDelta); this.tileWidth = config.tileWidth; this.tileHeight = config.tileHeight; this.levels = new Level[config.numLevels]; @@ -160,7 +154,8 @@ public LevelSet(LevelSetConfig config) { protected void assembleLevels() { for (int i = 0, len = this.levels.length; i < len; i++) { - double delta = firstLevelDelta / (1 << i); + int n = 1 << i; + Location delta = new Location(firstLevelDelta.latitude / n, firstLevelDelta.longitude / n); this.levels[i] = new Level(this, i, delta); } } @@ -209,7 +204,7 @@ public Level levelForResolution(double radiansPerPixel) { } double degreesPerPixel = Math.toDegrees(radiansPerPixel); - double firstLevelDegreesPerPixel = this.firstLevelDelta / Math.min(this.tileWidth, this.tileHeight); + double firstLevelDegreesPerPixel = Math.max(this.firstLevelDelta.longitude / this.tileWidth, this.firstLevelDelta.latitude / this.tileHeight); double level = Math.log(firstLevelDegreesPerPixel / degreesPerPixel) / Math.log(2); // fractional level address int levelNumber = (int) Math.round(level); // nearest neighbor level diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java index f8876bf6a..83e189ccc 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java @@ -25,9 +25,9 @@ public class LevelSetConfig { public final Location tileOrigin = new Location(-90, -180); /** - * The geographic width and height in degrees of tiles in the first level (lowest resolution) of the level set. + * The geographic width and height in degrees of tiles in the first level (the lowest resolution) of the level set. */ - public double firstLevelDelta = 90; + public final Location firstLevelDelta = new Location(90, 90); /** * The number of levels in the level set. @@ -69,12 +69,12 @@ public LevelSetConfig() { * sample points in the latitudinal direction of elevation tiles associate with the level * set */ - public LevelSetConfig(Sector sector, double firstLevelDelta, int numLevels, int tileWidth, int tileHeight) { + public LevelSetConfig(Sector sector, Location firstLevelDelta, int numLevels, int tileWidth, int tileHeight) { if (sector != null) { this.sector.set(sector); } - this.firstLevelDelta = firstLevelDelta; + this.firstLevelDelta.set(firstLevelDelta); this.numLevels = numLevels; this.tileWidth = tileWidth; this.tileHeight = tileHeight; @@ -97,7 +97,7 @@ public int numLevelsForResolution(double radiansPerPixel) { } double degreesPerPixel = Math.toDegrees(radiansPerPixel); - double firstLevelDegreesPerPixel = this.firstLevelDelta / Math.min(this.tileWidth, this.tileHeight); + double firstLevelDegreesPerPixel = Math.max(this.firstLevelDelta.longitude / this.tileWidth, this.firstLevelDelta.latitude / this.tileHeight); double level = Math.log(firstLevelDegreesPerPixel / degreesPerPixel) / Math.log(2); // fractional level address int levelNumber = (int) Math.ceil(level); // ceiling captures the resolution @@ -126,7 +126,7 @@ public int numLevelsForMinResolution(double radiansPerPixel) { } double degreesPerPixel = Math.toDegrees(radiansPerPixel); - double firstLevelDegreesPerPixel = this.firstLevelDelta / this.tileHeight; + double firstLevelDegreesPerPixel = Math.max(this.firstLevelDelta.longitude / this.tileWidth, this.firstLevelDelta.latitude / this.tileHeight); double level = Math.log(firstLevelDegreesPerPixel / degreesPerPixel) / Math.log(2); // fractional level address int levelNumber = (int) Math.floor(level); // floor prevents exceeding the min scale diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/Tile.java b/worldwind/src/main/java/gov/nasa/worldwind/util/Tile.java index 319ea0c9e..e4cf38a0b 100755 --- a/worldwind/src/main/java/gov/nasa/worldwind/util/Tile.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/Tile.java @@ -109,7 +109,7 @@ public Tile(Sector sector, Level level, int row, int column) { this.row = row; this.column = column; this.tileKey = level.levelNumber + "." + row + "." + column; - this.texelSizeFactor = Math.toRadians(level.tileDelta / level.tileWidth) * Math.cos(Math.toRadians(sector.centroidLatitude())); + this.texelSizeFactor = Math.toRadians(level.tileDelta.longitude / level.tileWidth) * Math.cos(Math.toRadians(sector.centroidLatitude())); } /** @@ -217,29 +217,25 @@ public static Collection assembleTilesForLevel(Level level, TileFactory ti Sector sector = level.parent.sector; Location tileOrigin = level.parent.tileOrigin; - double tileDelta = level.tileDelta; + Location tileDelta = level.tileDelta; - int firstRow = Tile.computeRow(tileDelta, sector.minLatitude(), tileOrigin.latitude); - int lastRow = Tile.computeLastRow(tileDelta, sector.maxLatitude(), tileOrigin.latitude); - int firstCol = Tile.computeColumn(tileDelta, sector.minLongitude(), tileOrigin.longitude); - int lastCol = Tile.computeLastColumn(tileDelta, sector.maxLongitude(), tileOrigin.longitude); + int firstRow = Tile.computeRow(tileDelta.latitude, sector.minLatitude(), tileOrigin.latitude); + int lastRow = Tile.computeLastRow(tileDelta.latitude, sector.maxLatitude(), tileOrigin.latitude); + int firstCol = Tile.computeColumn(tileDelta.longitude, sector.minLongitude(), tileOrigin.longitude); + int lastCol = Tile.computeLastColumn(tileDelta.longitude, sector.maxLongitude(), tileOrigin.longitude); - double firstRowLat = tileOrigin.latitude + firstRow * tileDelta; - double firstRowLon = tileOrigin.longitude + firstCol * tileDelta; - double lat = firstRowLat; - double lon; + double firstRowLat = tileOrigin.latitude + firstRow * tileDelta.latitude; + double firstColLon = tileOrigin.longitude + firstCol * tileDelta.longitude; + double lat = firstRowLat; for (int row = firstRow; row <= lastRow; row++) { - lon = firstRowLon; - + double lon = firstColLon; for (int col = firstCol; col <= lastCol; col++) { - Sector tileSector = new Sector(lat, lon, tileDelta, tileDelta); + Sector tileSector = new Sector(lat, lon, tileDelta.latitude, tileDelta.longitude); result.add(tileFactory.createTile(tileSector, level, row, col)); - - lon += tileDelta; + lon += tileDelta.longitude; } - - lat += tileDelta; + lat += tileDelta.latitude; } return result; @@ -334,26 +330,27 @@ public Tile[] subdivide(TileFactory tileFactory) { double lonMin = this.sector.minLongitude(); double latMid = this.sector.centroidLatitude(); double lonMid = this.sector.centroidLongitude(); - double childDelta = this.level.tileDelta * 0.5; + double childDeltaLat = this.level.tileDelta.latitude * 0.5; + double childDeltaLon = this.level.tileDelta.longitude * 0.5; int childRow = 2 * this.row; int childCol = 2 * this.column; - Sector childSector = new Sector(latMin, lonMin, childDelta, childDelta); + Sector childSector = new Sector(latMin, lonMin, childDeltaLat, childDeltaLon); children[0] = tileFactory.createTile(childSector, childLevel, childRow, childCol); // Southwest childRow = 2 * this.row; childCol = 2 * this.column + 1; - childSector = new Sector(latMin, lonMid, childDelta, childDelta); + childSector = new Sector(latMin, lonMid, childDeltaLat, childDeltaLon); children[1] = tileFactory.createTile(childSector, childLevel, childRow, childCol); // Southeast childRow = 2 * this.row + 1; childCol = 2 * this.column; - childSector = new Sector(latMid, lonMin, childDelta, childDelta); + childSector = new Sector(latMid, lonMin, childDeltaLat, childDeltaLon); children[2] = tileFactory.createTile(childSector, childLevel, childRow, childCol); // Northwest childRow = 2 * this.row + 1; childCol = 2 * this.column + 1; - childSector = new Sector(latMid, lonMid, childDelta, childDelta); + childSector = new Sector(latMid, lonMid, childDeltaLat, childDeltaLon); children[3] = tileFactory.createTile(childSector, childLevel, childRow, childCol); // Northeast return children; diff --git a/worldwind/src/test/java/gov/nasa/worldwind/globe/BasicTerrainTest.java b/worldwind/src/test/java/gov/nasa/worldwind/globe/BasicTerrainTest.java index 2eebe1267..f12f46d2e 100644 --- a/worldwind/src/test/java/gov/nasa/worldwind/globe/BasicTerrainTest.java +++ b/worldwind/src/test/java/gov/nasa/worldwind/globe/BasicTerrainTest.java @@ -82,7 +82,7 @@ public void setUp() { this.terrain = new BasicTerrain(); // Add a terrain tile used to the mocked terrain - LevelSet levelSet = new LevelSet(new Sector().setFullSphere(), new Location(-90, -180), 1.0, 1, 5, 5); // tiles with 5x5 vertices + LevelSet levelSet = new LevelSet(new Sector().setFullSphere(), new Location(-90, -180), new Location(1.0, 1.0), 1, 5, 5); // tiles with 5x5 vertices TerrainTile tile = new TerrainTile(new Sector(0, 0, 1, 1), levelSet.firstLevel(), 90, 180); ((BasicTerrain) this.terrain).addTile(tile); From 06ee66caf9b73ba90a99a323dad72917c4c00298 Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Mon, 7 Dec 2020 16:22:37 +0200 Subject: [PATCH 08/10] Add MercatorTiledImageLayer support. --- .../layer/mercator/MercatorImageTile.java | 144 ++++++++++++++++++ .../layer/mercator/MercatorSector.java | 47 ++++++ .../mercator/MercatorTiledImageLayer.java | 44 ++++++ .../mercator/MercatorTiledSurfaceImage.java | 16 ++ .../nasa/worldwind/render/ImageRetriever.java | 14 +- .../nasa/worldwind/render/ImageSource.java | 20 +++ .../worldwind/util/DownloadPostprocessor.java | 14 ++ 7 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorImageTile.java create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorSector.java create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledImageLayer.java create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/MercatorTiledSurfaceImage.java create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/util/DownloadPostprocessor.java 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); +} From 714d607f0703ea7e15a06aaf1307c56bc3a49bd8 Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Sun, 31 Jul 2022 19:28:39 +0300 Subject: [PATCH 09/10] Use latitude per pixel resolution in LevelSet calcualtions. --- worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java | 2 +- .../src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java index 71c1b5798..86c46fea4 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSet.java @@ -204,7 +204,7 @@ public Level levelForResolution(double radiansPerPixel) { } double degreesPerPixel = Math.toDegrees(radiansPerPixel); - double firstLevelDegreesPerPixel = Math.max(this.firstLevelDelta.longitude / this.tileWidth, this.firstLevelDelta.latitude / this.tileHeight); + double firstLevelDegreesPerPixel = this.firstLevelDelta.latitude / this.tileHeight; double level = Math.log(firstLevelDegreesPerPixel / degreesPerPixel) / Math.log(2); // fractional level address int levelNumber = (int) Math.round(level); // nearest neighbor level diff --git a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java index 83e189ccc..917a53a84 100644 --- a/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java +++ b/worldwind/src/main/java/gov/nasa/worldwind/util/LevelSetConfig.java @@ -97,7 +97,7 @@ public int numLevelsForResolution(double radiansPerPixel) { } double degreesPerPixel = Math.toDegrees(radiansPerPixel); - double firstLevelDegreesPerPixel = Math.max(this.firstLevelDelta.longitude / this.tileWidth, this.firstLevelDelta.latitude / this.tileHeight); + double firstLevelDegreesPerPixel = this.firstLevelDelta.latitude / this.tileHeight; double level = Math.log(firstLevelDegreesPerPixel / degreesPerPixel) / Math.log(2); // fractional level address int levelNumber = (int) Math.ceil(level); // ceiling captures the resolution @@ -126,7 +126,7 @@ public int numLevelsForMinResolution(double radiansPerPixel) { } double degreesPerPixel = Math.toDegrees(radiansPerPixel); - double firstLevelDegreesPerPixel = Math.max(this.firstLevelDelta.longitude / this.tileWidth, this.firstLevelDelta.latitude / this.tileHeight); + double firstLevelDegreesPerPixel = this.firstLevelDelta.latitude / this.tileHeight; double level = Math.log(firstLevelDegreesPerPixel / degreesPerPixel) / Math.log(2); // fractional level address int levelNumber = (int) Math.floor(level); // floor prevents exceeding the min scale From dabf6af05857ccbb993fc9a6b10b807496e88c1d Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Sun, 7 Aug 2022 22:33:09 +0300 Subject: [PATCH 10/10] Add Google, OpenStreetMap, OpenTogographicMap and WikiMap layers support. --- .../layer/mercator/google/GoogleLayer.java | 39 +++++++++++++++++++ .../layer/mercator/osm/OSMLayer.java | 23 +++++++++++ .../layer/mercator/osm/OTMLayer.java | 23 +++++++++++ .../layer/mercator/wiki/WikiLayer.java | 24 ++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/google/GoogleLayer.java create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/osm/OSMLayer.java create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/osm/OTMLayer.java create mode 100644 worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/wiki/WikiLayer.java diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/google/GoogleLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/google/GoogleLayer.java new file mode 100644 index 000000000..03d79820e --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/google/GoogleLayer.java @@ -0,0 +1,39 @@ +package gov.nasa.worldwind.layer.mercator.google; + +import gov.nasa.worldwind.layer.mercator.MercatorTiledImageLayer; + +public class GoogleLayer extends MercatorTiledImageLayer { + + public GoogleLayer(Type type) { + super(type.layerName, 22, 0, 256, type.overlay); + this.lyrs = type.lyrs; + } + + private final String lyrs; + + 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); + + private final String layerName; + private final String lyrs; + private final boolean overlay; + + Type(String layerName, String lyrs, boolean overlay) { + this.layerName = layerName; + this.lyrs = lyrs; + this.overlay = overlay; + } + } + + @Override + public String getImageSourceUrl(int x, int y, int z) { + return "https://mt.google.com/vt/lyrs="+lyrs+"&x="+x+"&y="+y+"&z="+z+"&hl=ru"; + } +} \ No newline at end of file diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/osm/OSMLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/osm/OSMLayer.java new file mode 100644 index 000000000..a76549cf5 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/osm/OSMLayer.java @@ -0,0 +1,23 @@ +package gov.nasa.worldwind.layer.mercator.osm; + +import java.util.Random; + +import gov.nasa.worldwind.layer.mercator.MercatorTiledImageLayer; + +public class OSMLayer extends MercatorTiledImageLayer { + + public static final String NAME = "OpenStreetMap"; + + private final Random random = new Random(); + + public OSMLayer() { + super(NAME, 20, 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"; + } + +} \ No newline at end of file diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/osm/OTMLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/osm/OTMLayer.java new file mode 100644 index 000000000..a91916ce5 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/osm/OTMLayer.java @@ -0,0 +1,23 @@ +package gov.nasa.worldwind.layer.mercator.osm; + +import java.util.Random; + +import gov.nasa.worldwind.layer.mercator.MercatorTiledImageLayer; + +public class OTMLayer extends MercatorTiledImageLayer { + + public static final String NAME = "OpenTopoMap"; + + private final Random random = new Random(); + + public OTMLayer() { + super(NAME, 18, 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"; + } + +} \ No newline at end of file diff --git a/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/wiki/WikiLayer.java b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/wiki/WikiLayer.java new file mode 100644 index 000000000..48c035154 --- /dev/null +++ b/worldwind/src/main/java/gov/nasa/worldwind/layer/mercator/wiki/WikiLayer.java @@ -0,0 +1,24 @@ +package gov.nasa.worldwind.layer.mercator.wiki; + +import gov.nasa.worldwind.layer.mercator.MercatorTiledImageLayer; + +public class WikiLayer extends MercatorTiledImageLayer { + + public enum Type { MAP, HYBRID } + + public static final String NAME = "Wiki"; + + private final Type type; + + public WikiLayer(Type type) { + super(NAME + type.name().toLowerCase(), 23, 3, 256, Type.HYBRID == 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(); + } + +} \ No newline at end of file