Google 3D Tiles is a photorealistic globe-covering dataset. It follows the OGC 3D Tiles format 1.0 specification, and has a GLB payload. It contains detailed textured 3D mesh data with high-resolution imagery for over 2000 cities.

To find out the exact coverage of the 3D data, open Google Earth and activate the 3D Coverage layer. The uncovered areas contain 3D-textured elevation-like mesh data.

Loading and using Google 3D Tiles in LuciadCPillar will give you a Google Earth look and feel, but the Google Map Tiles API for Photorealistic 3D Tiles may provide tiles at a lower resolution than what you experience in Google products.

google3DTiles LeuvenOffice
Figure 1. A LuciadCPillar view of the Leuven Hexagon office in Google 3D Tiles.

Visualize Google 3D Tiles

To show Google 3D Tiles on a LuciadCPillar map, you must access the Google 3D Tiles API to create a session, create a model for the Google 3D Tiles data, and visualize that model in a map layer.

Create a session

To start working with Google 3D Tiles, you must get an API key from Google. With this key, you can fetch a session ID and a root URL as illustrated in Program: Get a rootURL and sessionID to start working with Google 3D Tiles.

Each root tile request counts against your daily quota. Further requests don’t count. The root request results in a session, which is valid for two hours. We advise to re-use this session by saving the necessary information as a local environment variable onto your system or to a file. This way you’ll be able to automatically renew the sessionID when it expires.

Expand for Program: Get a rootURL and sessionID to start working with Google 3D Tiles:

Program: Get a rootURL and sessionID to start working with Google 3D Tiles.
struct Google3DTilesInfo {
  bool valid = false;
  std::string apiKey;
  std::string rootUrl;
  std::string sessionId;
  long long int validUntil = -1;
};

const long long SessionValiditySeconds = std::chrono::seconds(2 * 60 * 60).count();

/**
 * Calling this method counts against your daily quota.
 * You should consider using a local storage (file) solution to store your
 * Google3DTilesInfo. If the key in that info is the same as the one provided here,
 * you can check the validity of the sessionID, and only request a new one if the
 * saved one has expired.
 */
luciad::expected<HttpResponse, ErrorInfo> getGoogleResponse(const std::string& apiKey) {
  std::string apiKeyVerifyUrl = "https://tile.googleapis.com/v1/3dtiles/root.json?key=${apiKey}";
  apiKeyVerifyUrl = StringUtils::replace(apiKeyVerifyUrl, "${apiKey}", apiKey);

  auto httpClient = HttpClient::newBuilder().build();
  auto request = HttpRequest::newBuilder().uri(apiKeyVerifyUrl).build();
  return httpClient->send(request);
}

Google3DTilesInfo getGoogle3DTilesInfo(const std::string& apiKey) {
  if (apiKey.empty()) {
    return Google3DTilesInfo{};
  }

  auto response = getGoogleResponse(apiKey);
  auto responseBody = response->getBody();

  if (responseBody.has_value()) {
    auto byteBuffer = responseBody->getData();

    rapidjson::Document document;
    document.Parse<rapidjson::kParseStopWhenDoneFlag>((const char*)byteBuffer.data(), byteBuffer.size());

    // When requesting information with an invalid apiKey, the document will not have a "root" member.
    if (!document.HasParseError() && document.IsObject() && document.HasMember("root")) {
      auto result = Google3DTilesInfo{};
      result.valid = true;
      result.apiKey = apiKey;

      // The root-url-extension (with session id) can be found at:
      // docRoot["root"]["children"][0]["children"][0]["content"]["uri"]
      // it will look like "/v1/3dtiles/datasets/CgA/files/<rootID>.json?session=<sessionID>"
      auto uri = (std::string)(document["root"]["children"][0]["children"][0]["content"]["uri"].GetString());
      std::string sessionQuery = "?session=";
      auto sessionQueryPosition = uri.find(sessionQuery);
      auto sessionPosition = sessionQueryPosition + sessionQuery.size();
      result.sessionId = uri.substr(sessionPosition, uri.size() - sessionPosition);

      // Now we prepare the root-url for further processing
      std::string baseURL = "https://tile.googleapis.com";
      auto rootUrlExtension = uri.substr(0, sessionQueryPosition);
      result.rootUrl = baseURL;
      result.rootUrl += rootUrlExtension;

      // Add the validity time (2 hours as of now)
      auto currentTime = std::chrono::system_clock::now();
      auto timeSinceEpochSeconds = std::chrono::duration_cast<std::chrono::seconds>(currentTime.time_since_epoch()).count();
      result.validUntil = timeSinceEpochSeconds + SessionValiditySeconds;

      return result;
    }
  }
  return Google3DTilesInfo{};
}
public class Google3DTilesInfo
{
    public Boolean valid = false;
    public long validUntil = -1;
    public string apiKey = "";
    public string rootUrl = "";
    public string sessionId = "";
}

static readonly int SESSION_VALIDITY_SECONDS = 2 * 60 * 60;

/**
 * Calling this method counts against your daily quota.
 * You should consider using a local storage (file) solution to store your
 * Google3DTilesInfo. If the key in that info is the same as the one provided here,
 * you can check the validity of the sessionID, and only request a new one if the
 * saved one has expired.
 */
public static HttpResponse GetGoogleResponse(string apiKey)
{
    string apiKeyVerifyUrl = "https://tile.googleapis.com/v1/3dtiles/root.json?key=${apiKey}";
    apiKeyVerifyUrl = apiKeyVerifyUrl.Replace("${apiKey}", apiKey);

    var httpClient = HttpClient.NewBuilder().Build();
    var request = HttpRequest.NewBuilder().Uri(apiKeyVerifyUrl).Build();
    return httpClient.Send(request);
}

public static Google3DTilesInfo GetGoogle3DTilesInfo(string apiKey)
{
    if (apiKey.Length == 0)
    {
        return new Google3DTilesInfo();
    }

    try
    {
        var response = GetGoogleResponse(apiKey);
        var body = response.Body;
        if (body == null || response.StatusCode != 200)
        {
            return new Google3DTilesInfo();
        }

        Google3DTilesInfo result = new Google3DTilesInfo
        {
            valid = true,
            apiKey = apiKey
        };

        // The root-url-extension (with session id) can be found at:
        // docRoot["root"]["children"][0]["children"][0]["content"]["uri"]
        // it will look like "/v1/3dtiles/datasets/CgA/files/<rootID>.json?session=<sessionID>"
        // After json-parsing, the uri could be found like this:
        // string uri = (String)(document["root"]["children"][0]["children"][0]["content"]["uri"].GetString());
        // But, we can also find the uri without actual JSON-parser
        string json = Encoding.UTF8.GetString(body.Data.Data);
        int startOfUriIdx = json.IndexOf("uri") + 6;
        int endOfUriIdx = json.IndexOf('"', startOfUriIdx);
        string uri = json.Substring(startOfUriIdx, endOfUriIdx - startOfUriIdx);

        string sessionQuery = "?session=";
        int sessionQueryPosition = uri.IndexOf(sessionQuery);
        int sessionIdIdx = sessionQueryPosition + sessionQuery.Length;
        result.sessionId = uri.Substring(sessionIdIdx);

        // Now we prepare the root-url for further processing
        string baseURL = "https://tile.googleapis.com";
        string rootUrlExtension = uri.Substring(0, sessionQueryPosition);
        result.rootUrl = baseURL + rootUrlExtension;

        // Add the validity time
        DateTime Jan1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        TimeSpan javaSpan = DateTime.UtcNow - Jan1970;
        long currentTimeInSeconds = (long)javaSpan.TotalMilliseconds / 1000;
        result.validUntil = currentTimeInSeconds + SESSION_VALIDITY_SECONDS;

        return result;
    }
    catch (IOException)
    {
        return new Google3DTilesInfo();
    }
}
static class Google3DTilesInfo {
  boolean valid = false;
  long validUntil = -1;
  String apiKey = "";
  String rootUrl = "";
  String sessionId = "";
}

static int SESSION_VALIDITY_SECONDS = 2 * 60 * 60;

/**
 * Calling this method counts against your daily quota.
 * You should consider using a local storage (file) solution to store your
 * Google3DTilesInfo. If the key in that info is the same as the one provided here,
 * you can check the validity of the sessionID, and only request a new one if the
 * saved one has expired.
 */
static HttpResponse getGoogleResponse(String apiKey) throws IOException {
  String apiKeyVerifyUrl = "https://tile.googleapis.com/v1/3dtiles/root.json?key=${apiKey}";
  apiKeyVerifyUrl = apiKeyVerifyUrl.replace("${apiKey}", apiKey);

  HttpClient httpClient = HttpClient.newBuilder().build();
  HttpRequest request = HttpRequest.newBuilder().uri(apiKeyVerifyUrl).build();
  return httpClient.send(request);
}

static Google3DTilesInfo getGoogle3DTilesInfo(String apiKey) {
  if (apiKey.isEmpty()) {
    return new Google3DTilesInfo();
  }

  try {
    HttpResponse response = getGoogleResponse(apiKey);
    DataEntity body = response.getBody();
    if (body == null) {
      return new Google3DTilesInfo();
    }

    Google3DTilesInfo result = new Google3DTilesInfo();
    result.valid = true;
    result.apiKey = apiKey;

    // The root-url-extension (with session id) can be found at:
    // docRoot["root"]["children"][0]["children"][0]["content"]["uri"]
    // it will look like "/v1/3dtiles/datasets/CgA/files/<rootID>.json?session=<sessionID>"
    // After json-parsing, the uri could be found like this:
    // String uri = (String)(document["root"]["children"][0]["children"][0]["content"]["uri"].GetString());
    // But, we can also find the uri without actual JSON-parser
    String json = new String(body.getData().getData());
    int startOfUriIdx = json.indexOf("uri") + 6;
    int endOfUriIdx = json.indexOf('"', startOfUriIdx);
    String uri = json.substring(startOfUriIdx, endOfUriIdx);

    String sessionQuery = "?session=";
    int sessionQueryPosition = uri.indexOf(sessionQuery);
    int sessionIdIdx = sessionQueryPosition + sessionQuery.length();
    result.sessionId = uri.substring(sessionIdIdx);

    // Now we prepare the root-url for further processing
    String baseURL = "https://tile.googleapis.com";
    String rootUrlExtension = uri.substring(0, sessionQueryPosition);
    result.rootUrl = baseURL + rootUrlExtension;

    // Add the validity time
    result.validUntil = (System.currentTimeMillis() / 1000) + SESSION_VALIDITY_SECONDS;

    return result;
  } catch (IOException e) {
    return new Google3DTilesInfo();
  }
}

The snippets in this section include calls to functions that are explained later in this article.

Create a custom HttpClient

To be able to auto-renew the sessionID, you could create your own custom HttpClientHttpClientHttpClient, that catches failing requests, renews the sessionID and resends it. This is illustrated in Program: Create custom Google HttpClient.

Expand for Program: Create custom Google HttpClient:

Program: Create custom Google HttpClient.
class GoogleHttpClient final : public IHttpClient {
public:
  explicit GoogleHttpClient(std::string apiKey) : _apiKey(std::move(apiKey)), _httpClient(HttpClient::newBuilder().build()) {
    _googleInfo = getGoogle3DTilesInfo(_apiKey);
    _httpClient->setHttpRequestOptions(getGoogleRequestOptions());
  }

  std::string getRootURL() const {
    return _googleInfo.rootUrl;
  }

  luciad::expected<HttpResponse, ErrorInfo> send(const HttpRequest& request, const CancellationToken& token) override {
    auto response = _httpClient->send(request, token);

    // Check if the response is successful, if not, check if the sessionID needs renewal (no longer valid)
    if (response.has_value() && response->getStatusCode() != 200 && !isGoogleInfoValid()) {
      // renew the sessionID and try again
      _googleInfo = getGoogle3DTilesInfo(_apiKey);
      _httpClient->setHttpRequestOptions(getGoogleRequestOptions());
      response = _httpClient->send(request, token);
    }

    return response;
  }

private:
  HttpRequestOptions getGoogleRequestOptions() const {
    auto googleRequestOptionBuilder = HttpRequestOptions::newBuilder();
    googleRequestOptionBuilder.queryParameter("key", _googleInfo.apiKey);
    googleRequestOptionBuilder.queryParameter("session", _googleInfo.sessionId);
    return googleRequestOptionBuilder.build();
  }

  bool isGoogleInfoValid() const {
    auto currentTime = std::chrono::system_clock::now();
    auto currentTimeS = (std::chrono::duration_cast<std::chrono::seconds>(currentTime.time_since_epoch())).count();
    return currentTimeS < _googleInfo.validUntil;
  }

  std::string _apiKey;
  Google3DTilesInfo _googleInfo;
  std::shared_ptr<HttpClient> _httpClient;
};
public class GoogleHttpClient : IHttpClient
{

    private readonly string _apiKey;
    private Google3DTilesInfo _googleInfo;
    private readonly HttpClient _httpClient;

    public GoogleHttpClient(string apiKey)
    {
        _apiKey = apiKey;
        _googleInfo = GetGoogle3DTilesInfo(_apiKey);
        _httpClient = HttpClient.NewBuilder().Build();
        _httpClient.HttpRequestOptions = GetGoogleRequestOptions(_googleInfo);
    }

    public string GetRootUrl()
    {
        return _googleInfo.rootUrl;
    }

    public HttpResponse Send(HttpRequest request, CancellationToken token)
    {
        var response = _httpClient.Send(request, token);

        if (response.StatusCode != 200 && !IsGoogleInfoValid(_googleInfo))
        {
            // renew the sessionID and try again
            _googleInfo = GetGoogle3DTilesInfo(_apiKey);
            _httpClient.HttpRequestOptions = GetGoogleRequestOptions(_googleInfo);
            response = _httpClient.Send(request, token);
        }

        return response;
    }

    static Boolean IsGoogleInfoValid(Google3DTilesInfo google3DTilesInfo)
    {
        DateTime Jan1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        TimeSpan javaSpan = DateTime.UtcNow - Jan1970;
        long currentTimeInSeconds = (long)javaSpan.TotalMilliseconds / 1000;
        return google3DTilesInfo.valid && currentTimeInSeconds < google3DTilesInfo.validUntil;
    }

    private HttpRequestOptions GetGoogleRequestOptions(Google3DTilesInfo google3DTilesInfo)
    {
        return HttpRequestOptions.NewBuilder()
                .QueryParameter("key", google3DTilesInfo.apiKey)
                .QueryParameter("session", google3DTilesInfo.sessionId)
                .Build();
    }
};
static class GoogleHttpClient implements IHttpClient {

  private final String _apiKey;
  private final HttpClient _httpClient;
  private Google3DTilesInfo _googleInfo;

  GoogleHttpClient(String apiKey) {
    _apiKey = apiKey;
    _googleInfo = getGoogle3DTilesInfo(apiKey);
    _httpClient = HttpClient.newBuilder().build();
    _httpClient.setHttpRequestOptions(getGoogleRequestOptions());
  }

  public String getRootUrl() {
    return _googleInfo.rootUrl;
  }

  @Override
  public @NotNull HttpResponse send(@NotNull HttpRequest request, @NotNull CancellationToken token) throws IOException {
    HttpResponse response = _httpClient.send(request, token);

    if (response.getStatusCode() != 200 && !isGoogleInfoValid()) {
      // renew the sessionID and try again
      _googleInfo = getGoogle3DTilesInfo(_apiKey);
      _httpClient.setHttpRequestOptions(getGoogleRequestOptions());
      response = _httpClient.send(request, token);
    }

    return response;
  }

  private HttpRequestOptions getGoogleRequestOptions() {
    return HttpRequestOptions.newBuilder()
            .queryParameter("key", _apiKey)
            .queryParameter("session", _googleInfo.sessionId)
            .build();
  }

  private boolean isGoogleInfoValid() {
    return _googleInfo.valid && (System.currentTimeMillis() / 1000) < _googleInfo.validUntil;
  }
}

Create the model

You can use the custom GoogleHttpClient to build an OGC3DTilesModel as illustrated in Program: Create a model with the correct request parameters.

Expand for Program: Create a model with the correct request parameters:

Program: Create a model with the correct request parameters.
luciad::expected<std::shared_ptr<ITileSet3DModel>, ErrorInfo> createGoogle3dTilesModel(std::string apiKey) {
  // Create a custom GoogleHttpClient that auto-renews the session if needed
  auto googleHttpClient = std::make_shared<GoogleHttpClient>(GoogleHttpClient(std::move(apiKey)));

  // Create the model
  auto options = Ogc3DTilesModelDecoder::Options::newBuilder().httpClient(googleHttpClient).build();
  return Ogc3DTilesModelDecoder::decode(googleHttpClient->getRootURL(), options);
}
public static ITileSet3DModel CreateGoogle3dTilesModel(string apiKey)
{

    // Create a custom GoogleHttpClient that auto-renews the session if needed
    GoogleHttpClient googleHttpClient = new GoogleHttpClient(apiKey);

    // Create the model
    var options = Ogc3DTilesModelDecoder.Options.NewBuilder()
        .HttpClient(googleHttpClient)
        .Build();
    return Ogc3DTilesModelDecoder.Decode(googleHttpClient.GetRootUrl(), options);

}
static ITileSet3DModel createGoogle3dTilesModel(String apiKey) throws IOException {
  // Create a custom GoogleHttpClient that auto-renews the session if needed
  GoogleHttpClient googleHttpClient = new GoogleHttpClient(apiKey);

  // Create the model
  Ogc3DTilesModelDecoder.Options options = Ogc3DTilesModelDecoder.Options.newBuilder()
          .httpClient(googleHttpClient).build();
  return Ogc3DTilesModelDecoder.decode(googleHttpClient.getRootUrl(), options);
}

Create the layer

To visualize the Google 3D Tiles model, you must create a TileSet3DLayerTileSet3DLayerTileSet3DLayer for it. When you are creating that layer, you can set several layer properties. For optimal layer behavior, you must define a quality factorquality factorquality factor of 0.125, as illustrated in Program: Create a layer for your Google 3D Tiles model.

Expand for Program: Create a layer for your Google 3D Tiles model:

Program: Create a layer for your Google 3D Tiles model.
auto model = createGoogle3dTilesModel("<yourAPIKey>");
auto layer = TileSet3DLayer::newBuilder().title("Google 3D Tiles").model(*model).qualityFactor(0.125).build();
var model = Google3DTilesUtil.CreateGoogle3dTilesModel("<yourAPIKey>");
TileSet3DLayer.NewBuilder().Title("Google 3D Tiles").Model(model).QualityFactor(0.125).Build();
ITileSet3DModel model = Google3DTilesUtil.createGoogle3dTilesModel("<yourAPIKey>");
TileSet3DLayer.newBuilder().title("Google 3D Tiles").model(model).qualityFactor(0.125).build();

Configure the map for Google 3D Tiles

The Google 3D Tiles dataset is globe-covering and has the correct elevation baked in its meshes. Because of the particular nature of this dataset, you must take specific measures when you add it to the LuciadCPillar map.

By default, LuciadCPillar visualizes a 3D map with a globe of a certain color. This colored globe, an ellipsoid, intersects with your Google 3D Tiles data. To alleviate that problem, you can override the default color with any fully transparent colorfully transparent colorfully transparent color (alpha == 0).

As soon as you start loading layers with imagery data, they are draped on the globe, and the globe will no longer be transparent. These layers will break through your Google 3D Tiles data.

Convenience methods for working with Google 3D Tiles

Google API key validation

You can use this code to check the validity of a Google API key. Program: Validate the Google API key:

Expand for Program: Validate the Google API key:

Program: Validate the Google API key.
enum class ApiKeyStatus { NeedKey, Valid, Invalid };

ApiKeyStatus verifyApiKey(const std::string& apiKey) {
  if (apiKey.empty()) {
    return ApiKeyStatus::NeedKey;
  }

  auto response = getGoogleResponse(apiKey);

  if (response.has_value() && response->getStatusCode() == 200) {
    return ApiKeyStatus::Valid;
  } else {
    return ApiKeyStatus::Invalid;
  }
}
public enum ApiKeyStatus { NeedKey, Valid, Invalid }

public static ApiKeyStatus VerifyApiKey(string apiKey)
{
    if (apiKey.Length == 0)
    {
        return ApiKeyStatus.NeedKey;
    }

    try
    {
        HttpResponse response = GetGoogleResponse(apiKey);
        if (response.StatusCode == 200)
        {
            return ApiKeyStatus.Valid;
        }
        else
        {
            return ApiKeyStatus.Invalid;
        }
    }
    catch (IOException)
    {
        return ApiKeyStatus.Invalid;
    }
}
enum ApiKeyStatus {NeedKey, Valid, Invalid}

static ApiKeyStatus verifyApiKey(@NotNull String apiKey) {
  if (apiKey.isEmpty()) {
    return ApiKeyStatus.NeedKey;
  }

  try {
    HttpResponse response = getGoogleResponse(apiKey);
    if (response.getStatusCode() == 200) {
      return ApiKeyStatus.Valid;
    } else {
      return ApiKeyStatus.Invalid;
    }
  } catch (IOException e) {
    return ApiKeyStatus.Invalid;
  }
}

Add the Google attributions

Google requires you to show the correct attributions and a logo as an overlay on the map.

Overlay tileset attributions

The attributions for Google 3D Tiles vary depending on the tiles that you have in your view. Each loaded tile has its own copyright information. The Google Map Tiles API policies specify that you must collect, sort, and aggregate all that information. LuciadCPillar does that for you automatically.

Get the attributions on screen

LuciadCPillar exposes the attributions on a map through MapAttributionsMapAttributionsMapAttributions. You can get attributions from all layers, or request the attributions of a specific layer. For more information, see How to provide and retrieve attribution data