Feature Guide¶
Namespace: ThinkGeo.Core
Applies to: All ThinkGeo products (WPF, WinForms, Blazor, MAUI, GIS Server, etc.)
Overview¶
Feature is the fundamental data unit in ThinkGeo. Every piece of geographic data — a city point, a road line, a park polygon, a query result — is represented as a Feature. A feature bundles three things together: a geometry (the spatial shape), an identity (Id), and a dictionary of attribute values (ColumnValues).
Features are created by hand when building test data, returned by layer query methods, and added to InMemoryFeatureLayer for display or processing. Understanding how to construct, inspect, and manipulate features is central to almost all ThinkGeo workflows.
Constructors¶
From WKT string¶
The most common form in the samples. Pass any valid Well-Known Text string:
// Point
var pointFeature = new Feature("POINT(0 0)");
// Line
var lineFeature = new Feature("LINESTRING(0 0,100 0,100 50)");
// Multi-line
var multiLineFeature = new Feature("MULTILINESTRING((0 0,100 0),(100 50,0 50))");
// Polygon
var polygonFeature = new Feature("POLYGON((0 0,100 0,100 100,0 100,0 0))");
// Multi-polygon
var multiPolygonFeature = new Feature("MULTIPOLYGON(((0 0,100 0,100 100,0 100,0 0)))");
The WKT string constructor is the idiomatic choice whenever you are writing test features by hand. All topology validation samples use this form exclusively.
Sources: winformHowDoI:Samples/VectorDataTopologicalValidation/ValidateLineTopology.cs, wpfHowDoI:Samples/VectorDataTopologicalValidation/ValidateLineTopology.xaml.cs, mauiHowDoI:Samples/VectorDataTopologicalValidation/LineValidation.xaml.cs.
From a BaseShape object¶
Pass any BaseShape-derived object — PointShape, LineShape, PolygonShape, EllipseShape, MultilineShape, etc. — directly as the constructor argument:
// From a shape object you already have
Feature tempFeature = new Feature(ellipseShape);
// From a shape extracted from another feature
selectedResultItemFeatureLayer.InternalFeatures.Add(new Feature(locationFeature.GetShape()));
This form is the standard choice in geoprocessing pipelines where you derive new shapes from existing ones and want to wrap the result as a feature.
Sources: blazorHowDoI:Shared/GeometricFunctionHelper.cs, winformHowDoI:Samples/ThinkGeoCloudIntegration/ReverseGeocoding.cs.
From WKT with Id and column values¶
The full constructor form accepts a WKT string, a string ID, and a dictionary of attribute values. This is used when loading features from data sources that provide all three pieces:
// Loading from XML where id and attributes come from element attributes
Feature feature = new Feature(featureXElement.Value); // WKT is element text content
foreach (var xAttribute in featureXElement.Attributes())
{
feature.ColumnValues[xAttribute.Name.LocalName] = xAttribute.Value;
}
features.Add(featureXElement.Attribute("id").Value, feature);
In this pattern the feature is constructed from WKT only, then ColumnValues and the key used for GeoCollection.Add are set separately. The alternative is to use the constructor overload that accepts (string wkt, string id, IDictionary<string, string> columnValues) directly.
Source: blazorHowDoI:Shared/GeometricFunctionHelper.cs.
Properties¶
Id¶
A string that uniquely identifies the feature within its source. When features are returned from a FeatureLayer query, Id reflects the feature's identity in that source (row key, object ID, etc.). When you construct a feature by hand and do not supply an ID, it is assigned an empty string; the keyed GeoCollection<Feature>.Add(key, feature) call then acts as the external identifier.
ColumnValues¶
Dictionary<string, string> — the feature's attribute data. Every attribute is a string regardless of the underlying data type. Keys are case-sensitive column names matching those defined in the feature layer's Columns collection (or the shapefile's DBF field names).
// Reading an attribute
string zoneName = feature.ColumnValues["ZONE"];
// Writing an attribute — common when constructing programmatic features
feature.ColumnValues["Name"] = "SnappingBuffer";
feature.ColumnValues["Display"] = someValue;
When a spatial query is called with ReturningColumnsType.NoColumns, the returned features have an empty ColumnValues dictionary. Use ReturningColumnsType.AllColumns or pass a specific list of column names to populate attributes.
Sources: blazorHowDoI:Shared/GeometricFunctionHelper.cs, winformHowDoI:Samples/VectorDataSpatialQuery/FindFeaturesWithinADistance.cs.
Methods¶
GetShape()¶
Returns the feature's geometry as a BaseShape. Cast the result to the concrete type you expect (PointShape, PolygonShape, MultilineShape, etc.) before accessing type-specific members:
// Cast and test
MultilineShape multilineShape = feature.GetShape() as MultilineShape;
if (multilineShape != null)
{
foreach (var vertex in multilineShape.Lines.SelectMany(l => l.Vertices))
{
// work with each vertex
}
}
// Use directly to construct a new feature from the shape
selectedResultItemFeatureLayer.InternalFeatures.Add(new Feature(locationFeature.GetShape()));
Sources: blazorHowDoI:Shared/GeometricFunctionHelper.cs, winformHowDoI:Samples/ThinkGeoCloudIntegration/ReverseGeocoding.cs.
GetBoundingBox()¶
Returns a RectangleShape representing the minimum bounding rectangle of the feature's geometry. Most commonly used to center the map on a result:
MapView.CurrentExtent = locationFeature.GetBoundingBox();
Source: winformHowDoI:Samples/ThinkGeoCloudIntegration/ReverseGeocoding.cs.
Working with Collections of Features¶
Collection<Feature> — for APIs and temporary sets¶
The System.Collections.ObjectModel.Collection<Feature> is used when passing features as input to topology validators, spatial query results, and similar APIs. It is an ordered list without keyed access:
// Building input sets for TopologyValidator
var lineFeature = new Feature("LINESTRING(0 0,100 0,100 50)");
var pointFeature = new Feature("POINT(0 0)");
var lines = new Collection<Feature>() { lineFeature };
var points = new Collection<Feature>() { pointFeature };
var result = TopologyValidator.LineEndPointsMustTouchPoints(lines, points);
// Iterating query results
Collection<Feature> features = layer.QueryTools.GetFeaturesWithinDistanceOf(
shape, GeographyUnit.Meter, DistanceUnit.Meter, radius, ReturningColumnsType.NoColumns);
foreach (var feature in features)
{
highlightedFeaturesLayer.InternalFeatures.Add(feature);
}
Sources: winformHowDoI:Samples/VectorDataTopologicalValidation/ValidateLineTopology.cs, winformHowDoI:Samples/VectorDataSpatialQuery/FindFeaturesWithinADistance.cs.
GeoCollection<Feature> — for keyed data loading¶
GeoCollection<Feature> is a ThinkGeo-specific generic collection that supports both indexed and keyed (string) access. Use it when loading features from a data source that provides string identifiers:
GeoCollection<Feature> features = new GeoCollection<Feature>();
// ... parse source data ...
Feature feature = new Feature(wktString);
feature.ColumnValues["Name"] = parsedName;
features.Add(parsedId, feature); // keyed add — id becomes the GeoCollection key
Source: blazorHowDoI:Shared/GeometricFunctionHelper.cs.
Adding Features to a Layer¶
InternalFeatures.Add¶
InMemoryFeatureLayer.InternalFeatures is the underlying GeoCollection<Feature> that backs the layer. Features added here are immediately available for rendering and querying (assuming the layer is open). The standard pattern is to clear the collection then add fresh features:
// Get the layer, open it, clear old features, add new ones
var highlightedFeaturesLayer = (InMemoryFeatureLayer)overlay.Layers["Highlighted Features"];
highlightedFeaturesLayer.Open();
highlightedFeaturesLayer.InternalFeatures.Clear();
foreach (var feature in queryResultFeatures)
{
highlightedFeaturesLayer.InternalFeatures.Add(feature);
}
highlightedFeaturesLayer.Close();
await highlightedFeaturesOverlay.RefreshAsync();
Features can also be added without a key (the feature's own Id is used as the key internally), or with an explicit string key:
// No explicit key — uses feature.Id
validatedFeaturesLayer.InternalFeatures.Add(lineFeature);
// With explicit key
layer.InternalFeatures.Add("my-key", feature);
Sources: winformHowDoI:Samples/VectorDataSpatialQuery/FindFeaturesWithinADistance.cs, winformHowDoI:Samples/VectorDataTopologicalValidation/ValidateLineTopology.cs.
Reading Features from a Layer¶
FeatureLayer.QueryTools provides spatial and attribute query methods, all of which return Collection<Feature>. The layer must be opened before querying and should be closed afterwards:
layer.Open();
Collection<Feature> results = layer.QueryTools.GetFeaturesWithinDistanceOf(
queryShape,
GeographyUnit.Meter,
DistanceUnit.Meter,
radiusInMeters,
ReturningColumnsType.NoColumns); // or AllColumns, or specific column names
layer.Close();
The ReturningColumnsType argument controls whether ColumnValues is populated on the returned features. NoColumns gives the fastest query when you only need the geometry for display or further processing.
Source: winformHowDoI:Samples/VectorDataSpatialQuery/FindFeaturesWithinADistance.cs.
Full Example — Programmatic Feature Creation and Display¶
This pattern appears throughout all topology validation samples. It shows the complete lifecycle: construct features from WKT, run an API that returns result features, and display everything on the map using three separate InMemoryFeatureLayer instances:
// 1. Construct the input features
var lineFeature = new Feature("LINESTRING(0 0,100 0,100 50)");
var pointOnEndpointFeature = new Feature("POINT(0 0)");
// 2. Pass them to a processing API
var lines = new Collection<Feature>() { lineFeature };
var points = new Collection<Feature>() { pointOnEndpointFeature };
var result = TopologyValidator.LineEndPointsMustTouchPoints(lines, points);
// 3. Retrieve result features
Collection<Feature> invalidFeatures = result.InvalidFeatures;
// 4. Add features to display layers
var validatedLayer = (InMemoryFeatureLayer)MapView.FindFeatureLayer("Validated Features");
var filterLayer = (InMemoryFeatureLayer)MapView.FindFeatureLayer("Filter Features");
var resultLayer = (InMemoryFeatureLayer)MapView.FindFeatureLayer("Result Features");
validatedLayer.Open();
filterLayer.Open();
resultLayer.Open();
validatedLayer.Clear();
filterLayer.Clear();
resultLayer.Clear();
validatedLayer.InternalFeatures.Add(lineFeature);
filterLayer.InternalFeatures.Add(pointOnEndpointFeature);
foreach (var f in invalidFeatures)
{
resultLayer.InternalFeatures.Add(f);
}
validatedLayer.Close();
filterLayer.Close();
resultLayer.Close();
await MapView.RefreshAsync();
Source: winformHowDoI:Samples/VectorDataTopologicalValidation/ValidateLineTopology.cs.
Full Example — Loading Features from an XML Data Source¶
Demonstrates the WKT constructor, ColumnValues population, and GeoCollection<Feature> keyed insertion:
public static GeoCollection<Feature> LoadInputFeatures()
{
GeoCollection<Feature> features = new GeoCollection<Feature>();
XElement xElement = XElement.Load(inputFeaturesXmlPath);
foreach (var featureXElement in xElement.Descendants("Feature"))
{
if (string.IsNullOrWhiteSpace(featureXElement.Value))
continue;
// Construct from WKT stored in element text
Feature feature = new Feature(featureXElement.Value);
// Populate ColumnValues from XML attributes
foreach (var xAttribute in featureXElement.Attributes())
{
feature.ColumnValues[xAttribute.Name.LocalName] = xAttribute.Value;
}
// Add to collection using the "id" attribute as the key
features.Add(featureXElement.Attribute("id").Value, feature);
}
return features;
}
Source: blazorHowDoI:Shared/GeometricFunctionHelper.cs.
Full Example — Geoprocessing: Derive New Features from Existing Ones¶
Demonstrates the BaseShape constructor, GetShape() with a cast, and ColumnValues assignment for programmatic feature creation:
public static IEnumerable<Feature> GetSnappingBufferFeatures(IEnumerable<Feature> features)
{
foreach (var feature in features)
{
// Extract the shape and cast to the expected type
MultilineShape multilineShape = feature.GetShape() as MultilineShape;
if (multilineShape == null)
continue;
foreach (var vertex in multilineShape.Lines.SelectMany(l => l.Vertices))
{
double radiusInMeters = Conversion.ConvertMeasureUnits(100, DistanceUnit.Feet, DistanceUnit.Meter);
// Create a derived shape
EllipseShape ellipseShape = new EllipseShape(new PointShape(vertex), Math.Round(radiusInMeters, 2));
// Wrap it as a Feature using the BaseShape constructor
Feature tempFeature = new Feature(ellipseShape);
// Tag the feature with a style-driving column value
tempFeature.ColumnValues["Name"] = "SnappingBuffer";
yield return tempFeature;
}
}
}
Source: blazorHowDoI:Shared/GeometricFunctionHelper.cs.
Common Patterns and Pitfalls¶
GetShape() returns BaseShape — always cast before using geometry-specific members. The return type is the abstract base class. If you know the layer contains polygons, cast to PolygonShape or MultipolygonShape; if it might return either, use as and null-check.
Spatial query results have empty ColumnValues by default. ReturningColumnsType.NoColumns is the default in many samples because it's the fastest option when you only need the geometry. If you need attribute data from query results, pass ReturningColumnsType.AllColumns or a specific list of column names.
The layer must be open before querying or mutating InternalFeatures. Both Open() and Close() are synchronous. Always call Open() before accessing InternalFeatures or QueryTools, and Close() when done.
InternalFeatures.Clear() is not the same as layer.Clear(). InternalFeatures.Clear() removes features from the backing collection. layer.Clear() is a convenience method that opens the layer, clears features, and may reset columns depending on the overload. Both appear in the samples; check which one is appropriate for your scenario.
feature.Id may be empty for hand-constructed features. When you create features with new Feature(wktString) without an explicit ID, the Id is an empty string. If you use InternalFeatures.Add(feature) with an empty ID multiple times, only the last one will be retained since GeoCollection uses the ID as its key. Either supply distinct IDs via the constructor overload or use the explicit-key add overload: InternalFeatures.Add(uniqueKey, feature).
ColumnValues keys are case-sensitive. The key "NAME" and "Name" are different entries. Always match the exact casing used in the layer's column definitions.
Source References¶
| Sample | Namespace | Feature usage |
|---|---|---|
VectorDataTopologicalValidation/ValidateLineTopology | WPF, WinForms, MAUI | WKT constructor (LINESTRING, POINT, MULTILINESTRING); Collection<Feature> input sets; InternalFeatures.Add; result feature iteration |
VectorDataTopologicalValidation/ValidatePolygonTopology | WPF, WinForms, MAUI | WKT constructor (POLYGON, MULTIPOLYGON); same pattern |
VectorDataSpatialQuery/FindFeaturesWithinADistance | WinForms | layer.QueryTools.GetFeaturesWithinDistanceOf returning Collection<Feature>; InternalFeatures.Clear + Add |
ThinkGeoCloudIntegration/ReverseGeocoding | WinForms | new Feature(locationFeature.GetShape()); feature.GetBoundingBox() |
Shared/GeometricFunctionHelper.cs | Blazor | WKT constructor; ColumnValues population; GeoCollection<Feature>; GetShape() with MultilineShape cast; new Feature(ellipseShape) BaseShape constructor |