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.
MuffeTool.cs. below;
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 MuffeTool : 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}");
}
}
}
}
}
}
No matter how much I try - I cannot figure out why the first operation of edit always fails.
I have made sure that its not the specific layer or similar.
I noticed that if the first line being either muffePunktLayer or muffeArealLayer is first in line, that first feature being created in that layer, fails. The remaining features to be placed succeeds.
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.
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.