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. It is provided for C++ as well as C#.

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 Environment. While you are interacting with the library, you must hold a reference to that Environment object. The lifetime of this object determines when the functionality of the library is available.

Creating the Environment

You use a builder pattern to create the Environment, through 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.

For example:

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

    string licenseText = LicenseTextReader.ReadLicenseText();
    env = Environment.CreateInitializer()
        .WithLicenseText(licenseText)
        .WithResourcePath("resources")
        .Initialize();

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.

Disposing of the environment

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.

Program (C#): Dispose of an environment
env.Dispose();

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 LuciadCPllar 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 (C++): 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();
Program (C#): Creating the map
// 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();

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 GeoPackageModelDecoder 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 IRasterModel interface.

To visualize a raster model, you must create a RasterLayer. 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 (C++): Adding a raster layer
void addRasterLayer(const std::shared_ptr<Map>& map, const std::string& fileName) {
  luciad::expected<std::shared_ptr<Model>, ErrorInfo> model = GeoPackageModelDecoder::decode(fileName);
  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);
}
Program (C#): Adding a raster layer
private void AddRasterLayer(String fileName, Map map)
{
    try
    {
        Model model = GeoPackageModelDecoder.Decode(fileName);
        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}";
        Console.WriteLine(errorMessage);
    }
}

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 Feature class. Models containing features implement the IFeatureModel interface.

To visualize a feature model, you need a FeatureLayer. When you pass the model to the layer, you must also provide an instance of IFeaturePainter. 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 (C++): Adding a feature layer
void addFeatureLayer(const std::shared_ptr<Map>& map, const std::string& fileName) {
  luciad::expected<std::shared_ptr<Model>, ErrorInfo> model = GeoPackageModelDecoder::decode(fileName);
  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);
}
Program (C#): Adding a feature layer
private void AddFeatureLayer(String fileName, Map map)
{
    try
    {
        Model model = GeoPackageModelDecoder.Decode(fileName);
        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}";
        Console.WriteLine(errorMessage);
    }
}

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 (C++): Fitting the map
luciad::expected<std::shared_ptr<CoordinateReference>, ErrorInfo> crs = CoordinateReferenceProvider::create("EPSG:4326");
if (!crs) {
  return;
}
Coordinate coordinate{7.5, 45.9, 0};
Bounds bounds{crs.value(), coordinate, 2.5, 1.0, 0};
std::shared_ptr<MapNavigator> mapNavigator = MapNavigator::create(map);
mapNavigator->newFitAction().bounds(bounds).fit();
Program (C#): Fitting the map
CoordinateReference fitReference = CoordinateReferenceProvider.Create("EPSG:4326");
Bounds bounds = GeometryFactory.CreateBounds(fitReference, new Coordinate(7.5, 45.9), 2.5, 1.0, 0);
MapNavigator navigator = MapNavigator.Create(map);
navigator.NewFitAction().Bounds(bounds).Fit();