Select to view content in your preferred language

First Edit Operation Fails

101
2
Jump to solution
a week ago
GobiGobletsson
New Contributor

Hi Community, 

I have created an Add-in - In which whenever the button is active, when you create a polyline(Plan_Kabler) point features are placed along the line alongside with polygons. These are directional according to the polyline segments. Points are referred to as muffePunktLayer and the polygons are referred to as muffeArealLayer. 

When the polyline is edited or deleted, it deletes the original placements, and places new ones along the line if its edited, and none if its been deleted. 

No matter how much I try - I cannot figure out why the first operation of edit always fails. 

i.e: I place the polyline, and given either muffePunktLayer or muffeArealLayer is placed before the other, that first feature that is being placed, fails, but the remaining of the features succeed to get placed. 

Similarly when it either gets deleted or edited, the first feature to be deleted fails to be deleted, but then in return the first feature to be placed succeeds, unlike when the polyline is first inserted.  I am really at a loss to what the mistake could be, and would need some assistance. 

Button1.cs. below; 

Spoiler

using ArcGIS.Core.CIM;
using ArcGIS.Core.Data;
using ArcGIS.Core.Events;
using ArcGIS.Core.Geometry;
using ArcGIS.Core.Internal.Geometry;
using ArcGIS.Desktop.Editing;
using ArcGIS.Desktop.Editing.Events;
using ArcGIS.Desktop.Framework.Contracts;
using ArcGIS.Desktop.Framework.Threading.Tasks;
using ArcGIS.Desktop.Mapping;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace muffe_add_in
{
internal class Button1 : Button
{
private SubscriptionToken _rowCreatedEventToken;
private SubscriptionToken _rowChangedEventToken;
private SubscriptionToken _rowDeletedEventToken; // New token for RowDeletedEvent
private bool _isActive = false; // Field to track the active state
private Dictionary<long, Geometry> _originalGeometries = new Dictionary<long, Geometry>();

protected override void OnClick()
{
_isActive = !_isActive; // Toggle the active state

if (_isActive)
{
// Ensure the subscription logic is awaited properly
QueuedTask.Run(async () =>
{
try
{
// Assuming you have a way to access or select the feature layer you're working with
var featureLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>().FirstOrDefault(l => l.Name == "Plan Kabler");
if (featureLayer != null)
{
// Correctly obtain the spatial reference from the feature layer's dataset
var spatialReference = await QueuedTask.Run(() => featureLayer.GetFeatureClass().GetDefinition().GetSpatialReference());

_rowCreatedEventToken = RowCreatedEvent.Subscribe((args) => OnRowCreatedOrEdited(args, true), featureLayer.GetTable(), false);
_rowChangedEventToken = RowChangedEvent.Subscribe((args) => OnRowCreatedOrEdited(args, false), featureLayer.GetTable(), false);
_rowDeletedEventToken = RowDeletedEvent.Subscribe(OnRowDeleted, featureLayer.GetTable(), false); // Subscribe to RowDeletedEvent
}
}
catch (Exception ex)
{
// Log the exception to the ArcGIS Pro log or show a message box
System.Diagnostics.Debug.WriteLine($"Error subscribing to events: {ex.Message}");
}
}).Wait();
UpdateCaption("Deactivate Muffe");
}
else
{
// Unsubscribe logic should also be within QueuedTask.Run if it interacts with ArcGIS objects
QueuedTask.Run(() =>
{
RowCreatedEvent.Unsubscribe(_rowCreatedEventToken);
RowChangedEvent.Unsubscribe(_rowChangedEventToken);
RowDeletedEvent.Unsubscribe(_rowDeletedEventToken); // Unsubscribe from RowDeletedEvent
_rowCreatedEventToken = null;
_rowChangedEventToken = null;
_rowDeletedEventToken = null;
}).Wait();
UpdateCaption("Activate Muffe");
}
}

private void UpdateCaption(string newCaption)
{
// Directly update the caption since this method is called within QueuedTask.Run
this.Caption = newCaption;
}

private async void OnRowCreatedOrEdited(RowChangedEventArgs args, bool isInsert)
{
if (!_isActive) return; // Check if the script is active before proceeding
if (!(args.Row is Feature feature)) return;
if (feature.GetTable().GetName() != "Plan_Kabler") return;

var featureId = feature.GetObjectID();
var polyline = feature.GetShape() as Polyline;
if (polyline == null) return;

// If there is an existing geometry, delete it
if (_originalGeometries.ContainsKey(featureId))
{
await QueuedTask.Run(async () =>
{
var muffePunktLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>().FirstOrDefault(l => l.Name == "Muffe Punkt");
var muffeArealLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>().FirstOrDefault(l => l.Name == "Muffe Areal");
if (muffePunktLayer == null || muffeArealLayer == null) return;

// Delete intersecting templates
await DeleteIntersectingTemplates(featureId, muffePunktLayer, muffeArealLayer);
});

// Remove the old geometry from the dictionary
_originalGeometries.Remove(featureId);
}

// Store the new geometry
_originalGeometries[featureId] = polyline.Clone();

await QueuedTask.Run(async () =>
{
var muffePunktLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>().FirstOrDefault(l => l.Name == "Muffe Punkt");
var muffeArealLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>().FirstOrDefault(l => l.Name == "Muffe Areal");
if (muffePunktLayer == null || muffeArealLayer == null) return;

if (isInsert)
{
// Logic for handling row insertions
await ApplyTemplateAlongPolyline(polyline, muffePunktLayer, muffeArealLayer);
}
else
{
// Logic for handling row updates
await ApplyTemplateAlongPolyline(polyline, muffePunktLayer, muffeArealLayer);
}
});
}

private async void OnRowDeleted(RowChangedEventArgs args)
{
if (!_isActive) return; // Check if the script is active before proceeding
if (!(args.Row is Feature feature)) return;
if (feature.GetTable().GetName() != "Plan_Kabler") return;

var featureId = feature.GetObjectID();

await QueuedTask.Run(async () =>
{
var muffePunktLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>().FirstOrDefault(l => l.Name == "Muffe Punkt");
var muffeArealLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>().FirstOrDefault(l => l.Name == "Muffe Areal");
if (muffePunktLayer == null || muffeArealLayer == null) return;

// Logic for handling row deletions
await DeleteIntersectingTemplates(featureId, muffePunktLayer, muffeArealLayer);
});
}

private async Task ApplyTemplateAlongPolyline(Polyline polyline, FeatureLayer muffePunktLayer, FeatureLayer muffeArealLayer)
{
// Ensure spatial reference is correctly obtained
var spatialReference = await QueuedTask.Run(() => muffeArealLayer.GetFeatureClass().GetDefinition().GetSpatialReference());

double interval = 100.0; // Interval in meters
double distanceToNextPoint = 0; // Start with 0 to place the first point immediately

foreach (var part in polyline.Parts)
{
double distanceCoveredInSegment = 0.0;
foreach (var segment in part)
{
double segmentLength = CalculateSegmentLength(segment);

while (distanceCoveredInSegment + distanceToNextPoint <= segmentLength)
{
double proportion = distanceToNextPoint / segmentLength;
MapPoint position = CalculatePositionAlongSegment(segment, proportion + (distanceCoveredInSegment / segmentLength));
double angle = CalculateSegmentAngle(segment);

try
{
// Attempt to create and place the point and polygons at this position
await CreateAndPlacePolygons(position, angle, spatialReference, muffeArealLayer);
await CreateFeatureInLayer(muffePunktLayer, position);
}
catch (Exception ex)
{
// Log or handle the error
Console.WriteLine($"Failed to create feature: {ex.Message}");
}

distanceCoveredInSegment += distanceToNextPoint;
distanceToNextPoint = interval; // Reset after placing a point
}
distanceToNextPoint -= (segmentLength - distanceCoveredInSegment);
distanceCoveredInSegment = 0.0;
}
}
}

private double CalculateSegmentLength(Segment segment)
{
return Math.Sqrt(Math.Pow(segment.EndPoint.X - segment.StartPoint.X, 2) + Math.Pow(segment.EndPoint.Y - segment.StartPoint.Y, 2));
}

private MapPoint CalculatePositionAlongSegment(Segment segment, double proportion)
{
double x = segment.StartPoint.X + (segment.EndPoint.X - segment.StartPoint.X) * proportion;
double y = segment.StartPoint.Y + (segment.EndPoint.Y - segment.StartPoint.Y) * proportion;
return MapPointBuilder.CreateMapPoint(x, y, segment.SpatialReference);
}

private double CalculateSegmentAngle(Segment segment)
{
double dx = segment.EndPoint.X - segment.StartPoint.X;
double dy = segment.EndPoint.Y - segment.StartPoint.Y;
return Math.Atan2(dy, dx) * (180 / Math.PI); // Angle in degrees
}

private async Task CreateAndPlacePolygons(MapPoint position, double angle, SpatialReference spatialReference, FeatureLayer layer)
{
// Adjust the offset distances for left and right polygons
double offsetDistanceLeft = 5; // Offset distance for left polygons
double offsetDistanceRight = 10; // Offset distance for right polygons

// Calculate perpendicular direction for left and right offsets
double angleLeft = angle - 90; // Perpendicular to the left
double angleRight = angle + 90; // Perpendicular to the right

// Create offset points for left and right polygons using the perpendicular angles
MapPoint offsetPointLeft = RotateAndMovePoint(position, angleLeft, offsetDistanceLeft, spatialReference);
MapPoint offsetPointRight = RotateAndMovePoint(position, angleRight, offsetDistanceRight, spatialReference);

// Create polygons based on the direction of the segment
Polygon orientedPolygonLeft = CreateOrientedPolygon(offsetPointLeft, angle, spatialReference);
await CreateFeatureInLayer(layer, orientedPolygonLeft);

Polygon orientedPolygonRight = CreateOrientedPolygon(offsetPointRight, angle, spatialReference);
await CreateFeatureInLayer(layer, orientedPolygonRight);
}

private MapPoint RotateAndMovePoint(MapPoint point, double angleDegrees, double distance, SpatialReference spatialReference)
{
double angleRadians = angleDegrees * (Math.PI / 180);

double offsetX = Math.Cos(angleRadians) * distance;
double offsetY = Math.Sin(angleRadians) * distance;

return MapPointBuilder.CreateMapPoint(point.X + offsetX, point.Y + offsetY, spatialReference);
}

private async Task CreateFeatureInLayer(FeatureLayer layer, Geometry geometry)
{
var editOperation = new EditOperation
{
Name = $"Create feature in {layer.Name}"
};

editOperation.Create(layer, geometry);

var result = await editOperation.ExecuteAsync();
if (!result) // If the result is false, the operation failed
{
var errorMessage = editOperation.ErrorMessage;
// Log the errorMessage or display it to understand why the operation failed
System.Diagnostics.Debug.WriteLine($"Error creating feature in {layer.Name}: {errorMessage}");
}
}

private Polygon CreateOrientedPolygon(MapPoint centerPoint, double angle, SpatialReference spatialReference)
{
double width = 10; // Width of the rectangle
double height = 5; // Height of the rectangle

double halfWidth = width / 2;
double halfHeight = height / 2;

// Use MapPointBuilder to create points with a spatial reference
MapPoint[] cornerPoints = new MapPoint[]
{
MapPointBuilder.CreateMapPoint(centerPoint.X - halfWidth, centerPoint.Y - halfHeight, spatialReference),
MapPointBuilder.CreateMapPoint(centerPoint.X + halfWidth, centerPoint.Y - halfHeight, spatialReference),
MapPointBuilder.CreateMapPoint(centerPoint.X + halfWidth, centerPoint.Y + halfHeight, spatialReference),
MapPointBuilder.CreateMapPoint(centerPoint.X - halfWidth, centerPoint.Y + halfHeight, spatialReference)
};

MapPoint[] rotatedPoints = cornerPoints.Select(point => RotatePoint(point, centerPoint, angle, spatialReference)).ToArray();

Polygon orientedPolygon = new PolygonBuilder(rotatedPoints.Select(p => new Coordinate2D(p.X, p.Y)), spatialReference).ToGeometry();

return orientedPolygon;
}

private MapPoint RotatePoint(MapPoint point, MapPoint pivot, double angleDegrees, SpatialReference spatialReference)
{
double angleRadians = angleDegrees * (Math.PI / 180);

double cosTheta = Math.Cos(angleRadians);
double sinTheta = Math.Sin(angleRadians);

double translatedX = point.X - pivot.X;
double translatedY = point.Y - pivot.Y;

double rotatedX = translatedX * cosTheta - translatedY * sinTheta;
double rotatedY = translatedX * sinTheta + translatedY * cosTheta;

return MapPointBuilder.CreateMapPoint(rotatedX + pivot.X, rotatedY + pivot.Y, spatialReference);
}

private async Task DeleteIntersectingTemplates(long featureId, FeatureLayer muffePunktLayer, FeatureLayer muffeArealLayer)
{
if (!_originalGeometries.TryGetValue(featureId, out var originalGeometry))
{
return; // If not found, exit the method
}

var bufferedGeometry = GeometryEngine.Instance.Buffer(originalGeometry, 10);

// Use different spatial queries based on the layer type
await DeleteFeaturesBasedOnLayerType(muffePunktLayer, bufferedGeometry);
await DeleteFeaturesBasedOnLayerType(muffeArealLayer, bufferedGeometry);
}

private async Task DeleteFeaturesBasedOnLayerType(FeatureLayer layer, Geometry bufferedGeometry)
{
SpatialQueryFilter spatialQueryFilter = new SpatialQueryFilter
{
FilterGeometry = bufferedGeometry,
SpatialRelationship = SpatialRelationship.Intersects
};

List<Geometry> deletedGeometries = new List<Geometry>();
var featureTable = layer.GetTable();
if (featureTable == null) return;

using (var cursor = featureTable.Search(spatialQueryFilter, false))
{
while (cursor.MoveNext())
{
var feature = cursor.Current as Feature;
deletedGeometries.Add(feature.GetShape().Clone());

// Create a separate EditOperation for each feature
var editOperation = new EditOperation
{
Name = $"Delete feature in {layer.Name}"
};
editOperation.Delete(feature);

bool result = await editOperation.ExecuteAsync();
if (!result)
{
System.Diagnostics.Debug.WriteLine($"Failed to delete intersecting feature in {layer.Name}");
}
}
}
}
}
}

Kind regards, 
Marco

0 Kudos
1 Solution

Accepted Solutions
GobiGobletsson
New Contributor

Thanks for the reply. 

I tried your solution alongside the provided link. Super helpful, but it didn't solve the issue - But made me wiser. I realized my mistake, and found a solution just now. 

By adding a delay  "// Adding a small delay to ensure all initializations are complete
await Task.Delay(2);" at line 153 and line 305. 

My guess is, that the actions were executed inappropriately at the sametime. 

View solution in original post

0 Kudos
2 Replies
Aashis
by Esri Contributor
Esri Contributor

My suspicion is in the following areas:

  1. RowEvent callbacks are always called on the QueuedTask, so there is no need to wrap your code within a QueuedTask.Run lambda (line 136)
  2. Do not use a new edit operation in the RowEvent callbacks (line 332); instead, get an edit operation from RowChangedEventArgs, for example, var editOp = args.Operation

Please consult the Editing Conceptual doc for additional details.

GobiGobletsson
New Contributor

Thanks for the reply. 

I tried your solution alongside the provided link. Super helpful, but it didn't solve the issue - But made me wiser. I realized my mistake, and found a solution just now. 

By adding a delay  "// Adding a small delay to ensure all initializations are complete
await Task.Delay(2);" at line 153 and line 305. 

My guess is, that the actions were executed inappropriately at the sametime. 

0 Kudos