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.
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.
// 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 hereMapsAttributionProvider = createAttributionProvider(hereType, apiKey);
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)
.attributionProvider(hereMapsAttributionProvider)
.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);
var hereMapsAttributionProvider = CreateAttributionProvider(hereType, apiKey);
return QuadTreeRasterModelBuilder.NewBuilder()
.Reference(pseudoMercator)
.Bounds(hereMapsBounds)
.LevelCount(21)
.Level0ColumnCount(1)
.Level0RowCount(1)
.TileWidthPixels(256)
.TileHeightPixels(256)
.ModelMetadata(modelMetadata)
.DataRetriever(hereMapsDataRetriever)
.AttributionProvider(hereMapsAttributionProvider)
.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)
val hereMapsAttributionProvider = createAttributionProvider(hereType, apiKey)
return QuadTreeRasterModelBuilder.newBuilder()
.reference(pseudoMercator)
.levelCount(21)
.level0ColumnCount(1)
.level0RowCount(1)
.tileWidthPixels(256)
.tileHeightPixels(256)
.bounds(hereMapsBounds)
.modelMetadata(modelMetadata)
.dataRetriever(hereMapsDataRetriever)
.attributionProvider(hereMapsAttributionProvider)
.build()
Step 2: Retrieve the HERE Maps imagery data
The next step is to create an IMultilevelTiledRasterDataRetriever
IMultilevelTiledRasterDataRetriever
IMultilevelTiledRasterDataRetriever
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 IMultilevelTiledRasterDataRetriever
IMultilevelTiledRasterDataRetriever
IMultilevelTiledRasterDataRetriever
class is called.
The snippets show how this interface is implemented for HERE Maps.
For each tile for which data is requested, it:
-
Constructs a HERE Maps URL.
-
Performs a HTTP GET request to this URL to download the HERE Maps PNG image for the tile.
-
Passes the encoded PNG data to the
IMultilevelTiledRasterDataRetrieverCallback
IMultilevelTiledRasterDataRetrieverCallback
IMultilevelTiledRasterDataRetrieverCallback
, which decodes the PNG data, and passes it on to the raster visualization engine. -
Handles cancellation, possible errors, or missing tile data.
These snippets don’t show details for every operation. For the full source code, see the |
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:
-
Go to https://developer.here.com/.
-
Log in with your HERE account, or sign up for a new account.
-
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.
-
-
Select the project from the projects page.
-
On the Project Details page, you can create keys. In the
REST
section, click Generate App.
The portal generates an APP ID. -
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:
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.
Getting the HERE Maps attributions
LuciadCPillar exposes the attributions on a map through |