hippodrome
Figure 1. An example of a hippodrome with edithandles

This tutorial explains how you can create a handles provider.

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

  • A translate handle

  • A point handle to change the start point of the hippodrome

  • A point handle to change the end point of the hippodrome

  • A line handle to change the radius of the hippodrome

A hippodrome is represented by a CompositeRingCompositeRingCompositeRing that is built up of 2 lineslineslines and 2 circular arcscircular arcscircular arcs

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 hippodrome that we represent by a CompositeRingCompositeRingCompositeRing geometry, we create a new geometry handles providergeometry handles providergeometry handles provider implementation and implement these methods:

This sample code shows how you can do that:

Program: Add support for translation
bool HippodromeHandlesProvider::canProvide(const std::shared_ptr<Observable<std::shared_ptr<Geometry>>>& geometry,
                                           const std::shared_ptr<FeatureEditContext>& /*context*/) const {
  const auto *hippoDrome = dynamic_cast<CompositeRing*>(geometry->getValue().get());
  if (hippoDrome == nullptr || hippoDrome->getCurveCount() != 4) {
    return false;
  }

  // A hippodrome is built up of a line, circular arc, a line and then a circular arc again. If the shape is not built in this order it is not a hippodrome.
  const auto *line1 = dynamic_cast<Line*>(hippoDrome->getCurveAt(0).get());
  const auto *curveArc1 = dynamic_cast<CircularArcByCenterPoint*>(hippoDrome->getCurveAt(1).get());
  const auto *line2 = dynamic_cast<Line*>(hippoDrome->getCurveAt(2).get());
  const auto *curveArc2 = dynamic_cast<CircularArcByCenterPoint*>(hippoDrome->getCurveAt(3).get());

  return line1 && line2 && curveArc1 && curveArc2;
}

std::shared_ptr<ITranslateEditAction> HippodromeHandlesProvider::provideTranslateAction(
  std::shared_ptr<Observable<std::shared_ptr<Geometry>>> geometry,
  const std::shared_ptr<FeatureEditContext>& /*context*/,
  std::shared_ptr<IGeometryEditCallback> geometryEditCallback) const {
  auto observableHippodrome = deriveHippodrome(geometry);

  return std::make_shared<HippodromeTranslateAction>(observableHippodrome, std::move(geometryEditCallback));
}
public bool CanProvide(Observable<Geometry> geometry, FeatureEditContext context)
{
    CompositeRing hippodrome = geometry.Value as CompositeRing;
    if (hippodrome == null || hippodrome.CurveCount != 4)
    {
        return false;
    }

    return geometry.Value is CompositeRing;
}
public ITranslateEditAction ProvideTranslateAction(Observable<Geometry> geometry, FeatureEditContext context, IGeometryEditCallback geometryEditCallback)
{
    // Derive a composite ring. This method should only be called when canProvide is true, so we can assume that it is a Hippodrome
    var observableHippodrome = Observable<CompositeRing>.Create(geometry.Value as CompositeRing);

    // Create a translate action for the composite ring. This can be used by FeatureHandlesProvider (or a custom IFeatureHandlesProvider)
    // to create a translate handle.
    return new HippodromeTranslateAction(observableHippodrome, geometryEditCallback);
}
override fun canProvide(geometry: Observable<Geometry?>, context: FeatureEditContext): Boolean {
    if (!(geometry.value!! is CompositeRing)) {
        return false
    }

    val hippodrome = geometry.value!! as CompositeRing
    if (hippodrome.curveCount.toInt() != 4) {
        return false
    }

    return true
}
override fun provideTranslateAction(
    geometry: Observable<Geometry?>,
    context: FeatureEditContext,
    geometryEditCallback: IGeometryEditCallback
): ITranslateEditAction {
    val observableHippodrome = Observable(geometry!!.value as CompositeRing)

    return HippodromeTranslateAction(observableHippodrome, geometryEditCallback)
}

We use this action:

Program: ITranslateEditAction implementation
class HippodromeTranslateAction final : public ITranslateEditAction {
public:
  HippodromeTranslateAction(std::shared_ptr<Observable<std::shared_ptr<CompositeRing>>> hippodrome, std::shared_ptr<IGeometryEditCallback> callback)
      : _hippodrome(std::move(hippodrome)), _callback(std::move(callback)) {
  }

  void translate(Coordinate translation, EventStatus translateStatus, ChangeStatus changeStatus) override {
    if (translateStatus == EventStatus::Start) {
      _initialHippodrome = _hippodrome->getValue();
    }
    auto newStartPosition = getStartPoint(_initialHippodrome) + translation;
    auto newEndPosition = getEndPoint(_initialHippodrome) + translation;
    auto newHippodrome = Hippodrome::create(newStartPosition, newEndPosition, getRadius(_initialHippodrome), _initialHippodrome->getReference());
    _callback->onEdit(newHippodrome, changeStatus);
  }

private:
  std::shared_ptr<Observable<std::shared_ptr<CompositeRing>>> _hippodrome;
  std::shared_ptr<IGeometryEditCallback> _callback;

  std::shared_ptr<CompositeRing> _initialHippodrome;
};
private sealed class HippodromeTranslateAction : ITranslateEditAction
{
    public HippodromeTranslateAction(Observable<CompositeRing> hippodrome, IGeometryEditCallback callback)
    {
        _hippodrome = hippodrome;
        _callback = callback;
    }

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

        var newStartPoint = Hippodrome.GetStartPoint(_initialHippodrome) + translation;
        var newEndPoint = Hippodrome.GetEndPoint(_initialHippodrome) + translation;
        var newHippodrome = Hippodrome.Create(newStartPoint, newEndPoint, Hippodrome.GetRadius(_initialHippodrome), _initialHippodrome.Reference);
        _callback.OnEdit(newHippodrome, changeStatus);
    }

    private readonly Observable<CompositeRing> _hippodrome;
    private readonly IGeometryEditCallback _callback;
    private CompositeRing _initialHippodrome;
};
private class HippodromeTranslateAction(
    private val hippodrome: Observable<CompositeRing?>,
    private val callback: IGeometryEditCallback
) :
    ITranslateEditAction {
    private var initialHippodrome: CompositeRing? = null
    override fun translate(
        translation: Coordinate,
        translateStatus: EventStatus,
        changeStatus: ChangeStatus
    ) {
        if (translateStatus == EventStatus.Start) {
            initialHippodrome = hippodrome.value
        }

        val newStartPosition = Hippodrome.getStartPoint(initialHippodrome!!).add(translation)
        val newEndPosition = Hippodrome.getEndPoint(initialHippodrome!!).add(translation)
        val radius = Hippodrome.getRadius(initialHippodrome!!)
        val newHippodrome = Hippodrome.create(
            newStartPosition,
            newEndPosition,
            radius,
            initialHippodrome!!.reference
        )

        callback.onEdit(newHippodrome, 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 2 point handles to edit the start and end points of the hippodrome, and a line handle to edit the radius of the hippodrome.

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 start point, end point and radius handle and make sure that the IEditHandles::getList method returns them. This sample code shows how you can do that:

Program: Add start point handle
  _startPointHandle = createStartPointHandle(_hippodrome, callback);

static std::shared_ptr<PointEditHandle> createStartPointHandle(const std::shared_ptr<Observable<std::shared_ptr<CompositeRing>>>& observableHippodrome,
                                                               const std::shared_ptr<IGeometryEditCallback>& editCallback) {
  auto hippodrome = observableHippodrome->getValue();
  auto startPointProvider = Observable<std::shared_ptr<Point>>::create(GeometryFactory::createPoint(hippodrome->getReference(), getStartPoint(hippodrome)));

  auto action = IPointEditAction::create([editCallback, observableHippodrome](const std::shared_ptr<Point>& location, ChangeStatus changeStatus) {
    auto currentHippoDrome = observableHippodrome->getValue();
    auto newHippodrome = Hippodrome::create(location->getLocation(), getEndPoint(currentHippoDrome), getRadius(currentHippoDrome),
                                            currentHippoDrome->getReference());
    editCallback->onEdit(newHippodrome, changeStatus);
  });

  auto handle = std::make_shared<PointEditHandle>(startPointProvider);
  handle->addOnDragAction(action).cursor(MouseCursor::cross());
  return handle;
}
    _startPointHandle = CreateStartPointHandle(_hippodrome, geometryEditCallback);
private sealed class StartPointEditAction : IPointEditAction
{
    private readonly Observable<Geometry> _hippodrome;
    private readonly IGeometryEditCallback _editCallback;

    public StartPointEditAction(Observable<Geometry> hippodrome, IGeometryEditCallback editCallback)
    {
        _hippodrome = hippodrome;
        _editCallback = editCallback;
    }

    public void Execute(Point location, ChangeStatus changeStatus)
    {
        // Create a new hippodrome based on the new radius of the handle, and inform the IGeometryEditCallback
        var currentHippodrome = _hippodrome.Value;
        var calculations = GeodesyCalculations.Create(currentHippodrome.Reference, LineInterpolationType.Geodesic);

        var endPoint = Hippodrome.GetEndPoint(currentHippodrome as CompositeRing);
        var radius = Hippodrome.GetRadius(currentHippodrome as CompositeRing);

        var newHippodrome = Hippodrome.Create(location.Location, endPoint, radius, currentHippodrome.Reference);
        _editCallback.OnEdit(newHippodrome, changeStatus);
    }

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

private static Point DeriveStartPoint(CompositeRing hippodrome)
{
    return new Point(hippodrome.Reference, Hippodrome.GetStartPoint(hippodrome));
}

private static PointEditHandle CreateStartPointHandle(Observable<Geometry> hippodrome, IGeometryEditCallback editCallback)
{
    var startPoint = Observable<Point>.Create(DeriveStartPoint(hippodrome.Value as CompositeRing));
    var action = new StartPointEditAction(hippodrome, editCallback);

    var handle = new PointEditHandle(startPoint);
    handle.AddOnDragAction(action).Cursor(MouseCursor.Cross);
    return handle;
}
    startPointHandle = createStartPointHandle(hippodrome, geometryEditCallback)
private class StartPointEditAction(
    private val hippodrome: Observable<Geometry?>,
    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 hippodrome with a new start point based on the handle, and inform the IGeometryEditCallback
        val currentHippodrome = hippodrome.value as CompositeRing

        val startPoint = location.location
        val endPoint = Hippodrome.getEndPoint(currentHippodrome)
        val radius = Hippodrome.getRadius(currentHippodrome)

        val newHippodrome = Hippodrome.create(
            startPoint, endPoint, radius, currentHippodrome.reference
        )

        editCallback.onEdit(newHippodrome, changeStatus)
    }
}
    private fun deriveStartPoint(hippodrome: CompositeRing): Point {
        return Point(hippodrome.reference, Hippodrome.getStartPoint(hippodrome))
    }
    private fun createStartPointHandle(
        hippodrome: Observable<Geometry?>,
        editCallback: IGeometryEditCallback
    ): PointEditHandle {
        val handleLocation =
            Observable.create(deriveStartPoint(hippodrome.value!! as CompositeRing))
        val action =
            samples.editing.tutorial3.HippodromeHandlesProvider.HippodromeEditHandles.StartPointEditAction(
                hippodrome,
                editCallback
            )
        val handle = PointEditHandle(handleLocation)
        handle.addOnDragAction(action).cursor(MouseCursor.Cross)
        return handle
    }

The end point is the same as the start point handle, the only difference being which point it uses.

Program: Add radius handle
  _radiusHandle = createRadiusHandle(_hippodrome, callback);

static std::shared_ptr<LineEditHandle> createRadiusHandle(const std::shared_ptr<Observable<std::shared_ptr<CompositeRing>>>& observableHippodrome,
                                                          const std::shared_ptr<IGeometryEditCallback>& editCallback) {
  auto radiusProvider = Observable<std::shared_ptr<Geometry>>::create(observableHippodrome->getValue());

  auto action = IPointEditAction::create([editCallback, observableHippodrome](const std::shared_ptr<Point>& location, ChangeStatus changeStatus) {
    auto currentHippodrome = observableHippodrome->getValue();
    auto calculations = GeodesyCalculations::create(currentHippodrome->getReference(), LineInterpolationType::Geodesic);
    auto newRadius = *calculations->distance2D(location->getLocation(), closestPointOnSegment(location->getLocation(), getStartPoint(currentHippodrome),
                                                                                              getEndPoint(currentHippodrome)));

    auto newHippodrome = Hippodrome::create(getStartPoint(currentHippodrome), getEndPoint(currentHippodrome), newRadius, currentHippodrome->getReference());
    editCallback->onEdit(newHippodrome, changeStatus);
  });

  auto handle = std::make_shared<LineEditHandle>(radiusProvider);
  handle->addOnDragAction(action).cursor(MouseCursor::sizeHorizontal());
  return handle;
}
    _radiusHandle = CreateRadiusHandle(_hippodrome, geometryEditCallback);
private sealed class RadiusPointEditAction : IPointEditAction
{
    private readonly Observable<Geometry> _hippodrome;
    private readonly IGeometryEditCallback _editCallback;

    public RadiusPointEditAction(Observable<Geometry> hippodrome, IGeometryEditCallback editCallback)
    {
        _hippodrome = hippodrome;
        _editCallback = editCallback;
    }

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

    public void Execute(Point location, ChangeStatus changeStatus)
    {
        // Create a new hippodrome based on the new radius of the handle, and inform the IGeometryEditCallback
        var currentHippodrome = _hippodrome.Value;
        var calculations = GeodesyCalculations.Create(currentHippodrome.Reference, LineInterpolationType.Geodesic);

        var startPoint = Hippodrome.GetStartPoint(currentHippodrome as CompositeRing);
        var endPoint = Hippodrome.GetEndPoint(currentHippodrome as CompositeRing);

        var newRadius = calculations.Distance2D(Hippodrome.ClosestPointOnSegment(location.Location, startPoint, endPoint), location.Location).Value;
        var newHippodrome = Hippodrome.Create(startPoint, endPoint, newRadius, currentHippodrome.Reference);
        _editCallback.OnEdit(newHippodrome, changeStatus);
    }
}

private static LineEditHandle CreateRadiusHandle(Observable<Geometry> hippodrome, IGeometryEditCallback editCallback)
{
    var outline = Observable<Geometry>.Create(hippodrome.Value as Geometry);
    var action = new RadiusPointEditAction(hippodrome, editCallback);

    var handle = new LineEditHandle(outline);
    handle.AddOnDragAction(action).Cursor(MouseCursor.Cross);
    return handle;
}
    radiusHandle = createRadiusHandle(hippodrome, geometryEditCallback)
private class RadiusPointEditAction(
    private val hippodrome: Observable<Geometry?>,
    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 hippodrome based on the new radius of the handle, and inform the IGeometryEditCallback
        val currentHippodrome = hippodrome.value as CompositeRing
        val calculations = GeodesyCalculations.create(
            currentHippodrome.reference,
            LineInterpolationType.Geodesic
        )

        val startPoint = Hippodrome.getStartPoint(currentHippodrome)
        val endPoint = Hippodrome.getEndPoint(currentHippodrome)
        val newRadius = calculations.distance2D(
            Hippodrome.ClosestPointOnSegment(
                location.location,
                startPoint,
                endPoint
            ), location.location
        )!!

        val newHippodrome = Hippodrome.create(
            startPoint, endPoint, newRadius, currentHippodrome.reference
        )

        editCallback.onEdit(newHippodrome, changeStatus)
    }
}
    private fun createRadiusHandle(
        hippodrome: Observable<Geometry?>,
        editCallback: IGeometryEditCallback
    ): LineEditHandle {
        val action =
            samples.editing.tutorial3.HippodromeHandlesProvider.HippodromeEditHandles.RadiusPointEditAction(
                hippodrome,
                editCallback
            )
        val handle = LineEditHandle(hippodrome)
        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 hippodrome, so we’re using an ObservableObservableObservable for a CompositeRingCompositeRingCompositeRing.

This code shows how to detect changes:

Program: Detect changes to the edited hippodrome
HippodromeEditHandles(std::shared_ptr<Observable<std::shared_ptr<CompositeRing>>> hippodrome, const std::shared_ptr<IGeometryEditCallback>& callback)
    : _hippodrome{std::move(hippodrome)} {
  _hippodromeCallback = IInvalidationCallback::create([&]() {
    onGeometryChanged();

    auto hippodromeValue = _hippodrome->getValue();
    _startPointHandle->getLocationProvider()->setValue(GeometryFactory::createPoint(hippodromeValue->getReference(), getStartPoint(hippodromeValue)));
    _endPointHandle->getLocationProvider()->setValue(GeometryFactory::createPoint(hippodromeValue->getReference(), getEndPoint(hippodromeValue)));
    _radiusHandle->getGeometryProvider()->setValue(hippodromeValue);
  });
  _hippodrome->addCallback(_hippodromeCallback);
}
public HippodromeEditHandles(Observable<Geometry> hippodrome, IGeometryEditCallback geometryEditCallback)
{
    _hippodrome = hippodrome;

    IInvalidationCallback hippodromeCallback = new HippodromeInvalidateCallback(hippodrome, this,
        _radiusHandle.GeometryProvider, _startPointHandle.LocationProvider, _endPointHandle.LocationProvider);
    _hippodrome.AddCallback(hippodromeCallback);
}

private sealed class HippodromeInvalidateCallback : IInvalidationCallback
{
    private readonly Observable<Geometry> _hippodrome;
    private readonly WeakReference<HippodromeEditHandles> _editHandles;

    private readonly Observable<Geometry> _radiusHandleGeometry;
    private readonly Observable<Point> _startPointHandle;
    private readonly Observable<Point> _endPointHandle;

    public HippodromeInvalidateCallback(Observable<Geometry> hippodrome, HippodromeEditHandles editHandles, Observable<Geometry> radiusHandleGeometry, Observable<Point> startPointHandle, Observable<Point> endPointHandle)
    {
        _hippodrome = hippodrome;
        _editHandles = new WeakReference<HippodromeEditHandles>(editHandles);
        _radiusHandleGeometry = radiusHandleGeometry;
        _startPointHandle = startPointHandle;
        _endPointHandle = endPointHandle;
    }

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

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

        _radiusHandleGeometry.Value = _hippodrome.Value;

        _startPointHandle.Value = DeriveStartPoint(_hippodrome.Value as CompositeRing);
        _endPointHandle.Value = DeriveEndPoint(_hippodrome.Value as CompositeRing);
    }
}
init {
    val hippodromeCallback: IInvalidationCallback =
        HippodromeInvalidateCallback(
            hippodrome,
            this,
            startPointHandle.locationProvider,
            endPointHandle.locationProvider
        )
    hippodrome.addCallback(hippodromeCallback)
}

private class HippodromeInvalidateCallback(
    private val hippodrome: Observable<Geometry?>,
    editHandles: HippodromeEditHandles,
    private val startPointHandle: Observable<Point?>,
    private val endPointHandle: Observable<Point?>,
) :
    IInvalidationCallback {
    private val editHandles: WeakReference<HippodromeEditHandles>

    init {
        this.editHandles = WeakReference(editHandles)
    }

    override fun onInvalidate() {
        val editHandles = editHandles.get()
        if (editHandles == null) {
            hippodrome.removeCallback(this)
            return
        }

        // Handle all possible changes to the geometry
        editHandles.onGeometryChanged()

        startPointHandle.value = deriveStartPoint(hippodrome.value as CompositeRing)
        endPointHandle.value = deriveEndPoint(hippodrome.value as CompositeRing)

    }
}

In this tutorial, we only need an action to check if the edited geometry is still a CompositeRingCompositeRingCompositeRing. 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 CompositeRingCompositeRingCompositeRing anymore, but that another geometry type is required.

A CompositeRingCompositeRingCompositeRing 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 fireInvalidHandlesEvent() {
  auto observers = _observers;
  for (const auto& observer : observers) {
    observer->onEditHandlesInvalid();
  }
}
private void OnGeometryChanged()
{
    if (_hippodrome.Value == null)
    {
        // This set of handles should not be used anymore since the current geometry is not a Hippodrome anymore.
        FireInvalidHandlesEvent();
    }
}
private void FireInvalidHandlesEvent()
{
    var observers = new List<IEditHandlesObserver>(_observers);
    foreach (var observer in observers)
    {
        observer.OnEditHandlesInvalid();
    }
}
private fun onGeometryChanged() {
    if (hippodrome.value == null) {
        // This set of handles should not be used anymore since the current geometry is not a Hippodrome 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> HippodromeHandlesProvider::provide(
  std::shared_ptr<Observable<std::shared_ptr<Geometry>>> geometry,
  const std::shared_ptr<FeatureEditContext>& /*context*/,
  std::shared_ptr<IGeometryEditCallback> geometryEditCallback) const {
  auto observableHippodrome = deriveHippodrome(geometry);

  return std::make_shared<HippodromeEditHandles>(observableHippodrome, geometryEditCallback);
}
public IEditHandles Provide(Observable<Geometry> geometry, FeatureEditContext context, IGeometryEditCallback geometryEditCallback)
{
    // Create a set of handles for this hippodrome
    return new HippodromeEditHandles(geometry, geometryEditCallback);
}
override fun provide(
    geometry: Observable<Geometry?>,
    context: FeatureEditContext,
    geometryEditCallback: IGeometryEditCallback
): IEditHandles {
    return HippodromeEditHandles(geometry, 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
class CustomEditConfiguration final : public IFeatureEditConfiguration {
public:
  CustomEditConfiguration() {
    // Create an IGeometryHandlesFactory that can also edit a hippodrome using 2 point handles and a line handle
    auto hippodromeHandlesProvider = std::make_shared<HippodromeHandlesProvider>();
    auto compositeHandlesProvider = CompositeGeometryHandlesProvider::createDefault();
    compositeHandlesProvider->add(hippodromeHandlesProvider, Priority::high());

    // Make sure the FeatureHandlesProvider uses this custom IGeometryHandlesFactory
    _featureHandlesProvider = std::make_shared<FeatureHandlesProvider>();
    _featureHandlesProvider->setGeometryHandlesProvider(compositeHandlesProvider);
  }

  void edit(const Feature& /*feature*/, LayerId /*layerId*/, const std::shared_ptr<Map>& /*map*/, FeatureEditConfigurationBuilder& builder) const override {
    // Use a custom handles provider for all features
    builder.handlesProvider(_featureHandlesProvider).submit();
  }

private:
  std::shared_ptr<FeatureHandlesProvider> _featureHandlesProvider;
};
// 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();
private sealed class CustomEditConfiguration : IFeatureEditConfiguration
{
    private readonly FeatureHandlesProvider _featureHandlesProvider;

    public void Edit(Feature feature, ulong layerId, Map map, FeatureEditConfigurationBuilder builder)
    {
        // Use a custom handles provider for all features
        builder.HandlesProvider(_featureHandlesProvider).Submit();
    }
}
    // 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();
private class CustomEditConfiguration : IFeatureEditConfiguration {
    private val featureHandlesProvider: FeatureHandlesProvider

    init {
        // Create an IGeometryHandlesFactory that can also edit a hippodrome using 2 point handles and a line handle
        val compositeHandlesProvider = CompositeGeometryHandlesProvider.createDefault()
        compositeHandlesProvider.add(HippodromeHandlesProvider, Priority.High)

        // Make sure the FeatureHandlesProvider uses this custom IGeometryHandlesFactory
        featureHandlesProvider = FeatureHandlesProvider()
        featureHandlesProvider.geometryHandlesProvider = compositeHandlesProvider
    }

    override fun edit(
        feature: Feature,
        layerId: Long,
        map: Map,
        builder: FeatureEditConfigurationBuilder
    ) {
        // Use a custom handles provider for all features
        builder.handlesProvider(featureHandlesProvider).submit()
    }
}
    // 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()
private fun createFeatureModel(): IFeatureModel {
    val reference = CoordinateReferenceProvider.create("EPSG:4326")

    // Add a predefined feature to demonstrate the custom editing capabilities
    val hippodrome = Hippodrome.create(
        Coordinate(30.0, -7.0),
        Coordinate(50.0, -7.0),
        500000.0,
        reference
    )
    val feature = createFeature(hippodrome, 0)

    // Create an editable model
    return createModel(reference, listOf(feature))
}