HERE Maps offers map-rendering services that provide world-wide raster data, such as aerial maps. You can use those as background data in your LuciadCPillar map application.

The HERE Maps data is structured as a quad tree with 21 levels. In short, a quad tree is a multi-leveled tile structure where a tile at a certain level is split up in 2x2 tiles at a more detailed level.

This figure illustrates the tile layout of a quad tree with 4 levels. Each level has the same model bounds. The image on the left shows the root tile at level 0. Each new level has 4 times as many tiles.

quadtree tile structure
Figure 1. Quad tree tile structure

Each of the tiles in this quad tree corresponds to an image that you can download using the HERE Maps service.

This article shows how you can do that with the LuciadCPillar API.

Step 1: Create a model with a quad-tree structure

The code snippets show how the HERE Maps model is created. It uses a quad-tree structure with a single root tile and 21 levels. Images have a resolution of 256 x 256 pixels.

Note that this snippet already mentions the HereMapsDataRetriever class. Step 2: Retrieve the HERE Maps imagery data elaborates on this class.

Program: Create a HERE Maps model
// The reference for the HERE maps tile structure.
auto pseudoMercator = CoordinateReferenceProvider::create("EPSG:3857");
if (!pseudoMercator) {
  throw luciad::RuntimeException("Cannot create Pseudo Mercator reference 'EPSG:3857'");
}
// The extent of the tile structure; which is also the extent of the data.
Bounds hereMapsBounds = Bounds(*pseudoMercator, Coordinate{-20037508.34278925, -20037508.34278925}, Coordinate{20037508.34278925, 20037508.34278925});
auto modelMetadata = ModelMetadata::newBuilder().title(toTitle(hereType)).build();

auto hereMapsDataRetriever = std::make_shared<HereMapsDataRetriever>(hereMapInfo, dpi);
return QuadTreeRasterModelBuilder::newBuilder()
    .reference(*pseudoMercator)
    .levelCount(21)
    .level0ColumnCount(1)
    .level0RowCount(1)
    .tileWidthPixels(256)
    .tileHeightPixels(256)
    .bounds(hereMapsBounds)
    .modelMetadata(modelMetadata)
    .dataRetriever(hereMapsDataRetriever)
    .build();
// The reference for the HERE maps tile structure.
var pseudoMercator = CoordinateReferenceProvider.Create("EPSG:3857");

// The extent of the tile structure; which is also the extent of the data.
var hereMapsBounds = new Bounds(pseudoMercator,
    new Coordinate(-20037508.34278925, -20037508.34278925),
    new Coordinate(20037508.34278925, 20037508.34278925));

var modelMetadata = ModelMetadata.NewBuilder().Title(ToTitle(hereType)).Build();

var hereMapsDataRetriever = new HereMapsDataRetriever(hereMapInfo, dpi);
return QuadTreeRasterModelBuilder.NewBuilder()
    .Reference(pseudoMercator)
    .Bounds(hereMapsBounds)
    .LevelCount(21)
    .Level0ColumnCount(1)
    .Level0RowCount(1)
    .TileWidthPixels(256)
    .TileHeightPixels(256)
    .ModelMetadata(modelMetadata)
    .DataRetriever(hereMapsDataRetriever)
    .Build();
// The reference for the HERE maps tile structure.
val pseudoMercator: CoordinateReference = CoordinateReferenceProvider.create("EPSG:3857")

// The extent of the tile structure; which is also the extent of the data.
val hereMapsBounds = Bounds(
    pseudoMercator,
    Coordinate(-20037508.34278925, -20037508.34278925),
    Coordinate(20037508.34278925, 20037508.34278925)
)
val modelMetadata: ModelMetadata =
    ModelMetadata.newBuilder().title(hereType.toTitle()).build()
val hereMapsDataRetriever = HereMapsDataRetriever(hereMapInfo, dpi)
return QuadTreeRasterModelBuilder.newBuilder()
    .reference(pseudoMercator)
    .levelCount(21)
    .level0ColumnCount(1)
    .level0RowCount(1)
    .tileWidthPixels(256)
    .tileHeightPixels(256)
    .bounds(hereMapsBounds)
    .modelMetadata(modelMetadata)
    .dataRetriever(hereMapsDataRetriever)
    .build()

Step 2: Retrieve the HERE Maps imagery data

The next step is to create an IMultilevelTiledRasterDataRetrieverIMultilevelTiledRasterDataRetrieverIMultilevelTiledRasterDataRetriever implementation that can return HERE Maps data for each tile. When the raster visualization engine requests data for a certain tile that’s visible on the map, the IMultilevelTiledRasterDataRetrieverIMultilevelTiledRasterDataRetrieverIMultilevelTiledRasterDataRetriever class is called.

The snippets show how this interface is implemented for HERE Maps.

For each tile for which data is requested, it:

  1. Constructs a HERE Maps URL.

  2. Performs a HTTP GET request to this URL to download the HERE Maps PNG image for the tile.

  3. Passes the encoded PNG data to the IMultilevelTiledRasterDataRetrieverCallbackIMultilevelTiledRasterDataRetrieverCallbackIMultilevelTiledRasterDataRetrieverCallback, which decodes the PNG data, and passes it on to the raster visualization engine.

  4. Handles cancellation, possible errors, or missing tile data.

These snippets don’t show details for every operation. For the full source code, see the HereMapsModelFactory class in the data formats sample.

Program: Create a HERE Maps data retriever
class HereMapsDataRetriever : public IMultilevelTiledRasterDataRetriever {
public:
  void retrieveTileData(const MultilevelTileCoordinate& tileCoordinate,
                        const CancellationToken& cancellationToken,
                        const std::shared_ptr<IMultilevelTiledRasterDataRetrieverCallback>& callback) override {
    // Create a HERE Maps url, based on the (fixed) base URL and the tile coordinate for this tile
    const std::string tileUrl = getUrl(tileCoordinate);

    // Perform a HTTP GET request to retrieve the data from the HERE Maps url
    const HttpRequest httpRequest = HttpRequest::newBuilder().uri(tileUrl).build();
    const expected<HttpResponse, ErrorInfo> httpResponse = _httpClient->send(httpRequest, cancellationToken);

    // Handle cancellation. This happens for example when a layer is removed. In that case, the data is not needed anymore
    if (cancellationToken.isCanceled()) {
      callback->onCanceled(tileCoordinate);
      return;
    }

    if (httpResponse.has_value()) {
      // Read the byte data and add it to a DataEntity
      std::optional<DataEntity> content = httpResponse->getBody();
      if (content.has_value()) {
        // Pass the data to the onDataAvailable callback, which will decode the HERE Maps PNG data
        // and pass on the decoded image to the raster painting engine.
        callback->onDataAvailable(tileCoordinate, content.value());
      } else {
        callback->onDataNotAvailable(tileCoordinate);
      }
    } else {
      // Handle errors
      const std::string message = httpResponse.get_unexpected().value().getMessage();
      callback->onError(tileCoordinate, message);
    }
  }
};
private sealed class HereMapsDataRetriever : IMultilevelTiledRasterDataRetriever
{
    public void RetrieveTileData(MultilevelTileCoordinate tileCoordinate,
        CancellationToken cancellationToken,
        IMultilevelTiledRasterDataRetrieverCallback callback)
    {

        try
        {
            // Create a HERE Maps url, based on the (fixed) base URL and the tile coordinate for this tile
            string tileUrl = GetUrl(tileCoordinate);

            // Perform a HTTP GET request to retrieve the data from the HERE Maps url
            var request = new CancellableHttpRequest(tileUrl);
            cancellationToken.RunTask(request);

            var response = request.Response;
            if (response != null)
            {
                var status = response.StatusCode;
                if (status == HttpStatusCode.OK)
                {
                    // Read the byte data and add it to a DataEntity
                    using (MemoryStream memoryStream = new MemoryStream())
                    {
                        response.GetResponseStream().CopyTo(memoryStream);
                        var byteBuffer = new ByteBuffer(memoryStream.ToArray());
                        DataEntity dataEntity = new DataEntity(byteBuffer, response.ContentType);

                        // Pass the data to the onDataAvailable callback, which will decode the HERE Maps PNG data
                        // and pass on the decoded image to the raster painting engine.
                        callback.OnDataAvailable(tileCoordinate, dataEntity);
                    }
                }
                else
                {
                    // Handle errors
                    if (status == HttpStatusCode.NotFound)
                    {
                        callback.OnDataNotAvailable(tileCoordinate);
                    }
                    else
                    {
                        callback.OnError(tileCoordinate, response.StatusDescription);
                    }
                }
            }
            else
            {
                // Request was canceled
                callback.OnCanceled(tileCoordinate);
            }
        }
        catch (Exception exception)
        {
            // Handle errors
            callback.OnError(tileCoordinate, exception.Message);
        }
    }
}
/**
 * Raster data retriever implementation to get the raster tiles from the HERE maps.
 */
private class HereMapsDataRetriever
/**
 * Constructs the HERE maps data retriever with the info _parameters_ to fill in the template URL.
 */(
    override fun retrieveTileData(
        tileCoordinate: MultilevelTileCoordinate,
        cancellationToken: CancellationToken,
        callback: IMultilevelTiledRasterDataRetrieverCallback
    ) {
        // Create a HERE Maps url, based on the (fixed) base URL and the tile coordinate for this tile
        val tileUrl = getUrl(tileCoordinate)
        val url = URL(tileUrl)

        // Perform a HTTP GET request to retrieve the data from the HERE Maps url
        val urlConnection = url.openConnection() as HttpURLConnection

        try {
            urlConnection.requestMethod = "GET"
            urlConnection.connect()

            cancellationToken.runTask(object : ICancellableTask {
                override fun run() {
                    try {
                        when (urlConnection.responseCode) {
                            200 -> {
                                val mimeType = urlConnection.getHeaderField("content-type")
                                if (mimeType.isEmpty()) {
                                    callback.onError(
                                        tileCoordinate,
                                        "No content-type header available for [$tileUrl]"
                                    )
                                    return
                                }
                                val responseBody = urlConnection.inputStream.use { inputStream ->
                                        inputStreamToBytes(inputStream)
                                }
                                val dataEntity =
                                    DataEntity(ByteBuffer(responseBody), mimeType)

                                // Pass the data to the onDataAvailable callback, which will decode the HERE Maps PNG
                                // data and pass on the decoded image to the raster painting engine.
                                callback.onDataAvailable(tileCoordinate, dataEntity)
                            }
                            404 -> {
                                callback.onDataNotAvailable(tileCoordinate)
                            }
                            else -> {
                                // Handle errors
                                val responseBody = urlConnection.errorStream.use { inputStream ->
                                    inputStreamToBytes(inputStream)
                                }
                                callback.onError(tileCoordinate, String(responseBody))
                            }
                        }
                    } catch (e : SocketException) {
                        if (cancellationToken.isCanceled) {
                            callback.onCanceled(tileCoordinate)
                        }
                    }
                }

                override fun cancel() {
                    // Handle cancellation. This happens for example when a layer is removed.
                    // In that case, the data is not needed anymore
                    urlConnection.disconnect()
                }
            })

        } catch (e: IOException) {
            // Handle errors
            val message = """
                ${e.message}
                ${Arrays.toString(e.stackTrace)}
                """.trimIndent()
            callback.onError(tileCoordinate, java.lang.String.join("\n", message))
        } finally {
            urlConnection.disconnect()
        }
    }
}

Step 3: Getting a HERE Maps API key

The next thing you need before connecting to HERE Maps is a HERE API key. On the HERE developer portal, you can create a HERE account and one or more keys.

To create a key for HERE Maps:

  1. Go to https://developer.here.com/.

  2. Log in with your HERE account, or sign up for a new account.

    1. If you sign up for a new account, select a plan that fits your project. The default is the Freemium plan.
      Once you have logged on, you see a Projects page that lists all your projects and the corresponding HERE plan. For new accounts with a Freemium plan, the Projects page shows one project called Freemium and the project creation date.

  3. Select the project from the projects page.

  4. On the Project Details page, you can create keys. In the REST section, click Generate App.
    The portal generates an APP ID.

  5. Click Create API key.
    The portal generates a key. Click the Copy button to copy/paste it in your code.

Note that more terms may apply for deployment. For more information about HERE licensing, see the HERE Terms and Conditions.

Step 4: Add the HERE Maps model to the map

The final step is adding the HERE Maps model to the map:

Program: Add the HERE Maps model to the map
const auto map = _mapObject->getMap();

// Create a model for HereMaps using the API Key
auto hereMapsModel = HereMapsModelFactory::createHereModel(hereType, map->getDpi(), hereMapsApiKey.toStdString());

// Create a layer for the model using the layer builder
auto hereMapsLayer = RasterLayer::newBuilder().model(hereMapsModel).build();

// Add the layer to the map
map->getLayerList()->add(hereMapsLayer);
LayerList layerList = Map.LayerList;

// Create a model for HereMaps using the API Key
IRasterModel hereMapsModel = HereMapsModelFactory.CreateHereModel(hereMapType, Map.Dpi, ApiKey);

// Create a layer for the model using the layer builder
RasterLayer hereMapsLayer = RasterLayer.NewBuilder().Model(hereMapsModel).Build();

// Add the layer to the map
layerList.Add(hereMapsLayer);
// Create a model for HereMaps using the API Key
val hereMapsModel = HereMapsModelFactory.createHereModel(type, map.dpi, apiKey)

// Create a layer for the model using the layer builder
val hereMapslayer = RasterLayer.newBuilder().model(hereMapsModel).build()

// Add the layer to the map
map.layerList.add(hereMapslayer)

The tutorial Introduction to styling raster data explains the styling options you have on the raster layer.