This article describes how to apply constraints to the geometry editing and creation process. We configure this PolylinePolylinePolyline 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.

Step 1- Implement the constraint

The constraint interface has two methods to implement:

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: 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;
};
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);
    }
}
public static class MyPolylineConstraint1 implements IPolylineConstraint {
  private double maxHeight = 100e3;

  @Override
  public Polyline apply(Polyline oldPolyline, Polyline newPolyline, PolylineChange changes) {
    return this.applyImpl(newPolyline);
  }

  @Override
  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.
    Bounds bounds = polyline.getBounds();
    if (bounds.getLocation().getZ() + 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.
    List<Coordinate> points = new ArrayList<>();
    for (Coordinate point : polyline.getPoints()) {
      double z = Math.min(point.getZ(), maxHeight);
      points.add(new Coordinate(point.getX(), point.getY(), z));
    }

    return GeometryFactory.createPolyline(polyline.getReference(), points, polyline.getInterpolationType());
  }
}

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 PolylineChangePolylineChangePolylineChange parameter.

Program: 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;
};
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);
    }
}
public static class MyPolylineConstraint implements IPolylineConstraint {
  private double maxHeight = 100e3;

  @Override
  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.getPointCount() - 1));
  }

  @Override
  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, boolean adjustFirstPoint) {
    long lastPointIndex = polyline.getPointCount() - 1;
    if (polyline.getPoint(0).getY() != polyline.getPoint(lastPointIndex).getY()) {
      // if the last point was moved, adjust the first point. Otherwise adjust the last point.
      long pointToAdjust = adjustFirstPoint ? 0 : lastPointIndex;
      Coordinate location = polyline.getPoint(pointToAdjust);
      Coordinate newLocation = new Coordinate(location.getX(),
                                              polyline.getPoint(lastPointIndex - pointToAdjust).getY(), location.getZ());
      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.
    Bounds bounds = polyline.getBounds();
    if (bounds.getLocation().getZ() + 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.
    List<Coordinate> points = new ArrayList<>();
    for (Coordinate point : polyline.getPoints()) {
      double z = Math.min(point.getZ(), maxHeight);
      points.add(new Coordinate(point.getX(), point.getY(), z));
    }

    return GeometryFactory.createPolyline(polyline.getReference(), points, polyline.getInterpolationType());
  }
}

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::setConstraintPolylineCreator::setConstraintPolylineCreator::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 IFeatureEditConfigurationIFeatureEditConfigurationIFeatureEditConfiguration.

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

Program: 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();
  }
};
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();
    }
}
public static class ConstraintEditConfiguration implements IFeatureEditConfiguration {
  @Override
  public void edit(Feature feature, long layerId, Map map, FeatureEditConfigurationBuilder builder) {
    // create a new polyline handles provider on which we configure our custom constraint.
    PolylineHandlesProvider polylineHandlesProvider = new PolylineHandlesProvider();
    polylineHandlesProvider.setConstraint(new MyPolylineConstraint());

    // put the factory in a composite, with high priority, so it is used before the default factories.
    CompositeGeometryHandlesProvider 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.
    FeatureHandlesProvider featureHandlesProvider = new FeatureHandlesProvider();
    featureHandlesProvider.setGeometryHandlesProvider(compositeProvider);
    builder.handlesProvider(featureHandlesProvider);

    builder.submit();
  }
}

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

Program: 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);
var editConfiguration = new ConstraintEditConfiguration();
var layer = FeatureLayer.NewBuilder().Model(model).EditConfiguration(editConfiguration).Build();
map.LayerList.Add(layer);
ConstraintEditConfiguration editConfiguration = new ConstraintEditConfiguration();
FeatureLayer layer = FeatureLayer.newBuilder()
                                 .model(model)
                                 .editConfiguration(editConfiguration)
                                 .build();
map.getLayerList().add(layer);