This tutorial explains how you can create a handles provider.
The goal of this tutorial is to add support for editing a CircleByCenterPoint
CircleByCenterPoint
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 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 CircleByCenterPoint
CircleByCenterPoint
CircleByCenterPoint
geometry, we create a new geometry handles provider
geometry handles provider
geometry handles provider
implementation. To add support for translation, we must implement these methods:
-
IGeometryHandlesProvider::canProvide
IGeometryHandlesProvider::canProvide
IGeometryHandlesProvider::canProvide
: to make sure that the new handle provider implementation is used during the editing of aCircleByCenterPoint
CircleByCenterPoint
CircleByCenterPoint
. -
IGeometryHandlesProvider::provideTranslateAction
IGeometryHandlesProvider::provideTranslateAction
IGeometryHandlesProvider::provideTranslateAction
: this method creates a translate action that afeature handles provider
feature handles provider
feature handles provider
can use. In this tutorial, we use theFeatureHandlesProvider
FeatureHandlesProvider
FeatureHandlesProvider
implementation. This implementation calls theprovideTranslateAction
method, and uses the returned action to create a handle that can translate the feature.
This sample code shows how you can do that:
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:
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
IEditHandles
IEditHandles
IEditHandles
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:
_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 CircleByCenterPoint
CircleByCenterPoint
CircleByCenterPoint
, so we’re using an Observable
Observable
Observable
for a CircleByCenterPoint
CircleByCenterPoint
CircleByCenterPoint
.
This code shows how to detect changes:
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
CircleByCenterPoint
CircleByCenterPoint
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
IEditHandles
IEditHandles
implementation notifies the registered observers
observers
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
Feature
Feature
doesn’t contain a CircleByCenterPoint
CircleByCenterPoint
CircleByCenterPoint
anymore, but that another geometry type is required.
An IEditHandles
IEditHandles
IEditHandles
implementation can send a notification that it isn’t suitable anymore for the current geometry.
This sample code demonstrates such a notification:
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:
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 IFeatureEditConfiguration
IFeatureEditConfiguration
IFeatureEditConfiguration
, and set that configuration
on the FeatureLayer
FeatureLayer
FeatureLayer
.
// 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()