This tutorial explains the basics of working with LuciadCPillar:

  • Initializing the LuciadCPillar environment

  • Creating a map

  • Adding data

The sample_firstapp sample supports this tutorial.

Configuring the library through global initialization

Before you can use the LuciadCPillar library, you need to define a global configuration. You can configure global settings for the library by initializing an EnvironmentEnvironmentEnvironment. While you are interacting with the library, you must hold a reference to that EnvironmentEnvironmentEnvironment object. The lifetime of this object determines when the functionality of the library is available.

Creating and disposing the Environment

You use a builder pattern to create the EnvironmentEnvironmentEnvironment, through Environment::createInitializer()Environment::createInitializer()Environment::createInitializer(). Typically you need to configure the following:

  • the contents of the license file

  • the location to the LuciadCPillar resources. You can find those in the cpp/resources folder of the release.

When the application is closed, you must dispose of the created environment. In C++, the destructor is automatically invoked when the environment object goes out of scope. Please make sure that a reference to the environment is kept while the application uses the LuciadCPillar API.

For example:

Program: Initialize and disposing an environment
std::string licenseText = readLicenseText();
std::shared_ptr<Environment>
    environment = Environment::createInitializer().withLicenseText(std::move(licenseText)).withResourcePath("resources").initialize();
private Environment env;

void OnStartup(object sender, StartupEventArgs e)
{
    string licenseText = LicenseTextReader.ReadLicenseText();
    env = Environment.CreateInitializer()
        .WithLicenseText(licenseText)
        .WithResourcePath("resources")
        .Initialize();

}

void OnExit(object sender, ExitEventArgs e)
{
    env.Dispose();
}
        val licenseText = License.getText(application)
        if (!FileSystem.extractDir(this, "resources")) {
            throw RuntimeException("Could not find resources in package.")
        }
        environment = com.luciad.Environment.createInitializer()
            .withResourcePath(FileSystem.getPath(this).resolve("resources").toString())
            .withLicenseText(licenseText)
            .withLoggingBackend(DefaultLoggingBackend())
            .initialize()

override fun onDestroy() {
    environment?.close()
    super.onDestroy()
}

Setting global configuration options

The initializer allows you to set global configuration options for the library, so that you can replace some default behavior:

  • You can plug in a logging framework. See Logging for more information.

  • You can decide whether to initialize and tear down the curl library for network requests.

Creating a map and visualizing geographical data

To visualize geographic data, you need to create a map.

Next, you typically need to integrate the map into an UI framework.

  • For C++: the LuciadCPillar samples integrate the map with QtQuick by default. A specific Qt Widgets integration sample is provided.

  • For C#: the LuciadCPillar samples integrate with WPF by default.

Creating the map

When creating the map, you must pass a spatial reference that will be used to display the data. All data will be transformed to this reference on-the-fly. For a 3D view, you must specify a geocentric reference, for example EPSG:4978. For a 2D view, you can pick another, non-geodetic reference. The map is created using the builder pattern.

Program: Creating the map
luciad::expected<std::shared_ptr<CoordinateReference>, ErrorInfo> reference = CoordinateReferenceProvider::create("EPSG:4978");
if (!reference) {
  std::cerr << "Could not create the coordinate reference: " << reference.error() << std::endl;
  std::exit(EXIT_FAILURE);
}
std::shared_ptr<Map> map = Map::newBuilder().reference(*reference).build();
// Create a Map with a 3D Map reference
CoordinateReference mapReference = CoordinateReferenceProvider.Create("EPSG:4978");
Debug.Assert(mapReference != null, "Could not create the coordinate reference.");
Map map = Map.NewBuilder().Reference(mapReference).Build();
// create the Map
val crs by lazy { if (is3D) CRS3D else CRS2D }
val coordinateReference by lazy { CoordinateReferenceProvider.create(crs) }
val map by lazy { Map.newBuilder().reference(coordinateReference).build() }

Visualizing data

To visualize data, you must take these steps:

  1. Create a model for the data

  2. Create a layer for that model

  3. Add the layer to the map

You typically create a model by decoding a data source. LuciadCPillar offers out-of-the-box functionality to create models from data in certain formats, such as the GeoPackageModelDecoderGeoPackageModelDecoderGeoPackageModelDecoder for the GeoPackage format. If necessary, you can add support for other formats yourself.

Decoding and visualizing raster data

Raster data consists of matrices of pixels, or grid cells. Models containing raster data implement the IRasterModelIRasterModelIRasterModel interface.

To visualize a raster model, you must create a RasterLayerRasterLayerRasterLayer. You can use a builder pattern to create the layer.

Finally, you can show the layer on the map by adding it to the layer list of the map.

Program: Adding a raster layer
void addRasterLayer(const std::shared_ptr<Map>& map, const std::string& fileName) {
  std::shared_ptr<GeoPackageDataSource> datasource = GeoPackageDataSource::newBuilder().source(fileName).build();
  luciad::expected<std::shared_ptr<Model>, ErrorInfo> model = GeoPackageModelDecoder::decode(datasource);
  if (!model) {
    ErrorInfo& errorInfo = model.error();
    std::cerr << "Cannot load GeoPackage file (raster) " + fileName + ": " + errorInfo.getMessage() << std::endl;
    return;
  }

  std::shared_ptr<IRasterModel> rasterModel = std::dynamic_pointer_cast<IRasterModel>(*model);
  if (!rasterModel) {
    std::cerr << "Not a raster model" << std::endl;
    return;
  }
  std::shared_ptr<RasterLayer> rasterLayer = RasterLayer::newBuilder().model(rasterModel).build();
  map->getLayerList()->add(rasterLayer);
}
private void AddRasterLayer(String fileName, Map map)
{
    try
    {
        var datasource = GeoPackageDataSource.NewBuilder().Source(fileName).Build();
        Model model = GeoPackageModelDecoder.Decode(datasource);
        RasterLayer rasterLayer = RasterLayer.NewBuilder().Model(model as IRasterModel).Build();
        map.LayerList.Add(rasterLayer);
    }
    catch (Exception exception)
    {
        string errorMessage = $"Cannot load data from file '{fileName}': {exception.Message}";
        MessageBox.Show(errorMessage);
    }
}
private fun addRasterLayer(map: Map, fileName: String) {
    val dataSource = GeoPackageDataSource.newBuilder().source(fileName).build()
    val model = GeoPackageModelDecoder.decode(dataSource)
    if (model is IRasterModel) {
        val rasterLayer = RasterLayer.newBuilder().model(model).build()
        map.layerList.add(rasterLayer)
    } else {
        throw RuntimeException("Not a raster model")
    }
}

Decoding and visualizing vector data

Vector data consists of objects that have a geometry and typically some extra object information in the attributes, such as name, type and so on. Those types of objects are called features, and are represented in LuciadCPillar by the FeatureFeatureFeature class. Models containing features implement the IFeatureModelIFeatureModelIFeatureModel interface.

To visualize a feature model, you need a FeatureLayerFeatureLayerFeatureLayer. When you pass the model to the layer, you must also provide an instance of IFeaturePainterIFeaturePainterIFeaturePainter. This painter determines how the features will be visualized. In the samples, a simple implementation, called the RiverPainter, is used to visualize river features.

Again, you can show the layer on the map by adding it to the layer list of the map.

Program: Adding a feature layer
void addFeatureLayer(const std::shared_ptr<Map>& map, const std::string& fileName) {
  std::shared_ptr<GeoPackageDataSource> datasource = GeoPackageDataSource::newBuilder().source(fileName).build();
  luciad::expected<std::shared_ptr<Model>, ErrorInfo> model = GeoPackageModelDecoder::decode(datasource);
  if (!model) {
    ErrorInfo& errorInfo = model.error();
    std::cerr << "Cannot load GeoPackage file (vector) " + fileName + ": " + errorInfo.getMessage() << std::endl;
    return;
  }

  std::shared_ptr<IFeatureModel> featureModel = std::dynamic_pointer_cast<IFeatureModel>(*model);
  if (!featureModel) {
    std::cerr << "Not a feature model" << std::endl;
    return;
  }

  std::shared_ptr<FeatureLayer> featureLayer = FeatureLayer::newBuilder().model(featureModel).painter(std::make_shared<RiverPainter>()).build();
  map->getLayerList()->add(featureLayer);
}
private void AddFeatureLayer(String fileName, Map map)
{
    try
    {
        var datasource = GeoPackageDataSource.NewBuilder().Source(fileName).Build();
        Model model = GeoPackageModelDecoder.Decode(datasource);
        IFeaturePainter featurePainter = new RiverPainter();
        FeatureLayer featureLayer = FeatureLayer.NewBuilder().Model(model as IFeatureModel)
            .Painter(featurePainter)
            .Build();
        map.LayerList.Add(featureLayer);
    }
    catch (Exception exception)
    {
        string errorMessage = $"Cannot load data from file '{fileName}': {exception.Message}";
        MessageBox.Show(errorMessage);
    }
}
private fun addFeatureLayer(map: Map, fileName: String) {
    val datasource = GeoPackageDataSource.newBuilder().source(fileName).build()
    val model = GeoPackageModelDecoder.decode(datasource)

    if (model is IFeatureModel) {
        val featureLayer = FeatureLayer.newBuilder()
            .model(model)
            .painter(RiverPainter())
            .build()
        map.layerList.add(featureLayer)
    } else {
        throw RuntimeException("Not a feature model")
    }
}

Fitting on the data

When you are adding data to the map, it can be useful to automatically move the viewport of the map to the data position, so that it becomes visible to the map user. This is referred to as fitting on the data in LuciadCPillar.

To fit on map data:

  1. Create the bounds that you want to fit on, or take the bounds of the data model.

  2. Create a map navigator for the map. The map navigator is an object that allows you to navigate in the map programmatically.

  3. Use the map navigator to fit on the bounds.

Program: Fitting the map
luciad::expected<std::shared_ptr<CoordinateReference>, ErrorInfo> crs = CoordinateReferenceProvider::create("EPSG:4326");
if (!crs) {
  return;
}
static constexpr Coordinate lowerLeft{5.5, 45.5, 0};
const Bounds bounds{crs.value(), lowerLeft, 6, 2.5, 0};
map->getMapNavigator().newFitAction().bounds(bounds).fit();
CoordinateReference fitReference = CoordinateReferenceProvider.Create("EPSG:4326");
Bounds bounds = GeometryFactory.CreateBounds(fitReference, new Coordinate(5.5, 45.5), 6.0, 2.5, 0);
map.MapNavigator.NewFitAction().Bounds(bounds).Fit();
val wgs84 = CoordinateReferenceProvider.create("EPSG:4326")
    val fitBounds = Bounds(wgs84, Coordinate(5.3895, 43.307), 0.0065, 0.005, 0.0)
    map.mapNavigator.newFitAction().bounds(fitBounds).fit()

Integrating the map in your Android layout

In order to integrate the map into your Android application, a custom view class must be created. The com.luciad.android.MapView class is provided within the integration module used in the samples.

If you declare your app UI in XML, you can use the MapView as any other widget.

Simple layout with a MapView
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:ignore="MissingDefaultResource">

  <com.luciad.android.MapView
    android:id="@+id/mapView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

</RelativeLayout>

Once the map has been created, it must be set on the Mapview using MapView#setMap. You can retrieve the MapView using Activity#findViewById,