Skip to content

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:

  1. Draw shapes with TrackOverlay (or load them from a data source).
  2. Move those shapes into EditOverlay.EditShapesLayer to enter edit mode.
  3. The user manipulates them visually using the control point handles.
  4. When done, move the edited shapes back out of EditShapesLayer into 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: GetArea and GetLength both accept a Projection overload 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 WinForms MapView expose MapWidth and MapHeight as the rendered pixel dimensions of the map surface. Use these (not ActualWidth/ActualHeight from the WPF layout system) when calling MapUtil methods.


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:

  • TrackOverlay creates new features (via TrackMode.Point, TrackMode.Line, TrackMode.Polygon, etc.).
  • EditOverlay modifies 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.


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.