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 Environment
Environment
Environment
.
While you are interacting with the library, you must hold a reference to that Environment
Environment
Environment
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 Environment
Environment
Environment
, 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:
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()
}
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.
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:
-
Create a model for the data
-
Create a layer for that model
-
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
GeoPackageModelDecoder
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
IRasterModel
IRasterModel
interface.
To visualize a raster model, you must create a RasterLayer
RasterLayer
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.
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 Feature
Feature
Feature
class.
Models containing features implement the IFeatureModel
IFeatureModel
IFeatureModel
interface.
To visualize a feature model, you need a FeatureLayer
FeatureLayer
FeatureLayer
.
When you pass the model to the layer, you must also provide an instance of IFeaturePainter
IFeaturePainter
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.
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:
-
Create the bounds that you want to fit on, or take the bounds of the data model.
-
Create a map navigator for the map. The map navigator is an object that allows you to navigate in the map programmatically.
-
Use the map navigator to fit on the bounds.
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()