TrackOverlay Guide (WPF & WinForms)¶
Namespace: ThinkGeo.UI.Wpf / ThinkGeo.UI.WinForms
Accessed via: MapView.TrackOverlay
Applies to: ThinkGeo Desktop (WPF and WinForms)
Overview¶
TrackOverlay is the built-in interactive overlay that lets users draw new shapes directly on the map using the mouse. You set a TrackMode to tell the overlay what shape to draw, and the user clicks and/or drags to produce it. When the user finishes drawing, a TrackEnded event fires, giving you the completed shape to work with.
The key distinction from EditOverlay (which modifies existing shapes) is that TrackOverlay creates new shapes from scratch. The typical workflow is:
- Set
TrackModeto activate drawing. - The user draws a shape on the map.
- Handle
TrackEndedto receive the completed shape and do something with it — save it, run a spatial query, trigger a cloud API call, etc. - Set
TrackMode = TrackMode.Noneto return to normal navigation.
TrackOverlay is a singleton property on MapView — you do not construct it yourself.
How It Works: Drawing Mechanics¶
When TrackMode is set to anything other than None, the map intercepts mouse clicks and routes them into the overlay's drawing logic instead of the default pan-and-zoom behavior. The in-progress shape is rendered live in TrackShapeLayer as the user moves the mouse and adds vertices.
Drawing gestures differ by shape type:
- Point — single left-click places the point immediately.
TrackEndedfires at once. - Line — each left-click adds a vertex. Double-click closes the shape and fires
TrackEnded. - Polygon — each left-click adds a vertex. Double-click closes the polygon (connecting the last vertex back to the first) and fires
TrackEnded.
While drawing a multi-vertex shape (Line or Polygon), the user can hold the middle mouse button (scroll wheel) and drag to pan the map without interrupting the drawing session. In WPF, the user can also hold Shift while adding vertices to constrain movement to the North–South or East–West axis — a useful snapping aid for orthogonal shapes.
Key Members¶
Properties¶
| Property | Type | Description |
|---|---|---|
TrackMode | TrackMode | Controls what shape the overlay draws. Set to TrackMode.None to disable drawing and restore normal map navigation. |
TrackShapeLayer | InMemoryFeatureLayer | The layer holding the in-progress and completed drawn shapes. Clear this after consuming a shape to reset the drawing state. |
Events¶
| Event | Event Args Type | Fired When |
|---|---|---|
TrackEnded | TrackEndedTrackInteractiveOverlayEventArgs | The user finishes drawing a shape (double-click for Line/Polygon, single click for Point). |
MouseMoved | MouseMovedTrackInteractiveOverlayEventArgs | The mouse moves while a drawing is in progress. Useful for live measurement feedback. |
TrackEndedTrackInteractiveOverlayEventArgs¶
| Member | Type | Description |
|---|---|---|
TrackShape | BaseShape | The completed shape. Cast to PointShape, LineShape, or PolygonShape as appropriate. |
MouseMovedTrackInteractiveOverlayEventArgs¶
| Member | Type | Description |
|---|---|---|
AffectedFeature | Feature | The in-progress shape as it currently stands. Call GetShape() on it to measure the partial geometry during drawing. |
TrackMode Enum¶
The following TrackMode values are confirmed in use across the HowDoI samples:
| Value | Behavior |
|---|---|
TrackMode.None | Drawing disabled; normal map navigation (pan and zoom). The default state. |
TrackMode.Point | One click places a point; TrackEnded fires immediately. |
TrackMode.Line | Click to add vertices; double-click to finish the line. |
TrackMode.Polygon | Click to add vertices; double-click to close and finish the polygon. |
Usage Patterns¶
Pattern 1: Basic Draw-and-Collect Workflow¶
This is the canonical workflow when building an editing tool — the user draws shapes freely, and everything accumulates in TrackShapeLayer until the mode changes. When the user switches away from a drawing mode, you flush TrackShapeLayer into your own InMemoryFeatureLayer for persistent storage.
// ---- Setup ----
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);
// ---- Activate a drawing mode ----
// Call from a button handler. Only one mode should be active at a time.
MapView.TrackOverlay.TrackMode = TrackMode.Point; // draw points
MapView.TrackOverlay.TrackMode = TrackMode.Line; // draw lines
MapView.TrackOverlay.TrackMode = TrackMode.Polygon; // draw polygons
// ---- Deactivate drawing and collect drawn shapes ----
// Call when the user switches to a different mode (Navigation, Edit, etc.)
private void ExitDrawMode(InMemoryFeatureLayer featureLayer, LayerOverlay layerOverlay)
{
// Move everything accumulated in TrackShapeLayer into the persistent layer
foreach (var feature in MapView.TrackOverlay.TrackShapeLayer.InternalFeatures)
featureLayer.InternalFeatures.Add(feature.Id, feature);
MapView.TrackOverlay.TrackShapeLayer.InternalFeatures.Clear();
MapView.TrackOverlay.TrackMode = TrackMode.None;
_ = MapView.RefreshAsync(new Overlay[] { MapView.TrackOverlay, layerOverlay });
}
Why flush into a separate layer?
TrackShapeLayeris transient — it is cleared when the mode changes or when theEditOverlaytakes over. Copying features into your ownInMemoryFeatureLayerbefore switching modes is how you preserve them across mode transitions.
Pattern 2: React-on-Completion with TrackEnded¶
TrackEnded is the right hook when you want to immediately act on each completed shape rather than accumulate them. Common uses include triggering spatial queries, calling cloud APIs, or running geometry analysis. Every sample that uses TrackEnded follows the same three-step handler pattern:
- Set
TrackMode = TrackMode.Noneto stop drawing. - Clear
TrackShapeLayer(the in-progress shape is now ine.TrackShape; the layer copy is no longer needed). - Act on
e.TrackShape.
// ---- Subscribe during initialization ----
MapView.TrackOverlay.TrackEnded += OnShapeDrawn;
MapView.TrackOverlay.TrackMode = TrackMode.Polygon; // start in polygon draw mode
// ---- Handler: runs your logic on the completed shape ----
private async void OnShapeDrawn(object sender, TrackEndedTrackInteractiveOverlayEventArgs e)
{
// Step 1: disable further drawing
MapView.TrackOverlay.TrackMode = TrackMode.None;
// Step 2: clear the in-progress shape from the draw layer
MapView.TrackOverlay.TrackShapeLayer.InternalFeatures.Clear();
// Step 3: use e.TrackShape — cast to the type you expect
var polygon = (PolygonShape)e.TrackShape;
// Example: run a spatial query against your feature layer
await RunSpatialQueryAsync(polygon);
}
If you want to allow the user to re-draw after each completed shape, re-enable TrackMode at the end of your handler (or hook MapView.MapClick to re-enable it):
// Re-enable drawing after handling the completed shape
private void mapView_MapClick(object sender, MapClickMapViewEventArgs e)
{
if (MapView.TrackOverlay.TrackMode != TrackMode.Polygon)
MapView.TrackOverlay.TrackMode = TrackMode.Polygon;
}
Pattern 3: Validate the Shape in TrackEnded Before Using It¶
When shapes are used as inputs to distance- or area-sensitive operations (such as cloud elevation queries or area-based analysis), validate size immediately in TrackEnded and reject oversized shapes before proceeding:
private async void OnShapeDrawn(object sender, TrackEndedTrackInteractiveOverlayEventArgs e)
{
MapView.TrackOverlay.TrackMode = TrackMode.None;
MapView.TrackOverlay.TrackShapeLayer.InternalFeatures.Clear();
// Validate before passing to your processing method
switch (e.TrackShape.GetWellKnownType())
{
case WellKnownType.Polygon:
var polygon = (PolygonShape)e.TrackShape;
if (polygon.GetArea(GeographyUnit.Meter, AreaUnit.SquareKilometers) > 5)
{
MessageBox.Show("Please draw a smaller polygon (limit: 5km²)");
return;
}
break;
case WellKnownType.Line:
var line = (LineShape)e.TrackShape;
if (line.GetLength(GeographyUnit.Meter, DistanceUnit.Kilometer) > 5)
{
MessageBox.Show("Please draw a shorter line (limit: 5km)");
return;
}
break;
}
await ProcessShapeAsync(e.TrackShape);
}
GetWellKnownType()is the idiomatic way to branch on shape type when you may receive different geometry types from the sameTrackEndedhandler. It avoids a type-check chain withisandascasts.
Pattern 4: Live Measurement with MouseMoved (WPF)¶
TrackOverlay.MouseMoved fires continuously as the user moves the mouse while drawing. The AffectedFeature in the event args holds the partially-drawn shape, letting you display a running area or length readout in the UI before the user finishes.
This event is confirmed in WPF HowDoI samples. In WinForms the same measurement approach works via TrackEnded (after the shape is complete), since the WinForms samples do not use MouseMoved for live feedback.
// Subscribe during initialization (WPF)
MapView.TrackOverlay.MouseMoved += TrackOverlay_MouseMoved;
private void TrackOverlay_MouseMoved(object sender, MouseMovedTrackInteractiveOverlayEventArgs e)
{
var shape = e.AffectedFeature.GetShape();
MeasureResult.Text = GetMeasureResult(shape);
}
private static string GetMeasureResult(BaseShape shape)
{
// Pass the map's Spherical Mercator projection for accurate measurements.
var projection = new Projection(Projection.GetSphericalMercatorProjString());
switch (shape)
{
case AreaBaseShape polygon:
var area = polygon.GetArea(projection, AreaUnit.SquareMiles);
return $"{area:N2} square miles";
case LineBaseShape line:
var length = line.GetLength(projection, DistanceUnit.Mile);
return $"{length:N2} miles";
default:
return string.Empty;
}
}
Projection-aware measurement:
GetArea()andGetLength()each have an overload that accepts aProjection. Always pass one when the map uses Web Mercator (EPSG:3857), as the raw coordinate values produce distorted measurements without it.Projection.GetSphericalMercatorProjString()is the correct Proj4 string for this projection.
Pattern 5: Draw-Then-Query (Spatial Query Trigger)¶
A very common pattern: TrackOverlay in Polygon or Line mode is used as a free-form selection tool. The user draws a shape, and TrackEnded triggers a spatial query against a loaded feature layer. The drawn shape is cleared from TrackShapeLayer immediately — it is only needed as a query input, not as a persistent feature.
// ---- Setup ----
MapView.TrackOverlay.TrackEnded += OnPolygonDrawn;
MapView.TrackOverlay.TrackMode = TrackMode.Polygon;
// ---- TrackEnded handler ----
private async void OnPolygonDrawn(object sender, TrackEndedTrackInteractiveOverlayEventArgs e)
{
await RunQueryAsync((PolygonShape)e.TrackShape);
}
// ---- Query method ----
private async Task RunQueryAsync(PolygonShape queryPolygon)
{
var queryFeaturesOverlay = (LayerOverlay)MapView.Overlays["Query Features Overlay"];
var queryFeatureLayer = (InMemoryFeatureLayer)queryFeaturesOverlay.Layers["Query Feature"];
var dataLayer = (ShapeFileFeatureLayer)MapView.FindFeatureLayer("My Data Layer");
// Replace the query shape shown on the map
queryFeatureLayer.InternalFeatures.Clear();
queryFeatureLayer.InternalFeatures.Add(new Feature(queryPolygon));
await queryFeaturesOverlay.RefreshAsync();
// Run the spatial query
dataLayer.Open();
var results = dataLayer.QueryTools.GetFeaturesWithin(queryPolygon, ReturningColumnsType.AllColumns);
dataLayer.Close();
// Display the results
await DisplayResultsAsync(results);
// Clear the drawn shape — it has been consumed
MapView.TrackOverlay.TrackShapeLayer.InternalFeatures.Clear();
await MapView.RefreshAsync();
}
For a Line mode spatial query (e.g., finding features that cross a drawn line):
MapView.TrackOverlay.TrackEnded += OnLineDrawn;
MapView.TrackOverlay.TrackMode = TrackMode.Line;
private async void OnLineDrawn(object sender, TrackEndedTrackInteractiveOverlayEventArgs e)
{
await RunCrossingQueryAsync((LineShape)e.TrackShape);
}
Pattern 6: Accept a Completed Shape into Another Layer¶
When you want the drawn shape to become a persistent, user-visible feature (rather than just a query input), add it to your own InMemoryFeatureLayer directly from the TrackEnded handler. The pattern matches what the WorldMapsQuery sample does — the drawn shape is transferred into a display layer, then TrackShapeLayer is cleared:
private async void OnShapeDrawn(object sender, TrackEndedTrackInteractiveOverlayEventArgs e)
{
// Stop drawing
MapView.TrackOverlay.TrackMode = TrackMode.None;
// Clear the TrackOverlay's own rendering of the shape
MapView.TrackOverlay.TrackShapeLayer.InternalFeatures.Clear();
// Add the completed shape to your persistent display layer
var overlay = (LayerOverlay)MapView.Overlays["My Overlay"];
var displayLayer = (InMemoryFeatureLayer)overlay.Layers["My Layer"];
displayLayer.InternalFeatures.Add(new Feature(e.TrackShape));
// Refresh the display layer to show the new feature
await overlay.RefreshAsync();
// Run any follow-on logic (e.g., cloud query, analysis, etc.)
await DoSomethingWithShapeAsync(e.TrackShape);
}
Styling the Draw Layer¶
TrackShapeLayer is an InMemoryFeatureLayer, and like any other feature layer you can override its default draw styles through its ZoomLevelSet. The overlay has built-in default styles (a thin blue outline), but you can replace them if you want the in-progress shape to appear differently — for example, to match your application's color scheme or to show a dashed rubber-band line.
// Apply custom styles to the in-progress draw layer
var drawLayer = MapView.TrackOverlay.TrackShapeLayer;
drawLayer.ZoomLevelSet.ZoomLevel01.DefaultPointStyle =
PointStyle.CreateSimpleCircleStyle(GeoColors.OrangeRed, 10, GeoColors.DarkRed);
drawLayer.ZoomLevelSet.ZoomLevel01.DefaultLineStyle =
LineStyle.CreateSimpleLineStyle(GeoColors.OrangeRed, 2, true);
drawLayer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle =
AreaStyle.CreateSimpleAreaStyle(GeoColor.FromArgb(60, GeoColors.OrangeRed), GeoColors.OrangeRed);
drawLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
Style the layer during initialization, before any drawing begins. Changes made mid-drawing may not take effect until the next shape starts.
Interaction with EditOverlay¶
TrackOverlay and EditOverlay are companion overlays that are almost always used together. The relationship is:
TrackOverlaycreates new shapes.EditOverlaymodifies existing shapes.
Only one should be "active" at a time. The standard handoff pattern when switching to edit mode is:
- Flush
TrackShapeLayerinto yourInMemoryFeatureLayer. - Clear
TrackShapeLayerand setTrackMode = TrackMode.None. - Move features from
InMemoryFeatureLayerintoEditOverlay.EditShapesLayer. - Call
EditOverlay.CalculateAllControlPoints().
The reverse handoff (edit → draw) clears EditShapesLayer back into the feature layer first. See the EditOverlay Developer Guide for the complete flush helper implementation.
Common Pitfalls¶
1. Not Clearing TrackShapeLayer After TrackEnded¶
After TrackEnded fires, the completed shape remains in TrackShapeLayer. If you do not clear it, the overlay continues to render it on the map while the next shape is being drawn — producing a confusing visual where multiple shapes appear to be active simultaneously. Always clear in your TrackEnded handler:
MapView.TrackOverlay.TrackShapeLayer.InternalFeatures.Clear();
2. Forgetting to Set TrackMode Back to None¶
After handling a completed shape, remember to set TrackMode = TrackMode.None unless you explicitly want the user to be able to immediately draw another shape. Leaving a drawing mode active means the user cannot pan the map normally.
3. Casting TrackShape to the Wrong Type¶
e.TrackShape is typed as BaseShape. Casting it directly to PolygonShape when the user drew a line will throw at runtime. Either guard with GetWellKnownType() (Pattern 3 above) or only subscribe to TrackEnded when a single known TrackMode is active.
4. Measuring Without a Projection¶
Calling GetArea(GeographyUnit.Meter, AreaUnit.SquareKilometers) or GetLength(GeographyUnit.Meter, DistanceUnit.Kilometer) on a Web Mercator shape without passing a Projection gives you a raw coordinate-space result, which is distorted. Pass a Projection built from Projection.GetSphericalMercatorProjString() when your map is in EPSG:3857. See Pattern 3 above for the correct overloads.
5. Subscribing to TrackEnded Multiple Times¶
TrackEnded is an instance event on the singleton TrackOverlay. If your initialization code can run more than once, guard with a flag or use -= before += to avoid accumulating duplicate handlers that each fire on every completed shape.
Related Classes¶
| Class / Member | Description |
|---|---|
EditOverlay | Companion overlay for modifying existing shapes. Accessed via MapView.EditOverlay. |
TrackMode | Enum controlling what TrackOverlay draws: None, Point, Line, Polygon. |
TrackEndedTrackInteractiveOverlayEventArgs | Event args for TrackEnded. Provides TrackShape — the completed BaseShape. |
MouseMovedTrackInteractiveOverlayEventArgs | Event args for MouseMoved. Provides AffectedFeature — the in-progress partial shape. |
InMemoryFeatureLayer | The type of TrackShapeLayer. Supports full ZoomLevelSet-based styling. |
BaseShape.GetWellKnownType() | Returns the geometry type (WellKnownType.Point, Line, Polygon) — use to safely branch on shape type in TrackEnded. |
Projection.GetSphericalMercatorProjString() | Returns the Proj4 string for EPSG:3857. Pass to GetArea() / GetLength() for accurate real-world measurements on a Web Mercator map. |