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.

Next to OpenStreetMap tile servers, you can also encounter OGC-based servers (WMS and WMTS) on the Internet that provide OpenStreetMap data. Loading and visualizing data from such servers is explained in the OGC web services articles for WMS and WMTS.

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

//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
//Adjust the level switch factor and layer type to optimize
//the OSM raster tile quality
TLspRasterStyle rasterStyle = TLspRasterStyle.newBuilder().levelSwitchFactor(0.3D).build();
ILspLayer layer = TLspRasterLayerBuilder.newBuilder()
                                        .layerType(ILspLayer.LayerType.INTERACTIVE)
                                        .styler(TLspPaintRepresentationState.REGULAR_BODY, rasterStyle)
                                        .model(model)
                                        .build();

//Add the layer to the Lightspeed view (an ILspView)
view.addLayer(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.