This article describes how to apply constraints to the geometry editing and creation process. We configure this Polyline constraint as an example:

  • The Z-values of all polyline points have a maximum height.

  • The first and the last point of the polyline must always have the same Y-coordinate.

For this constraint, we must implement an IPolylineConstraint and make sure that the feature handles provider and the creator use it.

Step 1- Implement the constraint

The constraint interface has two methods to implement:

  • A general apply method that takes only one Polyline parameter. The application calls this method when there is no particular change that prompted the constraint call. For example, the default LuciadCPillar geometry creators call it while the geometry is still being created.

  • An apply method that takes both an old polyline parameter with the original polyline and a new Polyline parameter, which is the polyline resulting from a change, without constraints applied. It also takes a PolylineChange object parameter that describes the change that turned the old polyline into the new polyline.
    The method returns the Polyline that results from applying the constraint. Editors created by PolylineHandlesProvider — and by the other default geometry handles provider — always call this method rather than the general method. For example, when users move a polyline point with an edit handle, the application calls the apply method of the constraint with the original Polyline, the new Polyline, and a change object that describes the point move.

Let’s start by implementing the maximum height limitation. We don’t need any information about the change to apply this constraint, so we add a general helper method and make both apply methods delegate to it.

Program (C++): The IPolylineConstraint with only the height limit.
class MyPolylineConstraint : public IPolylineConstraint {
public:
  std::shared_ptr<Polyline> apply(const std::shared_ptr<Polyline>& /*oldPolyline*/,
                                  const std::shared_ptr<Polyline>& newPolyline,
                                  const PolylineChange& /*changes*/) override {
    return this->applyImpl(newPolyline);
  }

  std::shared_ptr<Polyline> apply(const std::shared_ptr<Polyline>& polyline) override {
    return this->applyImpl(polyline);
  }

private:
  std::shared_ptr<Polyline> applyImpl(const std::shared_ptr<Polyline>& polyline) const {
    // To avoid unnecessary copies, we first check the polyline's bounding box.
    // If the bounds don't surpass the max height, we don't need to apply any changes and simply return the given polyline.
    auto bounds = polyline->getBounds();
    if (bounds.getLocation().z + bounds.getDepth() <= _maxheight) {
      return polyline;
    }

    // At least one point of the polyline lies above the max height, so adjust the Z-values and recreate the polyline.
    auto points = polyline->getPoints();
    for (auto& point : points) {
      point.z = std::min(point.z, _maxheight);
    }
    return GeometryFactory::createPolyline(polyline->getReference(), std::move(points), polyline->getInterpolationType());
  }

  double _maxheight = 100e3;
};

C#

public class MyPolylineConstraint : IPolylineConstraint
{
    private double maxheight = 100e3;

    public Polyline Apply(Polyline oldPolyline, Polyline newPolyline, PolylineChange changes)
    {
        return this.ApplyImpl(newPolyline);
    }

    public Polyline Apply(Polyline polyline)
    {
        return ApplyImpl(polyline);
    }

    private Polyline ApplyImpl(Polyline polyline)
    {
        // To avoid unnecessary copies, we first check the polyline's bounding box. If the bounds don't surpass
        // the max height, we don't need to apply any changes and can simply return the given polyline.
        var bounds = polyline.Bounds;
        if (bounds.Location.Z + bounds.Depth <= maxheight)
        {
            return polyline;
        }

        // At least one point of the polyline lies above the max height, so adjust the Z-values and recreate the polyline.
        var points = new List<Coordinate>(polyline.Points.Count);
        foreach (var point in polyline.Points)
        {
            double z = Math.Min(point.Z, maxheight);
            points.Add(new Coordinate(point.X, point.Y, z));
        }

        return GeometryFactory.CreatePolyline(polyline.Reference, points, polyline.InterpolationType);
    }
}

For the second constraint, we need information about the change that happened: when the first point moves, we want to move the last point along, and the other way around. To find out which point changed, we can use the PolylineChange parameter.

Program (C++): The complete IPolylineConstraint.
class MyPolylineConstraint : public IPolylineConstraint {
public:
  std::shared_ptr<Polyline> apply(const std::shared_ptr<Polyline>& /*oldPolyline*/,
                                  const std::shared_ptr<Polyline>& newPolyline,
                                  const PolylineChange& changes) override {
    // If the last point was moved with this change, we need to adjust the first point to this new location, instead of the other way around.
    return this->applyImpl(newPolyline, changes.hasPointMoved(newPolyline->getPointCount() - 1));
  }

  std::shared_ptr<Polyline> apply(const std::shared_ptr<Polyline>& polyline) override {
    // There is no information about any change that happened, so we adjust the last point by default.
    return this->applyImpl(polyline, false);
  }

private:
  std::shared_ptr<Polyline> applyImpl(std::shared_ptr<Polyline> polyline, bool adjustFirstPoint) const {
    size_t lastPointIndex = polyline->getPointCount() - 1;
    if (polyline->getPoint(0).y != polyline->getPoint(lastPointIndex).y) {
      // if the last point was moved, adjust the first point. Otherwise adjust the last point.
      size_t pointToAdjust = adjustFirstPoint ? 0 : lastPointIndex;
      Coordinate location = polyline->getPoint(pointToAdjust);
      location.y = polyline->getPoint(lastPointIndex - pointToAdjust).y;
      polyline = polyline->movePoint(pointToAdjust, location);
    }

    // To avoid unnecessary copies, we first check the polyline's bounding box.
    // If the bounds don't surpass the max height, we don't need to apply any changes and can simply return the given polyline.
    auto bounds = polyline->getBounds();
    if (bounds.getLocation().z + bounds.getDepth() <= _maxheight) {
      return polyline;
    }

    // At least one point of the polyline lies above the max height, so adjust the Z-values and recreate the polyline.
    auto points = polyline->getPoints();
    for (auto& point : points) {
      point.z = std::min(point.z, _maxheight);
    }
    return GeometryFactory::createPolyline(polyline->getReference(), std::move(points), polyline->getInterpolationType());
  }

  double _maxheight = 100e3;
};

C#

public class MyPolylineConstraint : IPolylineConstraint
{
    private double maxheight = 100e3;

    public Polyline Apply(Polyline oldPolyline, Polyline newPolyline, PolylineChange changes)
    {
        // If the last point was moved with this change, we need to adjust the first point, instead of the last.
        return this.ApplyImpl(newPolyline, changes.HasPointMoved(newPolyline.PointCount - 1));
    }

    public Polyline Apply(Polyline polyline)
    {
        // There is no information about any change that happened, so we adjust the last point by default.
        return ApplyImpl(polyline, false);
    }

    private Polyline ApplyImpl(Polyline polyline, bool adjustFirstPoint)
    {
        uint lastPointIndex = polyline.PointCount - 1;
        if (polyline.GetPoint(0).Y != polyline.GetPoint(lastPointIndex).Y)
        {
            // if the last point was moved, adjust the first point. Otherwise adjust the last point.
            uint pointToAdjust = adjustFirstPoint ? 0u : lastPointIndex;
            Coordinate location = polyline.GetPoint(pointToAdjust);
            Coordinate newLocation = new Coordinate(location.X,
                polyline.GetPoint(lastPointIndex - pointToAdjust).Y, location.Z);
            polyline = polyline.MovePoint(pointToAdjust, newLocation);
        }

        // To avoid unnecessary copies, we first check the polyline's bounding box. If the bounds don't surpass
        // the max height, we don't need to apply any changes and can simply return the given polyline.
        var bounds = polyline.Bounds;
        if (bounds.Location.Z + bounds.Depth <= maxheight)
        {
            return polyline;
        }

        // At least one point of the polyline lies above the max height, so adjust the Z-values and recreate the polyline.
        var points = new List<Coordinate>(polyline.Points.Count);
        foreach (var point in polyline.Points)
        {
            double z = Math.Min(point.Z, maxheight);
            points.Add(new Coordinate(point.X, point.Y, z));
        }

        return GeometryFactory.CreatePolyline(polyline.Reference, points, polyline.InterpolationType);
    }
}

Step 2 - Configure the constraint on the editor or creator

We created our custom constraint in step 1. Now, we make sure it’s actually used.

For geometry creators, we can set the constraint directly on the specific creator class. See PolylineCreator::setConstraint for example.

To apply the constraint when users are editing the polyline, we need extra configuration. We must set the constraint on the appropriate geometry factory. Then, we must configure it on the feature layer. We can do so with the IFeatureEditConfiguration.

The geometry handles providers offer more configuration options than just constraints. On the PolylineHandlesProvider, for example, you can set the minimum or maximum number of points a polyline can have, or override which edit handles it provides.

Program (C++): Setting a geometry constraint through an IFeatureEditConfiguration.
class ConstraintEditConfiguration : public IFeatureEditConfiguration {
public:
  void edit(const Feature& /*feature*/, LayerId /*layerId*/, const std::shared_ptr<Map>& /*map*/, FeatureEditConfigurationBuilder& builder) const override {
    // create a new polyline handles provider on which we configure our custom constraint.
    auto polylineHandlesProvider = std::make_shared<PolylineHandlesProvider>();
    polylineHandlesProvider->setConstraint(std::make_shared<MyPolylineConstraint>());

    // put the handles provider in a composite, with high priority, so it is used before the default factories.
    auto compositeProvider = CompositeGeometryHandlesProvider::createDefault();
    compositeProvider->add(std::move(polylineHandlesProvider), Priority::High);

    // Set the composite provider as delegate of a new Feature handles provider, and submit this to the edit configuration builder.
    auto featureHandlesProvider = std::make_shared<FeatureHandlesProvider>();
    featureHandlesProvider->setGeometryHandlesProvider(std::move(compositeProvider));
    builder.handlesProvider(std::move(featureHandlesProvider));

    builder.submit();
  }
};

C#

class ConstraintEditConfiguration : IFeatureEditConfiguration
{
    public void Edit(Feature feature, ulong layerId, Map map, FeatureEditConfigurationBuilder builder)
    {
        // create a new polyline handles provider on which we configure our custom constraint.
        var PolylineHandlesProvider = new PolylineHandlesProvider {Constraint = new MyPolylineConstraint()};

        // put the factory in a composite, with high priority, so it is used before the default factories.
        var compositeProvider = CompositeGeometryHandlesProvider.CreateDefault();
        compositeProvider.Add(PolylineHandlesProvider, Priority.High);

        // Set the composite handles provider as delegate of a new Feature handles provider,
        // and submit this to the edit configuration builder.
        builder.HandlesProvider(new FeatureHandlesProvider {GeometryHandlesProvider = compositeProvider});

        builder.Submit();
    }
};

To complete step 2, we give this edit configuration to the FeatureLayer::Builder.

Program (C++): Provide the IFeatureEditConfiguration to a FeatureLayerBuilder
auto editConfiguration = std::make_shared<ConstraintEditConfiguration>();
auto layer = FeatureLayer::newBuilder().model(model).editConfiguration(editConfiguration).build();
map->getLayerList()->add(layer);

C#

var editConfiguration = new ConstraintEditConfiguration();
var layer = FeatureLayer.NewBuilder().Model(model).EditConfiguration(editConfiguration).Build();
map.LayerList.Add(layer);