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. z, x and y refer to zoom level, tile column and tile row respectively. For an overview of available servers, see http://wiki.openstreetmap.org/wiki/Tile_servers. You can apply the principles in this article to other tile servers adhering to a similar tile URL pattern.

LuciadLightspeed also supports the loading of worldwide OpenStreetMap vector data. See the Visualizing large vector data in Lightspeed views article for more information.

Next to OpenStreetMap tile servers, you can also encounter OGC-based WMS and WMTS servers that provide OpenStreetMap data. The OGC web services articles for WMS and WMTS explain how to loading and visualizing data from such servers.

Visualizing OSM raster tiles data on a GXY view requires the same two steps as the majority of the formats:

For your convenience, LuciadLightspeed provides a ready-to-use model decoder and layer factory: samples.earth.decoder.osm.OpenStreetMapModelDecoder and samples.earth.decoder.osm.OpenStreetMapLayerFactory.

The model decoder accepts a Java properties file with .osm extension that identifies an OSM tile server by means of the base URL and a human-readable title:

Java properties file with .osm extension
title=My OpenStreetMap tile server
url=http://a.tile.openstreetmap.fr/hot

For some OSM tile servers, you must include additional HTTP headers in the tile requests: the Origin or Referer request headers to identify the system that sends the tile request, for example. You can specify those HTTP headers in the Java properties file with:

Adding HTTP headers to the Java properties file
requestProperty.Origin=originName
requestProperty.Referer=refererName
...

The model decoder and layer factory are registered as a service, so that they are automatically available to applications that rely on the service loader. An example is the decoder sample samples.gxy.decoder.MainPanel. It supports loading and visualizing .osm files by discovering these classes through the service loader.

The model decoder and layer factory are designed specifically to work with tile servers that adhere to the OSM tile URL pattern and tile structure. The code snippets below show you how to support tile servers that vary from that pattern and structure. They illustrate how you can create a raster tile model and layer. You can modify the class UrlTileSet to support:

  • Different tile URL patterns, through the method getTileUrl

  • Different tile structures, through the constructors

//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, Collections.emptyMap());

// 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 text readability in the OSM raster tiles.
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(asyncLayer);
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 java.util.Map;

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 raster tile services.
 * <p/>
 * To create an instance of this class, a url is required that specifies the base url of a tile request;
 * for instance, "http://localhost.com:8080/myTileService". Additionally, a set of request properties can be supplied
 * that need to be used to connect with the tile service.
 * <p/>
 * By default, this implementation assumes a tile service that adheres to the common OpenStreetMap raster tile service
 * scheme: a tileset defined in the Pseudo-Mercator projection with world coverage, a single top level tile and a tile
 * size of 256 by 256 pixels. Different tileset configurations can be supplied at construction time.
 */
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 Map<String, String> fRequestProperties;
  private final ILcdBounds fBounds;
  private final ILcdEarthRasterTileSetCoverage fCoverage;

  /**
   * Creates a new {@code UrlTileSet} instance for the given tile service' base url and optional set of request properties to be
   * used for the connection with the service. The tile service is expected to adhere to the common OpenStreetMap raster tile
   * service scheme: a tileset defined in the Pseudo-Mercator projection with world coverage, a single top level tile and
   * a tile size of 256 by 256 pixels.
   * @param aBaseUrl The base url of the tile service
   * @param aRequestProperties An optional set of request properties to be used for the connection with the service
   */
  public UrlTileSet(String aBaseUrl, Map<String, String> aRequestProperties) {
    this(aBaseUrl, aRequestProperties, TOP_LEVEL_ID, TOP_LEVEL_ROW_COUNT, TOP_LEVEL_COLUMN_COUNT, createWorldBounds(GEO_REFERENCE, true), GEO_REFERENCE, TILE_WIDTH, TILE_HEIGHT);
  }

  /**
   * Creates a new {@code UrlTileSet} instance for the given tile service' base url, an optional set of request properties
   * to be used for the connection with the service, and the tileset parameters used by the tile service.
   * @param aBaseUrl The base url of the tile service
   * @param aRequestProperties An optional set of request properties to be used for the connection with the service
   * @param aLevelCount The maximum number of levels in the tileset
   * @param aLevel0Rows The number of tiles in the vertical direction at the lowest level of detail
   * @param aLevel0Columns The number of tiles in the horizontal direction at the lowest level of detail
   * @param aBounds The geographic bounds of the area covered by the tileset
   * @param aReference The reference in which the geographic bounds are defined
   * @param aTileWidth The width of each tile in pixels
   * @param aTileHeight The height of each tile in pixels
   */
  public UrlTileSet(String aBaseUrl, Map<String, String> aRequestProperties, int aLevelCount, long aLevel0Rows, long aLevel0Columns,
                    ILcdBounds aBounds, ILcdGeoReference aReference, int aTileWidth, int aTileHeight) {
    super(aLevelCount, aLevel0Rows, aLevel0Columns);
    fBaseUrl = aBaseUrl.endsWith("/") ? aBaseUrl.substring(0, aBaseUrl.length() - 1) : aBaseUrl;
    fRequestProperties = aRequestProperties;
    fBounds = aBounds;
    fCoverage = new ImageCoverage(this, aReference, 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();
    for (Map.Entry<String, String> entry : fRequestProperties.entrySet()) {
      urlConnection.setRequestProperty(entry.getKey(), entry.getValue());
    }
    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/Fusion 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;
  }

  /**
   * An Earth tileset coverage representing 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 code results in an OSM Raster Tiles layer with default styling. See Visualizing Raster Data for more information about visualizing and styling raster data.