EditOverlay Guide (WPF & WinForms)¶
Namespace: ThinkGeo.UI.Wpf / ThinkGeo.UI.WinForms
Accessed via: MapView.EditOverlay
Applies to: ThinkGeo Desktop (WPF and WinForms)
Overview¶
EditOverlay is a built-in interactive overlay on every ThinkGeo desktop MapView. It gives users direct, mouse-driven control over the geometry of features already on the map — translating, rotating, scaling, and vertex-editing shapes without writing any hit-testing or geometry math yourself.
The key distinction from TrackOverlay (which handles drawing new shapes) is that EditOverlay handles modifying shapes that already exist. The typical workflow is:
- Draw shapes with
TrackOverlay(or load them from a data source). - Move those shapes into
EditOverlay.EditShapesLayerto enter edit mode. - The user manipulates them visually using the control point handles.
- When done, move the edited shapes back out of
EditShapesLayerinto your own layer for persistence or further processing.
EditOverlay is a singleton property on the MapView — you do not construct it yourself.
How It Works: Control Points¶
When features are in EditShapesLayer, calling CalculateAllControlPoints() instructs the overlay to generate all the visual handles needed to edit each shape:
- Vertex handles — draggable points at every vertex of a line or polygon. Drag to move that vertex. Double-click to remove a vertex. Click on a segment midpoint handle to insert a new vertex.
- Transform handles — the rotation and scale handles that appear around area and line shapes, rendered as anchors at the corners and edges of the shape's bounding rectangle.
- Translation — drag anywhere inside the shape's interior to translate the whole shape.
Points have only a translation handle (they have no vertices or bounding box to resize). Lines have vertex handles plus a translation handle. Polygons and multipolygons have all three: vertex handles, transform handles, and translation.
You must call CalculateAllControlPoints() every time you add or modify features in EditShapesLayer. If you skip this call, the visual handles will be absent or stale and the overlay will appear unresponsive.
Key Members¶
Properties¶
| Property | Type | Description |
|---|---|---|
EditShapesLayer | InMemoryFeatureLayer | The layer whose features are currently being edited. Add features here to put them into edit mode. |
ControlPointsLayer | InMemoryFeatureLayer | Internal layer holding the generated control point handles. Read-only in normal use. |
IsEnabled | bool | Whether the overlay responds to mouse interaction. Set to false to temporarily suspend editing without removing features. |
Methods¶
| Method | Description |
|---|---|
CalculateAllControlPoints() | Regenerates all vertex and transform handles for every feature currently in EditShapesLayer. Must be called after adding or modifying features. |
Events¶
| Event | Event Args Type | Fired When |
|---|---|---|
VertexMoving | VertexMovingEditInteractiveOverlayEventArgs | A vertex drag is in progress (fires continuously). Cancel or override the target position here (e.g., for snapping). |
VertexMoved | VertexMovedEditInteractiveOverlayEventArgs | A vertex drag has completed. Use for live measurement feedback. |
VertexMovingEditInteractiveOverlayEventArgs¶
| Member | Type | Description |
|---|---|---|
TargetVertex | Vertex | The vertex being dragged. Writable — set X/Y to override the drop position (snapping). |
AffectedFeature | Feature | The feature being edited. |
VertexMovedEditInteractiveOverlayEventArgs¶
| Member | Type | Description |
|---|---|---|
AffectedFeature | Feature | The feature after the move completed. Call GetShape() on it to measure. |
WinForms Note: Registering the Overlay¶
In WinForms, the EditOverlay is not automatically included in mapView.Overlays — it exists as a standalone property but is not drawn unless you add it explicitly. In the basic draw-and-edit flow this is handled transparently because EditOverlay is always drawn on top. However, if you need the overlay to appear on top of other overlays that are added after initialization (as in the snapping sample), you must add it:
// WinForms only — add EditOverlay to the overlay stack so it renders correctly
mapView.Overlays.Add("Edit Overlay", mapView.EditOverlay);
In WPF, the EditOverlay renders automatically without being added to MapView.Overlays.
Usage Patterns¶
Pattern 1: Basic Draw-Then-Edit Workflow¶
This is the canonical desktop editing workflow: features are drawn with TrackOverlay, then moved into EditOverlay for modification, and finally returned to an InMemoryFeatureLayer for storage. The WPF and WinForms samples both implement this same state-machine pattern.
The application maintains one InMemoryFeatureLayer as the persistent store. Mode buttons transition between Navigation, Draw, Edit, and Delete states. The UpdateLayerFeatures helper is the glue that moves features between overlays when switching modes.
// ---- Setup (WPF: MapView_Loaded / WinForms: Form_Load) ----
mapView.MapUnit = GeographyUnit.Meter;
var featureLayer = new InMemoryFeatureLayer();
featureLayer.ZoomLevelSet.ZoomLevel01.DefaultPointStyle =
PointStyle.CreateSimpleCircleStyle(GeoColors.Blue, 8, GeoColors.Black);
featureLayer.ZoomLevelSet.ZoomLevel01.DefaultLineStyle =
LineStyle.CreateSimpleLineStyle(GeoColors.Blue, 4, true);
featureLayer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle =
AreaStyle.CreateSimpleAreaStyle(GeoColors.Blue, GeoColors.Black);
featureLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
var layerOverlay = new LayerOverlay();
layerOverlay.Layers.Add("featureLayer", featureLayer);
mapView.Overlays.Add("layerOverlay", layerOverlay);
// ---- Helper: flush both TrackOverlay and EditOverlay back into the feature layer ----
// Call this before every mode transition to collect any outstanding shapes.
private void FlushOverlaysToLayer(InMemoryFeatureLayer featureLayer, LayerOverlay layerOverlay)
{
// Collect anything drawn since the last flush
foreach (var feature in mapView.TrackOverlay.TrackShapeLayer.InternalFeatures)
featureLayer.InternalFeatures.Add(feature.Id, feature);
mapView.TrackOverlay.TrackShapeLayer.InternalFeatures.Clear();
// Collect anything edited since the last flush
foreach (var feature in mapView.EditOverlay.EditShapesLayer.InternalFeatures)
featureLayer.InternalFeatures.Add(feature.Id, feature);
mapView.EditOverlay.EditShapesLayer.InternalFeatures.Clear();
_ = mapView.RefreshAsync(new Overlay[] { mapView.TrackOverlay, mapView.EditOverlay, layerOverlay });
}
// ---- Entering Edit Mode ----
// (called from an "Edit Shapes" button click)
private void EnterEditMode(InMemoryFeatureLayer featureLayer, LayerOverlay layerOverlay)
{
// Flush anything in TrackOverlay first
FlushOverlaysToLayer(featureLayer, layerOverlay);
// Stop drawing
mapView.TrackOverlay.TrackMode = TrackMode.None;
// Move all features from the persistent layer into EditShapesLayer
foreach (var feature in featureLayer.InternalFeatures)
mapView.EditOverlay.EditShapesLayer.InternalFeatures.Add(feature.Id, feature);
featureLayer.InternalFeatures.Clear();
// Generate all vertex and transform handles — required after populating EditShapesLayer
mapView.EditOverlay.CalculateAllControlPoints();
_ = mapView.RefreshAsync(new Overlay[] { mapView.EditOverlay, layerOverlay });
}
// ---- Leaving Edit Mode (returning to Navigation or any other mode) ----
private void ExitEditMode(InMemoryFeatureLayer featureLayer, LayerOverlay layerOverlay)
{
// Return edited features back to the persistent layer
foreach (var feature in mapView.EditOverlay.EditShapesLayer.InternalFeatures)
featureLayer.InternalFeatures.Add(feature.Id, feature);
mapView.EditOverlay.EditShapesLayer.InternalFeatures.Clear();
_ = mapView.RefreshAsync(new Overlay[] { mapView.EditOverlay, layerOverlay });
}
Pattern 2: Loading Features Directly into EditOverlay¶
You do not have to go through TrackOverlay. If you have existing geometry — from a shapefile, a database, or constructed in code — you can put it directly into EditShapesLayer. This is useful when you want to display a pre-loaded feature and let the user refine it.
// Load features from WKT directly into EditShapesLayer
var polygon = new Feature("POLYGON((-10778500 3915600,-10778500 3910000,-10774040 3910000,-10774040 3915600,-10778500 3915600))");
var point = new Feature("POINT(-10773220 3913230)");
var line = new Feature("LINESTRING(-10780700 3916500, -10780700 3910040)");
MapView.EditOverlay.EditShapesLayer.InternalFeatures.Add(polygon);
MapView.EditOverlay.EditShapesLayer.InternalFeatures.Add(point);
MapView.EditOverlay.EditShapesLayer.InternalFeatures.Add(line);
// Always call CalculateAllControlPoints after populating EditShapesLayer
MapView.EditOverlay.CalculateAllControlPoints();
_ = MapView.RefreshAsync();
You can also build geometry programmatically from LineShape and Vertex objects:
var lineShape = new LineShape();
lineShape.Vertices.Add(new Vertex(-10783003, 3918370));
lineShape.Vertices.Add(new Vertex(-10783070, 3917335));
lineShape.Vertices.Add(new Vertex(-10781292, 3916438));
MapView.EditOverlay.EditShapesLayer.InternalFeatures.Add(new Feature(lineShape));
MapView.EditOverlay.CalculateAllControlPoints();
_ = MapView.RefreshAsync();
Pattern 3: Live Measurement with VertexMoved¶
The VertexMoved event fires after each completed drag operation, giving you the post-edit geometry. You can use it to display a live area or length readout as the user edits.
The measurement uses Projection.GetSphericalMercatorProjString() to get an accurate result from the Web Mercator coordinate space:
// Subscribe during initialization
MapView.EditOverlay.VertexMoved += EditOverlay_VertexMoved;
private void EditOverlay_VertexMoved(object sender, VertexMovedEditInteractiveOverlayEventArgs e)
{
var shape = e.AffectedFeature.GetShape();
MeasureResult.Text = GetMeasureResult(shape);
}
private static string GetMeasureResult(BaseShape shape)
{
// Pass the map's projection into the measurement methods for accurate results.
// The map is in Spherical Mercator, so we use the corresponding Proj string.
var currentProjection = new Projection(Projection.GetSphericalMercatorProjString());
switch (shape)
{
case AreaBaseShape polygon:
var area = polygon.GetArea(currentProjection, AreaUnit.SquareMiles);
return $"{area:N2} square miles";
case LineBaseShape line:
var length = line.GetLength(currentProjection, DistanceUnit.Mile);
return $"{length:N2} miles";
default:
return string.Empty;
}
}
Note:
GetAreaandGetLengthboth accept aProjectionoverload that corrects for the distortion inherent in Web Mercator. Always pass the map's projection when you need accurate real-world measurements, not just screen-space values.
Pattern 4: Vertex Snapping with VertexMoving¶
VertexMoving fires during a drag, before the vertex is committed. Because TargetVertex is writable, you can override its X and Y coordinates to snap the vertex to a nearby candidate point — a common requirement in precision editing workflows.
The sample below snaps to the nearest point in a reference layer if the screen distance between the dragged vertex and that point falls within a configurable pixel tolerance:
private const float Tolerance = 25; // pixels
private ShapeFileFeatureLayer _snapTargetLayer; // populated during setup
// Subscribe during initialization
mapView.EditOverlay.VertexMoving += EditOverlay_VertexMoving;
private void EditOverlay_VertexMoving(object sender, VertexMovingEditInteractiveOverlayEventArgs e)
{
// Find the nearest feature in the snap target layer to the vertex currently being dragged
var nearestFeatures = _snapTargetLayer.QueryTools.GetFeaturesNearestTo(
e.TargetVertex, GeographyUnit.Meter, 1, ReturningColumnsType.AllColumns);
if (nearestFeatures.Count == 0)
return;
var candidatePoint = nearestFeatures[0].GetShape() as PointShape;
if (candidatePoint == null)
return;
// Convert world-space distance to screen pixels for the tolerance check
// WinForms: use mapView.MapWidth / mapView.MapHeight
// WPF: use (float)MapView.MapWidth / (float)MapView.MapHeight
var screenDistance = MapUtil.GetScreenDistanceBetweenTwoWorldPoints(
mapView.CurrentExtent,
candidatePoint,
e.TargetVertex,
(float)mapView.MapWidth,
(float)mapView.MapHeight);
// If the candidate is within tolerance, snap to it
if (screenDistance < Tolerance)
{
e.TargetVertex.X = candidatePoint.X;
e.TargetVertex.Y = candidatePoint.Y;
}
}
MapWidth/MapHeight: Both WPF and WinFormsMapViewexposeMapWidthandMapHeightas the rendered pixel dimensions of the map surface. Use these (notActualWidth/ActualHeightfrom the WPF layout system) when callingMapUtilmethods.
Delete Mode: Removing Features on Click¶
Deleting a feature is not a built-in mode of EditOverlay — you implement it by subscribing to MapView.MapClick, finding the nearest feature using QueryTools, and removing it from the feature layer:
// Entering delete mode — hook the MapClick event
private void EnterDeleteMode()
{
// Flush any current overlay state first
FlushOverlaysToLayer(featureLayer, layerOverlay);
mapView.TrackOverlay.TrackMode = TrackMode.None;
mapView.MapClick += MapView_MapClick_Delete;
}
// Leaving delete mode — unhook to prevent unintended deletions
private void LeaveDeleteMode()
{
mapView.MapClick -= MapView_MapClick_Delete;
}
private void MapView_MapClick_Delete(object sender, MapClickMapViewEventArgs e)
{
var featureLayer = (InMemoryFeatureLayer)layerOverlay.Layers["featureLayer"];
// Find the closest feature within 100 meters of the click
var closestFeatures = featureLayer.QueryTools.GetFeaturesNearestTo(
e.WorldLocation, GeographyUnit.Meter, 1,
new Collection<string>(), 100, DistanceUnit.Meter);
if (closestFeatures.Count > 0)
{
featureLayer.InternalFeatures.Remove(closestFeatures[0]);
_ = mapView.RefreshAsync(layerOverlay);
}
}
Interaction with TrackOverlay¶
EditOverlay and TrackOverlay are typically used in tandem. The relationship is:
TrackOverlaycreates new features (viaTrackMode.Point,TrackMode.Line,TrackMode.Polygon, etc.).EditOverlaymodifies existing features.
Only one should be "active" at a time from the user's perspective. The standard pattern for switching between them is to flush both overlays into your persistent InMemoryFeatureLayer whenever the user changes modes, then re-populate whichever overlay is now active. This keeps features from existing in two places simultaneously — a bug that would cause them to appear duplicated or behave unexpectedly.
The flush helper shown in Pattern 1 handles both directions. Notice that when entering edit mode, features are moved out of the InMemoryFeatureLayer into EditShapesLayer (so they don't render twice), and moved back when exiting.
Common Pitfalls¶
1. Forgetting CalculateAllControlPoints()¶
This is the single most common EditOverlay bug. Features added to EditShapesLayer without a subsequent CalculateAllControlPoints() call will render as plain shapes with no handles. The overlay will look like it is ignoring the user's mouse entirely.
Always call it immediately after populating or modifying EditShapesLayer:
mapView.EditOverlay.EditShapesLayer.InternalFeatures.Add(feature);
mapView.EditOverlay.CalculateAllControlPoints(); // required
2. Features Existing in Both EditShapesLayer and Your Own Layer¶
If a feature is in EditShapesLayer and still in your InMemoryFeatureLayer, it will render twice — once with edit handles and once as a plain styled shape. The shapes will visually overlap, and any move or vertex change will only affect the copy in EditShapesLayer, making the results look broken.
Always clear the feature from your own layer when moving it into EditShapesLayer, and move it back when exiting edit mode.
3. WinForms: Not Adding EditOverlay to the Overlay Stack¶
In WinForms, if you need the EditOverlay to be explicitly part of the overlay draw order — particularly when other overlays are added dynamically after the map loads — you must add it:
// Required in WinForms when controlling draw order explicitly
mapView.Overlays.Add("Edit Overlay", mapView.EditOverlay);
In WPF this is not required; the EditOverlay renders on top automatically.
4. Refreshing the Wrong Overlays¶
After an edit-mode transition, always refresh all three relevant overlays together — TrackOverlay, EditOverlay, and your own LayerOverlay. Refreshing only one will leave stale visual state on screen:
_ = mapView.RefreshAsync(new Overlay[] { mapView.TrackOverlay, mapView.EditOverlay, layerOverlay });
5. Subscribing to Events Multiple Times¶
VertexMoving and VertexMoved are instance events on the singleton EditOverlay. If your initialization code can run more than once (e.g., in a WPF Loaded handler that fires multiple times), guard with a flag or use -= before += to ensure you don't accumulate duplicate subscriptions.
Related Classes¶
| Class / Member | Description |
|---|---|
TrackOverlay | Companion overlay for drawing new shapes. Accessed via MapView.TrackOverlay. |
TrackMode | Enum controlling what TrackOverlay draws (None, Point, Line, Polygon, Rectangle, Circle, etc.). |
InMemoryFeatureLayer | The layer type used by EditShapesLayer and ControlPointsLayer. Also the recommended persistent store in draw-edit workflows. |
MapUtil.GetScreenDistanceBetweenTwoWorldPoints | Converts a world-space distance to screen pixels — used in snapping tolerance checks. |
Projection.GetSphericalMercatorProjString() | Returns the Proj string for EPSG:3857; pass to GetArea() / GetLength() for accurate measurements on a Web Mercator map. |
VertexMovingEditInteractiveOverlayEventArgs | Event args for VertexMoving; provides writable TargetVertex for snapping. |
VertexMovedEditInteractiveOverlayEventArgs | Event args for VertexMoved; provides AffectedFeature after the edit completes. |