This tutorial explains how you can create a handles provider.

The goal of this tutorial is to add support for editing a CircleByCenterPoint with these handles:

  • A translate handle

  • A point handle to change the radius of the circle

Other customizations

You need to customize a handles provider for advanced use cases only. For other customization methods, see this article.

The sample_editing sample supports this tutorial. It’s available for C++ and C#.

See this article for more information about editing.

Step 1 - Add support for translation

To add support for the CircleByCenterPoint geometry, we create a new geometry handles provider implementation. To add support for translation, we must implement these methods:

This sample code shows how you can do that:

Program (C++): Add support for translation
bool CircleBy2PointsHandlesProvider::canProvide(const std::shared_ptr<ObservableGeometry>& geometry,
                                                const std::shared_ptr<FeatureEditContext>& /*context*/) const {
  return dynamic_cast<CircleByCenterPoint*>(geometry->getValue().get()) != nullptr;
}
std::shared_ptr<ITranslateEditAction> CircleBy2PointsHandlesProvider::provideTranslateAction(
    std::shared_ptr<ObservableGeometry> geometry,
    const std::shared_ptr<FeatureEditContext>& /*context*/,
    std::shared_ptr<IGeometryEditCallback> geometryEditCallback) const {
  // Derive a circle. This method should only be called when canProvide is true, so we can assume that it is a CircleByCenterPoint
  auto observableCircle = ObservableGeometry::deriveCircleByCenterPoint(geometry);

  // Create a translate action for the circle. This can be used by FeatureHandlesProvider (or a custom IFeatureHandlesProvider)
  // to create a translate handle.
  return std::make_shared<CircleTranslateAction>(observableCircle, std::move(geometryEditCallback));
}

C#

public bool CanProvide(ObservableGeometry geometry, FeatureEditContext context)
{
    return geometry.Value is CircleByCenterPoint;
}
public ITranslateEditAction ProvideTranslateAction(ObservableGeometry geometry, FeatureEditContext context, IGeometryEditCallback geometryEditCallback)
{
    // Derive a circle. This method should only be called when canProvide is true, so we can assume that it is a CircleByCenterPoint
    var observableCircle = ObservableGeometry.DeriveCircleByCenterPoint(geometry);

    // Create a translate action for the circle. This can be used by FeatureHandlesProvider (or a custom IFeatureHandlesProvider)
    // to create a translate handle.
    return new CircleTranslateAction(observableCircle, geometryEditCallback);
}

We use this action:

Program (C++): ITranslateEditAction implementation
class CircleTranslateAction final : public ITranslateEditAction {
public:
  CircleTranslateAction(std::shared_ptr<ObservableCircleByCenterPoint> circle, std::shared_ptr<IGeometryEditCallback> callback)
      : _circle{std::move(circle)}, _callback(std::move(callback)) {
  }

  void translate(Coordinate translation, EventStatus translateStatus, ChangeStatus changeStatus) override {
    if (translateStatus == EventStatus::Start) {
      _initialCircle = _circle->getValue();
    }
    auto newCenter = _initialCircle->getCenter() + translation;
    auto newCircle = GeometryFactory::createCircleByCenterPoint(_initialCircle->getReference(), newCenter, _initialCircle->getRadius());
    _callback->onEdit(newCircle, changeStatus);
  }

private:
  std::shared_ptr<ObservableCircleByCenterPoint> _circle;
  std::shared_ptr<IGeometryEditCallback> _callback;

  std::shared_ptr<CircleByCenterPoint> _initialCircle;
};

C#

private sealed class CircleTranslateAction : ITranslateEditAction
{
    public CircleTranslateAction(ObservableCircleByCenterPoint circle, IGeometryEditCallback callback)
    {
        _circle = circle;
        _callback = callback;
    }

    public void Translate(Coordinate translation, EventStatus translateStatus, ChangeStatus changeStatus)
    {
        if (translateStatus == EventStatus.Start)
        {
            _initialCircle = _circle.Value;
        }

        var newCenter = _initialCircle.Center + translation;
        var newCircle = GeometryFactory.CreateCircleByCenterPoint(_initialCircle.Reference, newCenter, _initialCircle.Radius);
        _callback.OnEdit(newCircle, changeStatus);
    }

    private readonly ObservableCircleByCenterPoint _circle;
    private readonly IGeometryEditCallback _callback;
    private CircleByCenterPoint _initialCircle;
};

Step 2 - Define the handles

The next step is to define which handles to use for editing certain aspects of the geometry. In this tutorial, we define a single point handle that users can drag to change the radius of the circle.

The edit state and the handles defined for a certain feature or geometry are managed in an IEditHandles implementation.

IEditHandles must:

  • Provide a set of handles for the geometry

  • Adapt the handles when the geometry changes

  • Send out notifications when the handles set becomes invalid

a) Provide a set of handles for the geometry

In this tutorial, we create a new radius handle and make sure that the IEditHandles::getList method returns it. This sample code shows how you can do that:

Program (C++): Add radius handle
  _radiusHandle = createRadiusHandle(_circle, geometryEditCallback);
std::vector<std::shared_ptr<IEditHandle>> getList() const override {
  return {_radiusHandle};
}
static std::shared_ptr<PointEditHandle> createRadiusHandle(const std::shared_ptr<ObservableCircleByCenterPoint>& circle,
                                                           const std::shared_ptr<IGeometryEditCallback>& editCallback) {
  auto handleLocation = ObservableCircleByCenterPoint::derivePointAtAngle(circle, 0);

  auto action = IPointEditAction::create([editCallback, circle](const std::shared_ptr<Point>& location, ChangeStatus changeStatus) {
    // Create a new circle based on the new radius of the handle, and inform the IGeometryEditCallback
    auto currentCircle = circle->getValue();
    auto calculations = GeodesyCalculations::create(currentCircle->getReference(), LineInterpolationType::Geodesic);
    auto newRadius = *calculations->distance2D(currentCircle->getCenter(), location->getLocation());
    auto newCircle = GeometryFactory::createCircleByCenterPoint(currentCircle->getReference(), currentCircle->getCenter(), newRadius);
    editCallback->onEdit(newCircle, changeStatus);
  });

  auto handle = std::make_shared<PointEditHandle>(handleLocation);
  handle->addOnDragAction(action).cursor(MouseCursor::Cross);
  return handle;
}

C#

    _radiusHandle = CreateRadiusHandle(_circle, geometryEditCallback);
public IList<IEditHandle> GetList()
{
    return new List<IEditHandle> {_radiusHandle};
}
private sealed class RadiusPointEditAction : IPointEditAction
{
    private readonly ObservableCircleByCenterPoint _circle;
    private readonly IGeometryEditCallback _editCallback;

    public RadiusPointEditAction(ObservableCircleByCenterPoint circle, IGeometryEditCallback editCallback)
    {
        _circle = circle;
        _editCallback = editCallback;
    }

    public void Execute(Point location, EventStatus eventStatus, ChangeStatus changeStatus)
    {
        Execute(location, changeStatus);
    }

    public void Execute(Point location, ChangeStatus changeStatus)
    {
        // Create a new circle based on the new radius of the handle, and inform the IGeometryEditCallback
        var currentCircle = _circle.Value;
        var calculations = GeodesyCalculations.Create(currentCircle.Reference, LineInterpolationType.Geodesic);
        var newRadius = calculations.Distance2D(currentCircle.Center, location.Location).Value;
        var newCircle = GeometryFactory.CreateCircleByCenterPoint(currentCircle.Reference, currentCircle.Center, newRadius);
        _editCallback.OnEdit(newCircle, changeStatus);
    }
}

private static PointEditHandle CreateRadiusHandle(ObservableCircleByCenterPoint circle, IGeometryEditCallback editCallback)
{
    var handleLocation = ObservableCircleByCenterPoint.DerivePointAtAngle(circle, 0);
    var action = new RadiusPointEditAction(circle, editCallback);

    var handle = new PointEditHandle(handleLocation);
    handle.AddOnDragAction(action).Cursor(MouseCursor.Cross);
    return handle;
}

b) Adapt the handles when the geometry changes

To adapt the handles to a geometry change, add an observer to the observable geometry or feature used during editing. In this tutorial, we edit a CircleByCenterPoint, so we’re using an ObservableCircleByCenterPoint.

This code shows how to detect changes:

Program (C): Detect changes to the edited link:{doxygendocPath}classluciad_1_1_circle_by_center_point.html++[CircleByCenterPoint]
CircleBy2PointsEditHandles(std::shared_ptr<ObservableCircleByCenterPoint> circle, const std::shared_ptr<IGeometryEditCallback>& geometryEditCallback)
    : _circle(std::move(circle)) {
  _circleCallback = IInvalidationCallback::create([&]() {
    // Handle all possible changes to the geometry
    onGeometryChanged();
  });
  _circle->addCallback(_circleCallback);
}
~CircleBy2PointsEditHandles() override {
  _circle->removeCallback(_circleCallback);
}

C#

public CircleBy2PointsEditHandles(ObservableCircleByCenterPoint circle, IGeometryEditCallback geometryEditCallback)
{
    _circle = circle;
    IInvalidationCallback circleCallback = new CircleInvalidateCallback(circle, this);
    _circle.AddCallback(circleCallback);
}

private sealed class CircleInvalidateCallback : IInvalidationCallback
{
    private readonly ObservableCircleByCenterPoint _circle;
    private readonly WeakReference<CircleBy2PointsEditHandles> _editHandles;

    public CircleInvalidateCallback(ObservableCircleByCenterPoint circle, CircleBy2PointsEditHandles editHandles)
    {
        _circle = circle;
        _editHandles = new WeakReference<CircleBy2PointsEditHandles>(editHandles);
    }

    public void OnInvalidate()
    {
        if (!_editHandles.TryGetTarget(out var editHandles))
        {
            _circle.RemoveCallback(this);
            return;
        }

        // Handle all possible changes to the geometry
        editHandles.OnGeometryChanged();
    }
}

In this tutorial, we only need an action to check if the edited geometry is still a CircleByCenterPoint. See c) Send out notifications when the set of handles becomes invalid. Apart from that, we don’t need any other actions to make sure that the handles are still up-to-date.

For other geometry types, you may need to do more, though. For example, when the point count of a polyline changes, you must add or remove handles. In that case, you must make sure that your IEditHandles implementation notifies the registered observers of these additions and removals.

c) Send out notifications when the set of handles becomes invalid

An edited geometry can change from one geometry type to another for some reason. For example, the model can decide that a Feature doesn’t contain a CircleByCenterPoint anymore, but that another geometry type is required.

An IEditHandles implementation can send a notification that it isn’t suitable anymore for the current geometry. This sample code demonstrates such a notification:

Program (C++): Notify that a set of handles isn’t valid anymore
void onGeometryChanged() {
  if (!_circle->getValue()) {
    // This set of handles should not be used anymore since the current geometry is not a CircleByCenterPoint anymore.
    fireInvalidHandlesEvent();
  }
}
void fireInvalidHandlesEvent() {
  auto observers = _observers;
  for (const auto& observer : observers) {
    observer->onEditHandlesInvalid();
  }
}

C#

private void OnGeometryChanged()
{
    if (_circle.Value == null)
    {
        // This set of handles should not be used anymore since the current geometry is not a CircleByCenterPoint anymore.
        FireInvalidHandlesEvent();
    }
}
private void FireInvalidHandlesEvent()
{
    var observers = new List<IEditHandlesObserver>(_observers);
    foreach (var observer in observers)
    {
        observer.OnEditHandlesInvalid();
    }
}

d) Use the IEditHandles implementation

We must now make sure that our custom handles provider uses the handles we defined:

Program (C++): Add support for the new handles
std::shared_ptr<IEditHandles> CircleBy2PointsHandlesProvider::provide(std::shared_ptr<ObservableGeometry> geometry,
                                                                      const std::shared_ptr<FeatureEditContext>& /*context*/,
                                                                      std::shared_ptr<IGeometryEditCallback> geometryEditCallback) const {
  // Derive a circle. This method should only be called when canProvide is true, so we can assume that it is a CircleByCenterPoint
  auto observableCircle = ObservableGeometry::deriveCircleByCenterPoint(geometry);

  // Create a set of handles for this circle
  return std::make_shared<CircleBy2PointsEditHandles>(observableCircle, geometryEditCallback);
}

C#

public IEditHandles Provide(ObservableGeometry geometry, FeatureEditContext context, IGeometryEditCallback geometryEditCallback)
{
    // Derive a circle. This method should only be called when canProvide is true, so we can assume that it is a CircleByCenterPoint
    var observableCircle = ObservableGeometry.DeriveCircleByCenterPoint(geometry);

    // Create a set of handles for this circle
    return new CircleBy2PointsEditHandles(observableCircle, geometryEditCallback);
}

Step 3 - Make sure that the new handles provider is used

We must make sure that the editing framework uses our custom handles provider, so we configure it in an IFeatureEditConfiguration, and set that configuration on the FeatureLayer.

Program (C++): Creating an IFeatureHandlesProvider
// Use a custom edit configuration that changes the editing behavior for this layer
auto customEditConfiguration = std::make_shared<CustomEditConfiguration>();

// Register the configuration. This makes the layer editable by default
return FeatureLayer::newBuilder()
    .model(model) //
    .title("Tutorial 3")
    .editConfiguration(customEditConfiguration)
    .build();

C#

// Use a custom edit configuration that changes the editing behavior for this layer
var customEditConfiguration = new CustomEditConfiguration();

// Register the configuration. This makes the layer editable by default
return FeatureLayer.NewBuilder()
    .Model(model)
    .Title("Tutorial 3")
    .EditConfiguration(customEditConfiguration)
    .Build();