This tutorial explains how you can create a handles provider.

The goal of this tutorial is to add support for editing a CircleByCenterPointCircleByCenterPointCircleByCenterPoint 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 code snippets in this tutorial are available in the editing sample.

See this article for more information about editing.

Step 1 - Add support for translation

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

This sample code shows how you can do that:

Program: Add support for translation
bool CircleBy2PointsHandlesProvider::canProvide(const std::shared_ptr<Observable<std::shared_ptr<Geometry>>>& 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<Observable<std::shared_ptr<Geometry>>> 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 = ObservableGeometryUtil::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));
}
public bool CanProvide(Observable<Geometry> geometry, FeatureEditContext context)
{
    return geometry.Value is CircleByCenterPoint;
}
public ITranslateEditAction ProvideTranslateAction(Observable<Geometry> 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 = ObservableGeometryUtil.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);
}
override fun canProvide(geometry: Observable<Geometry?>, context: FeatureEditContext): Boolean {
    return geometry.value is CircleByCenterPoint
}
override fun provideTranslateAction(
    geometry: Observable<Geometry?>,
    context: FeatureEditContext,
    geometryEditCallback: IGeometryEditCallback
): ITranslateEditAction {
    // Derive a circle. This method should only be called when canProvide is true, so we can assume that it is a CircleByCenterPoint
    val observableCircle = ObservableGeometryUtil.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 CircleTranslateAction(observableCircle, geometryEditCallback)
}

We use this action:

Program: ITranslateEditAction implementation
class CircleTranslateAction final : public ITranslateEditAction {
public:
  CircleTranslateAction(std::shared_ptr<Observable<std::shared_ptr<CircleByCenterPoint>>> 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<Observable<std::shared_ptr<CircleByCenterPoint>>> _circle;
  std::shared_ptr<IGeometryEditCallback> _callback;

  std::shared_ptr<CircleByCenterPoint> _initialCircle;
};
private sealed class CircleTranslateAction : ITranslateEditAction
{
    public CircleTranslateAction(Observable<CircleByCenterPoint> 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 Observable<CircleByCenterPoint> _circle;
    private readonly IGeometryEditCallback _callback;
    private CircleByCenterPoint _initialCircle;
};
private class CircleTranslateAction(
    private val circle: Observable<CircleByCenterPoint?>,
    private val callback: IGeometryEditCallback
) :
    ITranslateEditAction {
    private var initialCircle: CircleByCenterPoint? = null
    override fun translate(
        translation: Coordinate,
        translateStatus: EventStatus,
        changeStatus: ChangeStatus
    ) {
        if (translateStatus == EventStatus.Start) {
            initialCircle = circle.value
        }
        val newCenter = initialCircle!!.center.add(translation)
        val newCircle = GeometryFactory.createCircleByCenterPoint(
            initialCircle!!.reference, newCenter, initialCircle!!.radius
        )
        callback.onEdit(newCircle, changeStatus)
    }
}

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 IEditHandlesIEditHandlesIEditHandles implementation.

  • 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: 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<Observable<std::shared_ptr<CircleByCenterPoint>>>& circle,
                                                           const std::shared_ptr<IGeometryEditCallback>& editCallback) {
  auto handleLocation = ObservableCircleByCenterPointUtil::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;
}
    _radiusHandle = CreateRadiusHandle(_circle, geometryEditCallback);
public IList<IEditHandle> GetList()
{
    return new List<IEditHandle> {_radiusHandle};
}
private sealed class RadiusPointEditAction : IPointEditAction
{
    private readonly Observable<CircleByCenterPoint> _circle;
    private readonly IGeometryEditCallback _editCallback;

    public RadiusPointEditAction(Observable<CircleByCenterPoint> 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(Observable<CircleByCenterPoint> circle, IGeometryEditCallback editCallback)
{
    var handleLocation = ObservableCircleByCenterPointUtil.DerivePointAtAngle(circle, 0);
    var action = new RadiusPointEditAction(circle, editCallback);

    var handle = new PointEditHandle(handleLocation);
    handle.AddOnDragAction(action).Cursor(MouseCursor.Cross);
    return handle;
}
    radiusHandle = createRadiusHandle(circle, geometryEditCallback)
override fun getList(): List<IEditHandle> {
    return listOf<IEditHandle>(radiusHandle)
}
private class RadiusPointEditAction(
    private val circle: Observable<CircleByCenterPoint?>,
    private val editCallback: IGeometryEditCallback
) :
    IPointEditAction {
    override fun execute(
        location: Point,
        eventStatus: EventStatus,
        changeStatus: ChangeStatus
    ) {
        execute(location, changeStatus)
    }

    override fun execute(location: Point, changeStatus: ChangeStatus) {
        // Create a new circle based on the new radius of the handle, and inform the IGeometryEditCallback
        val currentCircle = circle.value
        val calculations = GeodesyCalculations.create(
            currentCircle!!.reference,
            LineInterpolationType.Geodesic
        )
        val newRadius = calculations.distance2D(currentCircle.center, location.location)!!
        val newCircle = GeometryFactory.createCircleByCenterPoint(
            currentCircle.reference, currentCircle.center, newRadius
        )
        editCallback.onEdit(newCircle, changeStatus)
    }
}

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 CircleByCenterPointCircleByCenterPointCircleByCenterPoint, so we’re using an ObservableObservableObservable for a CircleByCenterPointCircleByCenterPointCircleByCenterPoint.

This code shows how to detect changes:

Program: Detect changes to the edited CircleByCenterPointCircleByCenterPointCircleByCenterPoint
CircleBy2PointsEditHandles(std::shared_ptr<Observable<std::shared_ptr<CircleByCenterPoint>>> 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);
}
public CircleBy2PointsEditHandles(Observable<CircleByCenterPoint> circle, IGeometryEditCallback geometryEditCallback)
{
    _circle = circle;
    IInvalidationCallback circleCallback = new CircleInvalidateCallback(circle, this);
    _circle.AddCallback(circleCallback);
}

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

    public CircleInvalidateCallback(Observable<CircleByCenterPoint> 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();
    }
}
init {
    val circleCallback: IInvalidationCallback = CircleInvalidateCallback(circle, this)
    circle.addCallback(circleCallback)
}

private class CircleInvalidateCallback(
    private val circle: Observable<CircleByCenterPoint?>,
    editHandles: CircleBy2PointsEditHandles
) :
    IInvalidationCallback {
    private val editHandles: WeakReference<CircleBy2PointsEditHandles>

    init {
        this.editHandles = WeakReference(editHandles)
    }

    override fun onInvalidate() {
        val editHandles = editHandles.get()
        if (editHandles == null) {
            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 CircleByCenterPointCircleByCenterPointCircleByCenterPoint. 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 IEditHandlesIEditHandlesIEditHandles implementation notifies the registered observersobserversobservers 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 FeatureFeatureFeature doesn’t contain a CircleByCenterPointCircleByCenterPointCircleByCenterPoint anymore, but that another geometry type is required.

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

Program: 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();
  }
}
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();
    }
}
private fun 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 fun fireInvalidHandlesEvent() {
    val observers: List<IEditHandlesObserver> = ArrayList(
        observers
    )
    for (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: Add support for the new handles
std::shared_ptr<IEditHandles> CircleBy2PointsHandlesProvider::provide(std::shared_ptr<Observable<std::shared_ptr<Geometry>>> 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 = ObservableGeometryUtil::deriveCircleByCenterPoint(geometry);

  // Create a set of handles for this circle
  return std::make_shared<CircleBy2PointsEditHandles>(observableCircle, geometryEditCallback);
}
public IEditHandles Provide(Observable<Geometry> 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 = ObservableGeometryUtil.DeriveCircleByCenterPoint(geometry);

    // Create a set of handles for this circle
    return new CircleBy2PointsEditHandles(observableCircle, geometryEditCallback);
}
override fun provide(
    geometry: Observable<Geometry?>,
    context: FeatureEditContext,
    geometryEditCallback: IGeometryEditCallback
): IEditHandles {
    // Derive a circle. This method should only be called when canProvide is true, so we can assume that it is a CircleByCenterPoint
    val observableCircle = ObservableGeometryUtil.deriveCircleByCenterPoint(geometry)

    // Create a set of handles for this circle
    return 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 IFeatureEditConfigurationIFeatureEditConfigurationIFeatureEditConfiguration, and set that configuration on the FeatureLayerFeatureLayerFeatureLayer.

Program: 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();
// 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();
// Use a custom edit configuration that changes the editing behavior for this layer
val customEditConfiguration = CustomEditConfiguration()

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