In this article, we explain how you can load and display worldwide OpenStreetMap raster data from OpenStreetMap tile servers that adhere to a tile URL pattern similar to 'http(s)://baseUrl/${z}/${x}/${y}.png', with z, x and y respectively referring to zoom level, tile column and tile row. For an overview of available servers, see http://wiki.openstreetmap.org/wiki/Tile_servers. You can apply the principles used in this article to other tile servers adhering to a similar tile URL pattern.
LuciadLightspeed also supports loading worldwide OpenStreetMap vector data; see the Visualizing large vector data in Lightspeed views article for more information.
Visualizing OSM raster tiles data on a GXY view requires the same two steps as the majority of the formats:
-
Decode the data into an
ILcdModel
. -
Create an
ILcdGXYLayer
for theILcdModel
and add it to theILcdGXYView
.
//First create the model
//Start by creating a UrlTileSet for an OpenStreetMap raster tile service.
// See https://wiki.openstreetmap.org/wiki/Tile_servers for examples.
String sourceUrl = "http://a.tile.openstreetmap.fr/hot/";
UrlTileSet urlTileSet = new UrlTileSet(sourceUrl);
// Create a model for the tileset.
TLcd2DBoundsIndexedModel model = new TLcd2DBoundsIndexedModel();
model.setModelReference(new TLcdGridReference(new TLcdGeodeticDatum(), new TLcdPseudoMercator()));
model.setModelDescriptor(new TLcdEarthModelDescriptor(sourceUrl, "OSM", "OpenStreetMap"));
model.setModelMetadataFunction(m -> TLcdModelMetadata.newBuilder()
.fromModel(m)
.entryPoint(new TLcdModelMetadata.Source(m.getModelDescriptor().getSourceName(), "image/png"))
.addDataCategory(TLcdModelMetadata.DataCategory.RASTER)
.build());
model.addElement(urlTileSet, ILcdModel.NO_EVENT);
//Create a layer for the model
TLcdGXYLayer layer = TLcdGXYLayer.create(model);
TLcdGXYImagePainter painter = new TLcdGXYImagePainter();
//Adjust the painter's level switch factor and oversampling rate
//to optimize the OSM raster tile quality
painter.setLevelSwitchFactor(0.3D);
painter.setOversamplingRate(2);
layer.setGXYPainterProvider(painter);
//Wrap the layer with an async layer wrapper to ensure
//that the view remains responsive while data is being painted
ILcdGXYLayer asyncLayer = ILcdGXYAsynchronousLayerWrapper.create(layer);
//Add the async layer to the GXY view (an ILcdGXYView)
view.addGXYLayer(layer);
package com.luciad.format.osmrastertiles;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import javax.imageio.ImageIO;
import com.luciad.earth.tileset.ALcdEarthTileSet;
import com.luciad.earth.tileset.ILcdEarthRasterTileSetCoverage;
import com.luciad.earth.tileset.ILcdEarthTileSet;
import com.luciad.earth.tileset.ILcdEarthTileSetCallback;
import com.luciad.earth.tileset.ILcdEarthTileSetCoverage;
import com.luciad.earth.tileset.TLcdEarthTile;
import com.luciad.earth.tileset.TLcdEarthTileFormat;
import com.luciad.earth.tileset.TLcdEarthTileOperationMode;
import com.luciad.earth.tileset.TLcdEarthTileSetCoverage;
import com.luciad.geodesy.TLcdGeodeticDatum;
import com.luciad.projection.TLcdPseudoMercator;
import com.luciad.reference.ILcdGeoReference;
import com.luciad.reference.TLcdGeodeticReference;
import com.luciad.reference.TLcdGridReference;
import com.luciad.shape.ILcdBounds;
import com.luciad.shape.shape2D.ILcd2DEditableBounds;
import com.luciad.shape.shape2D.TLcdLonLatBounds;
import com.luciad.shape.shape3D.TLcdXYZBounds;
import com.luciad.transformation.TLcdGeoReference2GeoReference;
/**
* Implementation of an {@code ALcdEarthTileSet} that provides the fundamentals to connect with
* generic tile services. The implementation typically has to be slightly adapted to exactly match the
* characteristics of the desired tile service.
* <p/>
* The constructor takes a base URL, specifying the base of a tile request; for instance,
* "http://localhost.com:8080/myTileService".
* <p/>
* To be able to connect with this base URL and retrieve tiles from it, the following parameters are used:
* <ul>
* <li>TOP_LEVEL_ID: number of levels offered by the service; default is 15</li>
* <li>TOP_LEVEL_ROW_COUNT: number of rows at the top (most zoomed out) level; default is 4</li>
* <li>TOP_LEVEL_COLUMN_COUNT: number of columns at the top (most zoomed out) level; default is 4</li>
* <li>TILE_WIDTH: the width of each tile; default is 256 pixels</li>
* <li>TILE_HEIGHT: the height of each tile; default is 256 pixels</li>
* <li>fReference: the reference in which the data is offered; default is Web Mercator (modeled in LuciadLightspeed as
* a TLcdGridReference with TLcdPseudoMercator projection)</li>
* <li>fBounds: the bounds of the data, expressed in the coordinate system defined by fReference; default is world bounds</li>
* </ul>
* Next to these parameters, a key method is {@link #getTileUrl} which constructs the actual tile request to the service,
* based on the configured base URL and the requested tile. The exact structure of a tile requests often changes from
* one tile service to another, so this probably needs to be changed as well.
*/
public class UrlTileSet extends ALcdEarthTileSet {
// Default values based on common OSM raster tile service schemes.
private static final int TOP_LEVEL_ID = 15;
private static final int TOP_LEVEL_ROW_COUNT = 1;
private static final int TOP_LEVEL_COLUMN_COUNT = 1;
private static final int TILE_WIDTH = 256;
private static final int TILE_HEIGHT = 256;
private static final ILcdGeoReference GEO_REFERENCE = new TLcdGridReference(new TLcdGeodeticDatum(), new TLcdPseudoMercator());
private final String fBaseUrl;
private final ILcdBounds fBounds;
private final ILcdGeoReference fReference;
private final ILcdEarthRasterTileSetCoverage fCoverage;
public UrlTileSet(String aBaseUrl) {
this(aBaseUrl, TOP_LEVEL_ID, TOP_LEVEL_ROW_COUNT, TOP_LEVEL_COLUMN_COUNT, createWorldBounds(GEO_REFERENCE, true), GEO_REFERENCE, TILE_WIDTH, TILE_HEIGHT);
}
public UrlTileSet(String aBaseUrl, int aLevelCount, long aLevel0Rows, long aLevel0Columns, ILcdBounds aBounds, ILcdGeoReference aReference, int aTileWidth, int aTileHeight) {
super(aLevelCount, aLevel0Rows, aLevel0Columns);
fBaseUrl = aBaseUrl;
fBounds = aBounds;
fReference = aReference;
fCoverage = new ImageCoverage(this, fReference, aTileWidth, aTileHeight);
}
private static ILcdBounds createWorldBounds(ILcdGeoReference aReference, boolean aSquare) {
TLcdGeodeticReference wgs84Ref = new TLcdGeodeticReference(new TLcdGeodeticDatum());
TLcdXYZBounds worldBounds = new TLcdXYZBounds();
TLcdGeoReference2GeoReference g2g = new TLcdGeoReference2GeoReference(wgs84Ref, aReference);
try {
g2g.sourceBounds2destinationSFCT(new TLcdLonLatBounds(-180, -90, 360, 180), worldBounds);
} catch (Exception exc) {
throw new IllegalArgumentException("Could not compute world bounds for " + aReference);
}
if (aSquare) {
// Apply square aspect ratio
double minY = -worldBounds.getWidth() / 2.0;
worldBounds.move2D(worldBounds.getLocation().getX(), minY);
worldBounds.setHeight(worldBounds.getWidth());
}
return worldBounds;
}
@Override
public void produceTile(ILcdEarthTileSetCoverage aCoverage, int aLevel, long aTileX, long aTileY, ILcdGeoReference aReference, TLcdEarthTileFormat aFormat, TLcdEarthTileOperationMode aMode, ILcdEarthTileSetCallback aCallback, Object aUserData) {
try {
ILcd2DEditableBounds tileBounds = fBounds.cloneAs2DEditableBounds();
getTileBoundsSFCT(aLevel, aTileX, aTileY, tileBounds);
BufferedImage image = getTileImage(aLevel, aTileX, aTileY, tileBounds);
if (image == null) {
aCallback.tileNotAvailable(aLevel, aTileX, aTileY, aCoverage, aReference, aFormat, aCoverage, null, null);
} else {
aCallback.tileAvailable(new TLcdEarthTile(tileBounds, image, aLevel, aTileX, aTileY, aCoverage, aReference, aFormat), aUserData);
}
} catch (IOException e) {
aCallback.tileNotAvailable(aLevel, aTileX, aTileY, aCoverage, aReference, aFormat, aUserData, e.getMessage(), e);
}
}
/**
* Retrieves the image for the specified tile.
* <p/>
* The default implementation requests an image from {@link #getTileUrl(String, int, long, long)}.
*
* @param aLevel the tile level
* @param aTileX the tile x coordinate
* @param aTileY the tile y coordinate
* @param aTileBounds the bounds of the tile
*
* @return the image or {@code null} if the tile does not exist
*
* @throws IOException if an IO error occurs
*/
private BufferedImage getTileImage(int aLevel, long aTileX, long aTileY, ILcdBounds aTileBounds) throws IOException {
String url = getTileUrl(fBaseUrl, aLevel, aTileX, aTileY);
URLConnection urlConnection = new URL(url).openConnection();
BufferedImage data;
try (InputStream stream = urlConnection.getInputStream()) {
data = ImageIO.read(stream);
}
return data;
}
/**
* Returns request url for a specific tile.
*
* @param aBaseUrl the base url
* @param aLevel the tile level
* @param aTileX the tile X coordinate
* @param aTileY the tile Y coordinate
*
* @return the url
*/
private String getTileUrl(String aBaseUrl, int aLevel, long aTileX, long aTileY) {
// Example of a row inversion, which might be needed if the tile service and LuciadLightspeed have a different
// Y axis order direction. This is the case for OSM raster tile services.
long inverseTileY = getTileRowCount(aLevel) - aTileY - 1;
return aBaseUrl + "/" + aLevel + "/" + aTileX + "/" + inverseTileY + ".png";
}
@Override
public boolean isFormatSupported(ILcdEarthTileSetCoverage aCoverage, TLcdEarthTileFormat aFormat) {
return aFormat.getFormatClass() != null && aFormat.getFormatClass().isAssignableFrom(BufferedImage.class);
}
@Override
public int getTileSetCoverageCount() {
return 1;
}
@Override
public ILcdEarthTileSetCoverage getTileSetCoverage(int aIndex) {
return fCoverage;
}
@Override
public ILcdBounds getBounds() {
return fBounds;
}
/**
* The coverage for the image data.
*/
private static class ImageCoverage extends TLcdEarthTileSetCoverage implements ILcdEarthRasterTileSetCoverage {
private final int fTileWidth;
private final int fTileHeight;
private final double[] fPixelDensity;
ImageCoverage(ILcdEarthTileSet aTileSet, ILcdGeoReference aGeoReference, int aTileWidth, int aTileHeight) {
super("Image", CoverageType.IMAGE, new TLcdEarthTileFormat(BufferedImage.class), aGeoReference);
fTileWidth = aTileWidth;
fTileHeight = aTileHeight;
fPixelDensity = new double[aTileSet.getLevelCount()];
for (int i = 0; i < fPixelDensity.length; i++) {
fPixelDensity[i] = fTileWidth * fTileHeight *
(aTileSet.getTileRowCount(i) / aTileSet.getBounds().getWidth()) *
(aTileSet.getTileColumnCount(i) / aTileSet.getBounds().getHeight());
}
}
@Override
public double getPixelDensity(int aLevel) {
return fPixelDensity[aLevel];
}
@Override
public int getTileWidth(int aLevel) {
return fTileWidth;
}
@Override
public int getTileHeight(int aLevel) {
return fTileHeight;
}
}
}
This results in a OSM Raster Tiles layer with default styling. See Visualizing Raster Data for more information about visualizing and styling raster data.