The LuciadCPillar terrain analysis capability includes classes to visualize the line-of-sight (LOS) between a point and the terrain in its environment.

The visualization result is a LOS coverage. It takes the form of a draped disk with various colored areas indicating visibility. There are two main building blocks to visualizing a LOS coverage:

Creating line-of-sight features

A LOS coverage is defined by these properties:

Center

The observer point around which line-of-sight is calculated and visualized.

Center height

The height above the terrain of the observer in the center.

Radius

The radial extent, outwards from the center, of the coverage area.

Start angle

The starting angle of the arc span of the coverage area.

End angle

The ending angle of the arc span of the coverage area.

Minimum vertical angle

The angular lower bounds of the visible vertical area around the center.

Maximum vertical angle

The angular upper bounds of the visible vertical area around the center.

The start and end angle properties, together with the radius, define the horizontal area over which to visualize the line-of-sight. See Figure 1, “Horizontal angle LOS properties” for an illustration. These angles are measured clockwise with respect to the north direction. By default, the LOS coverage spans the full 360 degrees around the center.

horizontal angle
Figure 1. Horizontal angle LOS properties

The minimum and maximum vertical angle properties define the visible vertical area. The invisible vertical area is called the cone of silence. These properties can model the common use case where an observer can’t see directly below or above. See Figure 2, “Vertical angle LOS properties” for an illustration. Both angles start from 0, pointing towards the ground, to 180 degrees at zenith. By default, the LOS coverage spans the full 180 vertical degrees around the center.

vertical angle
Figure 2. Vertical angle LOS properties

To create a LOS feature with the necessary properties, you can use LineOfSightFeature::BuilderLineOfSightFeature::BuilderLineOfSightFeature::Builder. This program shows how to create a LOS feature:

Program: Creating a line-of-sight feature
auto feature = LineOfSightFeature::newBuilder().id(id).center(centerPoint).centerHeight(centerHeight).radius(radius).build();
var feature = LineOfSightFeature.NewBuilder().Id(id).Center(centerPoint).CenterHeight(10.0).Radius(radius).Build();
val feature = LineOfSightFeature.newBuilder().id(id).center(centerPoint).centerHeight(centerHeight).radius(radius).build()

The new LOS features are put in a feature modelfeature modelfeature model to use later on in a layer. The feature model metadata must have LineOfSightFeature::getLineOfSightPropertiesDataTypeLineOfSightFeature::getLineOfSightPropertiesDataTypeLineOfSightFeature::getLineOfSightPropertiesDataType as one of its feature types.

Visualizing line-of-sight features

To visualize the LOS features in a feature model as LOS coverages on the map, you use a LineOfSightLayerLineOfSightLayerLineOfSightLayer, which you can configure using its builderbuilderbuilder.

For the styling of the line-of-sight features, you can use a LineOfSightStyleLineOfSightStyleLineOfSightStyle. You can configure the colors used to indicate areas that are visible and invisible to the observer in the center.

Program: Creating a line-of-sight layer
auto losStyle = LineOfSightStyle::newBuilder().visibleColor(visibleColor).invisibleColor(invisibleColor).build();
auto layer = LineOfSightLayer::newBuilder().model(losModel).lineOfSightStyle(losStyle).build();
var losStyle = LineOfSightStyle.NewBuilder().VisibleColor(VisibleColor).InvisibleColor(InvisibleColor).Build();
var losLayer = LineOfSightLayer.NewBuilder().Model(losModel).LineOfSightStyle(losStyle).Build();
val losStyle = LineOfSightStyle.newBuilder().visibleColor(visibleColor).invisibleColor(invisibleColor).build()
val losLayer = LineOfSightLayer.newBuilder().model(losModel).lineOfSightStyle(losStyle).build()

Calculating LOS: source data and accuracy

The source data that LuciadCPillar takes into account for the line-of-sight calculation is the surrounding terrain, or more precisely, the visible elevation layers. This means that vector layers containing 3D Tiles or other 3D shapes aren’t included, for example.

The accuracy of the result depends on the radius of the LOS feature. The radius is used by a heuristic that tries to strike a balance between performance and accuracy in the calculation.

Changing LOS coverages in the terrain analysis sample

The LOS capability is demonstrated in the terrain analysis sample. The sample has a LOS layer displaying the LOS coverages and an editing layer which is used to make changes to the LOS coverages. Changes made to the circles in the editing layer are propagated to the LOS features in the LOS layer with an observer.

Program: Propagating model changes to update the LOS features
// add an observer that will update the line of sight layer when a circle is created/edited.
circleEditingLayer->getModel()->addObserver(IFeatureModelObserver::create(
    [losModel = losLayer->getModel()](const auto& event) {
      auto losUpdaterBuilder = FeatureModelUpdate::newBuilder();

      for (const auto& change : event.getFeatureChanges()) {
        if (change.getFeatureChangeType() == FeatureChangeType::Remove) {
          losUpdaterBuilder.removeFeature(change.getFeatureId());
        } else {
          const auto circle = std::dynamic_pointer_cast<CircleByCenterPoint>(change.getFeature()->findGeometry());
          const auto center = circle->getCenter();
          const auto radius = circle->getRadius();
          if (change.getFeatureChangeType() == FeatureChangeType::Add) {
            const auto newLosFeature = LineOfSightModel::createFeature(change.getFeatureId(), center, radius);
            losUpdaterBuilder.addFeature(newLosFeature);
          } else if (change.getFeatureChangeType() == FeatureChangeType::Update) {
            const auto updatedLosFeature = LineOfSightModel::createFeature(change.getFeatureId(), center, radius);
            losUpdaterBuilder.updateFeature(updatedLosFeature);
          }
        }
      }
      losModel->getUpdater()->update(losUpdaterBuilder.build());
}));
        // Add an observer that will update the line of sight layer when a circle is created/edited.
        _circleCreationLayer.Model.AddObserver(new LineOfSightModelObserver(losLayer.Model));

        // ...

sealed class LineOfSightModelObserver : IFeatureModelObserver
{
    private readonly IFeatureModel _losModel;

    public LineOfSightModelObserver(IFeatureModel losModel)
    {
        _losModel = losModel;
    }

    void IFeatureModelObserver.OnFeatureModelChanged(FeatureModelEvent featureModelEvent)
    {
        var losUpdaterBuilder = FeatureModelUpdate.NewBuilder();

        foreach (var change in featureModelEvent.FeatureChanges)
        {
            if (change.FeatureChangeType == FeatureChangeType.Remove)
            {
                losUpdaterBuilder.RemoveFeature(change.FeatureId);
            }
            else
            {
                var circle = change.Feature.FindGeometry() as CircleByCenterPoint;
                if (change.FeatureChangeType == FeatureChangeType.Add)
                {
                    var newLosFeature =
                        LineOfSightModel.CreateFeature(change.FeatureId, circle.Center, circle.Radius);
                    losUpdaterBuilder.AddFeature(newLosFeature);
                }
                else if (change.FeatureChangeType == FeatureChangeType.Update)
                {
                    var updatedLosFeature =
                        LineOfSightModel.CreateFeature(change.FeatureId, circle.Center, circle.Radius);
                    losUpdaterBuilder.UpdateFeature(updatedLosFeature);
                }
            }
        }

        _losModel.GetUpdater().Update(losUpdaterBuilder.Build());
    }
}
// Add an observer that will update the line of sight layer when a circle is created/edited.
circleEditingLayer.model.addObserver { event ->
    var updateBuilder = FeatureModelUpdate.newBuilder()
    for (change in event.featureChanges) {
        if (change.featureChangeType == FeatureChangeType.Remove) {
            updateBuilder.removeFeature(change.featureId)
        } else {
            val circle = change.feature!!.findGeometry() as CircleByCenterPoint
            if (change.featureChangeType == FeatureChangeType.Add) {
                val newLosFeature = LineOfSightModel.createFeature(change.featureId, circle.center, circle.radius)
                updateBuilder.addFeature(newLosFeature)
            } else if (change.featureChangeType == FeatureChangeType.Update) {
                val updatedLosFeature = LineOfSightModel.createFeature(change.featureId, circle.center, circle.radius)
                updateBuilder.updateFeature(updatedLosFeature)
            }
        }
    }
    losLayer.model.updater?.update(updateBuilder.build())
}
sample
Figure 3. The terrain analysis sample.